const { useState, useMemo, useEffect, useRef } = React; /* ---- small custom glyphs for brand icons lucide doesn't ship ---- */ function TikTokGlyph({ size = 15, color = "currentColor" }) { return ( ); } function ThreadsGlyph({ size = 15, color = "currentColor" }) { return ( ); } function BlueskyGlyph({ size = 15, color = "currentColor" }) { return ( ); } function GBPGlyph({ size = 18, color = "currentColor" }) { return ( ); } const PLATFORMS = { facebook: { label: "Facebook", icon: Facebook, color: "#1877F2", soft: "#E9F1FE", limit: 63206 }, twitter: { label: "Twitter", icon: Twitter, color: "#1DA1F2", soft: "#E8F7FE", limit: 280 }, instagram: { label: "Instagram", icon: Instagram, color: "#C13584", soft: "#FBEAF3", limit: 2200 }, linkedin: { label: "LinkedIn", icon: Linkedin, color: "#0A66C2", soft: "#E8F1FA", limit: 3000 }, youtube: { label: "YouTube", icon: Youtube, color: "#FF0000", soft: "#FFEAEA", limit: 5000 }, pinterest: { label: "Pinterest", icon: Pin, color: "#E60023", soft: "#FDE9EC", limit: 500 }, tiktok: { label: "TikTok", icon: TikTokGlyph, color: "#161823", soft: "#EFEFF2", limit: 2200 }, threads: { label: "Threads", icon: ThreadsGlyph, color: "#161823", soft: "#F1E9FA", limit: 500 }, bluesky: { label: "Bluesky", icon: BlueskyGlyph, color: "#1185FE", soft: "#E7F2FE", limit: 300 }, google: { label: "Google Business", icon: GBPGlyph, color: "#EA9F00", soft: "#FBF1DC", limit: 1500 }, }; /* short blurb for each platform's card on the "works everywhere" showcase */ const PLATFORM_BLURB = { facebook: "Publish to pages, groups, and personal profiles from one calendar.", linkedin: "Schedule to company pages and personal profiles.", twitter: "Post updates and threads, queued for exactly the right moment.", instagram: "Schedule feed posts, Reels, and Stories from a single queue.", google: "Keep your local listing fresh with scheduled updates.", pinterest: "Queue pins across every board without lifting a finger.", tiktok: "Line up short-form video drops ahead of time.", youtube: "Schedule uploads and track how each one performs.", threads: "Publish to Threads right alongside your other channels.", bluesky: "Schedule posts to Bluesky as the network grows.", }; /* decorative-only icons for the floating background field (not connectable platforms) */ const FLOATERS = [ { icon: Twitter, color: "#1DA1F2", top: "10%", left: "6%", dur: 7, delay: 0 }, { icon: Youtube, color: "#FF0000", top: "34%", left: "4%", dur: 9, delay: 0.6 }, { icon: Linkedin, color: "#0A66C2", top: "58%", left: "8%", dur: 8, delay: 1.1 }, { icon: Cloud, color: "#00A4EF", top: "78%", left: "16%", dur: 10, delay: 0.3 }, { icon: Instagram, color: "#C13584", top: "82%", left: "44%", dur: 8.5, delay: 0.8 }, { icon: TikTokGlyph, color: "#161823", top: "80%", left: "68%", dur: 7.5, delay: 0.2 }, { icon: Zap, color: "#FF8A00", top: "84%", left: "90%", dur: 9.5, delay: 1.3 }, { icon: Pin, color: "#E60023", top: "56%", left: "92%", dur: 8, delay: 0.5 }, { icon: HardDrive, color: "#34A853", top: "32%", left: "94%", dur: 10.5, delay: 0.9 }, { icon: Facebook, color: "#1877F2", top: "10%", left: "92%", dur: 7.2, delay: 0.4 }, { icon: ThreadsGlyph, color: "#161823", top: "12%", left: "70%", dur: 8.8, delay: 1.5 }, { icon: BlueskyGlyph, color: "#1185FE", top: "8%", left: "40%", dur: 6.5, delay: 0.1 }, ]; const INK = "#0F1E17"; const MUTED = "#647065"; const BORDER = "#E1E5DF"; const BG = "#F7F8F5"; const ACCENT = "#17A673"; const ACCENT_DEEP = "#0E8A5E"; const CORAL = "#FF6B4A"; const AMBER = "#C88A00"; const AMBER_SOFT = "#FBF1DC"; const GREEN = "#17A673"; const GREEN_SOFT = "#E4F6EE"; function uid() { return Math.random().toString(36).slice(2, 9); } function formatWhen(iso) { const d = new Date(iso); const day = d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }); const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }); return { day, time }; } function defaultDateTime(minutesAhead = 60) { const d = new Date(Date.now() + minutesAhead * 60000); d.setSeconds(0, 0); const tzOffset = d.getTimezoneOffset() * 60000; return new Date(d - tzOffset).toISOString().slice(0, 16); } /* ============================= LOGO ============================= */ function RecuraLogo({ size = 28 }) { return ( ); } /* ============================= MASCOT ============================= */ /* Recura's own round little mascot — an original character, not any existing brand's. */ function RecuraMascot({ size = 92 }) { return (
{/* ears */} {/* head */} {/* face */} {/* eyes */} {/* blush */} {/* smile */} {/* waving hand */}
); } /* ============================= NAVBAR ============================= */ function Navbar({ view, setView, loggedIn, onLogout, plan }) { const [scrolled, setScrolled] = useState(false); const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 8); window.addEventListener("scroll", onScroll); return () => window.removeEventListener("scroll", onScroll); }, []); return (
setView(loggedIn ? "app" : "landing")} > Recura
{!loggedIn && ( )}
{loggedIn ? ( <> ) : ( <> )}
); } /* ============================= LANDING ============================= */ function Landing({ setView }) { const [email, setEmail] = useState(""); return (
{/* animated gradient blobs */}
{/* floating platform badges */} {FLOATERS.map((f, i) => { const Icon = f.icon; return (
); })}
Now scheduling to 10 platforms

Social moves faster than your dashboards.

Your social media,
on autopilot.

Write a post once, connect every account you run, and let Recura queue it up for the right time — automatically.

Create a full week of social content in 10 minutes. Grow your audience with Recura's Nexa social media AI.

{[ { icon: Repeat, label: "Recycle Posts", delay: "0s" }, { icon: LayoutTemplate, label: "Inspiration Templates", delay: "0.4s" }, { icon: Rocket, label: "Nexa AI Assist", delay: "0.8s" }, ].map((f, i) => { const Icon = f.icon; return ( {f.label} ); })}
setEmail(e.target.value)} placeholder="Enter your email…" style={{ flex: 1, border: "none", outline: "none", padding: "10px 12px", fontSize: 14, background: "transparent", color: INK }} />

No credit card required. Disconnect anytime.

{/* ---- works-everywhere platform showcase (colorful cards, like the reference) ---- */}

One queue. Every platform.

Connect once and Recura handles the rest, wherever your audience is.

{Object.entries(PLATFORMS).map(([key, p], i) => { const Icon = p.icon; return (

{p.label}

{PLATFORM_BLURB[key]}

); })}
); } /* ============================= LOGIN ============================= */ function Login({ setView, onAuthed, startStep }) { const [mode, setMode] = useState("login"); const [step, setStep] = useState(startStep || "credentials"); // credentials | phone | otp const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [phone, setPhone] = useState(""); const [otp, setOtp] = useState(["", "", "", "", "", ""]); const [error, setError] = useState(false); const [errorMsg, setErrorMsg] = useState(""); const [devCode, setDevCode] = useState(null); // only ever set when the server runs OTP_PROVIDER=dev const [resendIn, setResendIn] = useState(0); const [busy, setBusy] = useState(false); const otpRefs = useRef([]); useEffect(() => { if (resendIn <= 0) return; const t = setInterval(() => setResendIn((s) => Math.max(0, s - 1)), 1000); return () => clearInterval(t); }, [resendIn]); const shake = () => { setError(true); setTimeout(() => setError(false), 400); }; const submitCredentials = async () => { if (!email.trim() || !password.trim()) return shake(); setBusy(true); try { // Try logging in first; if there's no account yet, register instead. let u; try { u = await Api.login(email.trim(), password); } catch (loginErr) { u = await Api.register(email.trim(), password); } setErrorMsg(""); onAuthed && onAuthed(); if (u && u.phone_verified) setView("connect"); else setStep("phone"); } catch (e) { setErrorMsg(e.message || "Something went wrong"); shake(); } finally { setBusy(false); } }; const sendCode = async () => { const digits = phone.replace(/\D/g, ""); if (digits.length < 10) { setErrorMsg("Enter your 10-digit mobile number."); return shake(); } setBusy(true); try { const res = await Api.sendOtp(phone); setDevCode(res.dev_code || null); setErrorMsg(""); setOtp(["", "", "", "", "", ""]); setResendIn(30); setStep("otp"); setTimeout(() => otpRefs.current[0]?.focus(), 50); } catch (e) { setErrorMsg(e.message || "Couldn't send the code."); shake(); } finally { setBusy(false); } }; const updateOtpDigit = (i, val) => { const digit = val.replace(/\D/g, "").slice(-1); setOtp((o) => { const next = [...o]; next[i] = digit; return next; }); if (digit && i < 5) otpRefs.current[i + 1]?.focus(); }; const onOtpKeyDown = (i, e) => { if (e.key === "Backspace" && !otp[i] && i > 0) otpRefs.current[i - 1]?.focus(); }; const verifyOtp = async () => { const entered = otp.join(""); if (entered.length < 6) { setErrorMsg("Enter all 6 digits."); return shake(); } setBusy(true); try { await Api.verifyOtp(phone, entered); setErrorMsg(""); onAuthed && onAuthed(); setView("connect"); } catch (e) { setErrorMsg(e.message || "Wrong code."); setOtp(["", "", "", "", "", ""]); setTimeout(() => otpRefs.current[0]?.focus(), 50); shake(); } finally { setBusy(false); } }; return (
Recura
{/* step indicator */}
{["credentials", "phone", "otp"].map((s, i) => ( ))}
{step === "credentials" && ( <>

{mode === "login" ? "Welcome back" : "Create your account"}

{mode === "login" ? "Log in to manage your queue." : "Start scheduling in under a minute."}

setEmail(e.target.value)} placeholder="you@example.com" style={{ border: "none", outline: "none", flex: 1, fontSize: 13.5, background: "transparent", color: INK }} />
setPassword(e.target.value)} placeholder="••••••••" style={{ border: "none", outline: "none", flex: 1, fontSize: 13.5, background: "transparent", color: INK }} />
or continue with
{["G", "in", ""].map((letter, i) => ( ))}

{mode === "login" ? "New to Recura?" : "Already have an account?"}{" "} setMode(mode === "login" ? "signup" : "login")} style={{ color: ACCENT, fontWeight: 600, cursor: "pointer" }}> {mode === "login" ? "Sign up" : "Log in"}

)} {step === "phone" && ( <>

Verify your phone

We'll text a 6-digit code to confirm it's you.

setPhone(e.target.value)} placeholder="98765 43210 (or +country code)" inputMode="tel" autoComplete="tel" style={{ border: "none", outline: "none", flex: 1, fontSize: 13.5, background: "transparent", color: INK }} />
{errorMsg &&

{errorMsg}

}

setStep("credentials")} style={{ textAlign: "center", fontSize: 12.5, color: MUTED, cursor: "pointer", margin: 0 }}> ← Back

)} {step === "otp" && ( <>

Enter the code

Sent to {phone || "your phone"}

{devCode ? (

Test mode (no SMS sent) — your code is {devCode}

) : (
)}
{otp.map((d, i) => ( (otpRefs.current[i] = el)} value={d} onChange={(e) => updateOtpDigit(i, e.target.value)} onKeyDown={(e) => onOtpKeyDown(i, e)} maxLength={1} inputMode="numeric" className="auth-input" style={{ flex: "1 1 0", minWidth: 0, maxWidth: 42, height: 50, textAlign: "center", fontSize: 19, fontWeight: 700, borderRadius: 10, border: `1.5px solid ${BORDER}`, outline: "none", color: INK, fontFamily: "'JetBrains Mono', monospace", }} /> ))}
{errorMsg &&

{errorMsg}

}

{resendIn > 0 ? ( <>Resend code in {resendIn}s ) : ( Resend code )} {" · "} setStep("phone")} style={{ color: MUTED, cursor: "pointer" }}> Change number

)}
); } /* ============================= CONNECT ACCOUNTS ============================= */ function Connect({ connected, setConnected, setView, accounts, refreshAccounts }) { const [authModal, setAuthModal] = useState(null); // platform key pending authorization const [authing, setAuthing] = useState(false); const [blueskyHandle, setBlueskyHandle] = useState(""); const [blueskyPw, setBlueskyPw] = useState(""); const [connectError, setConnectError] = useState(""); const accountIdFor = (key) => { const a = accounts.find((a) => a.platform === key); return a ? a.id : null; }; const disconnect = async (key) => { const id = accountIdFor(key); if (id) { try { await Api.disconnectAccount(id); } catch (e) { /* ignore, still refresh below */ } } await refreshAccounts(); }; const requestConnect = (key) => { setConnectError(""); setAuthModal(key); }; const confirmAuthorize = async () => { if (authModal === "bluesky") { if (!blueskyHandle.trim() || !blueskyPw.trim()) { setConnectError("Enter your handle and an app password."); return; } setAuthing(true); try { await Api.connectBluesky(blueskyHandle.trim(), blueskyPw.trim()); await refreshAccounts(); setAuthing(false); setAuthModal(null); setBlueskyHandle(""); setBlueskyPw(""); } catch (e) { setAuthing(false); setConnectError(e.message || "Couldn't connect Bluesky"); } return; } // Every other platform is real OAuth: leave the app and let the backend // + the platform's own login screen take over. It redirects back here // (?connected=platform&status=ok|error) when done. window.location.href = Api.oauthStartUrl(authModal); }; return (

Connect your accounts

Pick every platform you post to. You can add or remove accounts anytime.

{/* platform grid */}
{Object.entries(PLATFORMS).map(([key, p], i) => { const Icon = p.icon; const isConnected = connected.includes(key); return (
{p.label}
); })}
{/* vertical connected rail */}

Connected

{connected.length === 0 ? "Nothing connected yet." : `${connected.length} account${connected.length === 1 ? "" : "s"} linked`}

{connected.length === 0 ? (
Tap "Connect" on any platform to add it here.
) : (
{connected.map((key, i) => { const p = PLATFORMS[key]; const Icon = p.icon; return (
{p.label}
); })}
)}
{authModal && (
!authing && setAuthModal(null)} >
e.stopPropagation()} style={{ background: "#fff", borderRadius: 18, padding: 28, width: "100%", maxWidth: 360, boxShadow: "0 24px 60px rgba(15,30,23,0.25)", textAlign: "center" }} > {(() => { const Icon = PLATFORMS[authModal].icon; return ; })()}

Connect {PLATFORMS[authModal].label}

{authModal === "bluesky" ? "Bluesky doesn't use app-store style login — paste your handle and an app password (create one in the Bluesky app under Settings > App Passwords)." : "You'll be taken to " + PLATFORMS[authModal].label + " to sign in and approve access. Recura never sees your password."}

{authModal === "bluesky" && !authing && (
setBlueskyHandle(e.target.value)} placeholder="you.bsky.social" style={{ border: `1.5px solid ${BORDER}`, borderRadius: 10, padding: "10px 12px", fontSize: 13.5, outline: "none" }} /> setBlueskyPw(e.target.value)} type="password" placeholder="xxxx-xxxx-xxxx-xxxx (app password)" style={{ border: `1.5px solid ${BORDER}`, borderRadius: 10, padding: "10px 12px", fontSize: 13.5, outline: "none" }} />
)} {connectError && (

{connectError}

)} {authing ? (
Authorizing…
) : (
)}
)}
); } /* ============================= COMPOSER BOX ============================= */ function ComposerBox({ text, setText, overLimit, tightestLimit, selected, image, setImage }) { const [focused, setFocused] = useState(false); const fileInputRef = useRef(null); const pct = tightestLimit ? Math.min(100, (text.length / tightestLimit) * 100) : 0; const barColor = overLimit ? "#D64545" : pct > 80 ? AMBER : ACCENT; const onPickFile = (e) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => setImage({ name: file.name, url: reader.result, file }); reader.readAsDataURL(file); e.target.value = ""; }; return (