// app.jsx — Sendly main app (i18n + live AI + PayPal) const { useState, useEffect, useMemo, useRef } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": ["#9B5DE5", "#F15BB5", "#FEE440", "#00BBF9", "#00F5D4"], "previewShape": "postcard", "showDoodles": true, "confettiOnSend": true, "fontHeadline": "Bricolage Grotesque", "language": "en" }/*EDITMODE-END*/; const FONT_HEADLINE_OPTIONS = ["Bricolage Grotesque", "Fraunces", "DM Serif Display", "Space Grotesk"]; const PALETTE_OPTIONS = [ ["#FF4FA3", "#FFD23F", "#5BE0B8", "#66B6FF", "#7A4DFF"], ["#FF6B4A", "#FFCB45", "#F08CB0", "#5BC8B5", "#7A4DFF"], ["#E94B8A", "#FF8E3C", "#FFE066", "#3DB5C4", "#5E55E0"], ["#9B5DE5", "#F15BB5", "#FEE440", "#00BBF9", "#00F5D4"], ]; function App() { const [tw, setTweak] = useTweaks(TWEAK_DEFAULTS); const t = useMemo(() => makeT(tw.language), [tw.language]); const langInfo = LANGUAGES_15.find(l => l.code === tw.language) || LANGUAGES_15[0]; // form state const [step, setStep] = useState(1); const [recipient, setRecipient] = useState(""); const [family, setFamily] = useState(""); const [age, setAge] = useState(""); const [sender, setSender] = useState(""); const [occasionId, setOccasionId] = useState("birthday"); const [messageMode, setMessageMode] = useState("ai"); // 'ai' | 'mine' | 'mix' const [userText, setUserText] = useState(""); const [aiText, setAiText] = useState(""); const [generating, setGenerating] = useState(false); const [channel, setChannel] = useState("email"); const [contact, setContact] = useState(""); const [delivery, setDelivery] = useState("now"); const [scheduledFor, setScheduledFor] = useState(""); const [sent, setSent] = useState(false); const [checkingOut, setCheckingOut] = useState(false); const occasion = OCCASIONS.find(o => o.id === occasionId); const isSolemnMode = occasion && occasion.mood === "solemn"; /* ---- Document language + direction ---- */ useEffect(() => { document.documentElement.lang = langInfo.code; document.documentElement.dir = langInfo.dir; }, [langInfo]); /* ---- Palette via CSS vars ---- */ useEffect(() => { const [pink, yellow, mint, sky, plum] = tw.palette; const root = document.documentElement; if (!isSolemnMode) { root.style.setProperty("--pink", pink); root.style.setProperty("--yellow", yellow); root.style.setProperty("--mint", mint); root.style.setProperty("--sky", sky); root.style.setProperty("--plum", plum); } else { root.style.removeProperty("--pink"); root.style.removeProperty("--yellow"); root.style.removeProperty("--mint"); root.style.removeProperty("--sky"); root.style.removeProperty("--plum"); } }, [tw.palette, isSolemnMode]); useEffect(() => { document.documentElement.style.setProperty("--font-display", `"${tw.fontHeadline}", system-ui, sans-serif`); }, [tw.fontHeadline]); useEffect(() => { document.body.classList.toggle("mode-solemn", !!isSolemnMode); }, [isSolemnMode]); /* ---- AI message generation via window.claude.complete ---- */ // Debounced + cancellable: a new run replaces the previous result. const generationKey = useRef(0); useEffect(() => { if (messageMode === "mine") { setAiText(""); return; } // Need at least the basics to write a personalised note if (!recipient.trim() || !sender.trim()) { setAiText(""); return; } const myKey = ++generationKey.current; setGenerating(true); const timer = setTimeout(async () => { try { const text = await generateAIMessage({ occasionId, recipient, family, age, sender, langCode: tw.language, langLabel: langInfo.label, occasionName: t("occ." + occasionId), }); if (myKey === generationKey.current) setAiText(text); } finally { if (myKey === generationKey.current) setGenerating(false); } }, 700); return () => clearTimeout(timer); }, [occasionId, recipient, family, age, sender, messageMode, tw.language]); function regenAi() { generationKey.current++; setAiText(""); setGenerating(true); (async () => { const myKey = ++generationKey.current; try { const text = await generateAIMessage({ occasionId, recipient, family, age, sender, langCode: tw.language, langLabel: langInfo.label, occasionName: t("occ." + occasionId), }); if (myKey === generationKey.current) setAiText(text); } finally { if (myKey === generationKey.current) setGenerating(false); } })(); } const composedBody = useMemo(() => { if (messageMode === "mine") return userText; if (messageMode === "ai") return aiText; return [userText, aiText].filter(Boolean).join("\n\n— ✦ —\n\n"); }, [messageMode, userText, aiText]); const previewProps = { occasion, recipient, family, sender, body: composedBody, contact, t }; /* ---- step gating ---- */ const canAdvance = (() => { if (step === 1) return recipient.trim() && sender.trim(); if (step === 2) return !!occasionId; if (step === 3) { if (messageMode === "mine") return userText.trim().length > 0; if (messageMode === "mix") return userText.trim().length > 0 || aiText.trim().length > 0; return aiText.trim().length > 0; } if (step === 4) { if (channel === "email") return /.+@.+\..+/.test(contact); return contact.replace(/\D/g, "").length >= 7; } return true; })(); function next() { if (canAdvance) setStep(s => Math.min(s + 1, 5)); } function back() { setStep(s => Math.max(s - 1, 1)); } async function handleStripeCheckout() { setCheckingOut(true); try { const res = await fetch("https://sentwithlove.vip/create-checkout.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ to: contact, channel, recipientName: [recipient, family].filter(Boolean).join(" "), senderName: sender, occasion: occasion ? t("occ." + occasion.id) : occasionId, body: composedBody, }), }); const data = await res.json(); if (data.url) window.location.href = data.url; } catch (err) { console.error("[Sendly] Checkout failed:", err); setCheckingOut(false); } } function handleReset() { setSent(false); setStep(1); setRecipient(""); setFamily(""); setAge(""); setSender(""); setOccasionId("birthday"); setMessageMode("ai"); setUserText(""); setAiText(""); setChannel("email"); setContact(""); } const stepTitle = t(`step${step}_title`); const stepSub = t(`step${step}_sub`); return (
{tw.showDoodles && }
{/* Top bar */}
SSendly
🌍
{/* Hero */} {!sent && (
{t("eyebrow")}

{t("hero_send_a")} {t("hero_happy")}
{t("hero_or")} {t("hero_heartfelt")} {t("hero_note")}
{t("hero_seconds")}

{t("hero_sub")}

{t("hero_price_line")} €1.99
)} {/* Composer */} {!sent ? (
{stepTitle} {stepSub}
{step === 1 && (
setRecipient(e.target.value)} placeholder={t("ph_recipient")} />
setFamily(e.target.value)} placeholder={t("ph_family")} />
setSender(e.target.value)} placeholder={t("ph_sender")} />
setAge(e.target.value)} placeholder={t("ph_age")} />
)} {step === 2 && ( {isSolemnMode && (
{t("condolence_note_strong")} {t("condolence_note_rest")}
)}
)} {step === 3 && (
{(messageMode === "mine" || messageMode === "mix") && (