import { useEffect, useState, type FormEvent } from "react"; import { launchpadApi } from "../api/endpoints"; import type { CalendarItem, Platform, SocialAccount } from "../api/types"; import { errorMessage, Modal } from "./ui"; export const PLATFORMS: { id: Platform; label: string; limit: number; auto: boolean }[] = [ { id: "x", label: "X", limit: 280, auto: true }, { id: "linkedin", label: "LinkedIn", limit: 3000, auto: true }, { id: "bluesky", label: "Bluesky", limit: 300, auto: true }, { id: "substack_notes", label: "Substack Notes", limit: 2000, auto: false }, ]; export function toLocalInput(date: Date): string { const pad = (n: number) => String(n).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; } export function nextSlot(): Date { const d = new Date(Date.now() + 60 * 60 * 1000); d.setMinutes(0, 0, 0); return d; } /** Schedules one post (or a thread) on the Launchpad. Posts auto publish when an account is connected. */ export function ScheduleModal({ platform: initialPlatform, posts, artifactId, documentId, onClose, onScheduled, }: { platform: Platform; posts: string[]; artifactId?: string | null; documentId?: string | null; onClose: () => void; onScheduled?: (item: CalendarItem) => void; }) { const [platform, setPlatform] = useState(initialPlatform); const [texts, setTexts] = useState(posts.length ? posts : [""]); const [when, setWhen] = useState(toLocalInput(nextSlot())); const [accounts, setAccounts] = useState([]); const [accountId, setAccountId] = useState(""); const [remind, setRemind] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const meta = PLATFORMS.find((p) => p.id === platform)!; const threadable = platform === "x" || platform === "bluesky"; useEffect(() => { launchpadApi.accounts().then((a) => setAccounts(a.accounts), () => setAccounts([])); }, []); const options = accounts.filter((a) => a.platform === platform && a.status === "active"); useEffect(() => { setAccountId(options[0]?.id ?? ""); // eslint-disable-next-line react-hooks/exhaustive-deps }, [platform, accounts.length]); const submit = async (e: FormEvent) => { e.preventDefault(); setBusy(true); setError(null); const clean = texts.map((t) => t.trim()).filter(Boolean); const merged = threadable ? clean : [clean.join("\n\n")]; try { const item = await launchpadApi.create({ platform, scheduled_at: new Date(when).toISOString(), content: merged[0] ?? "", thread: merged.slice(1), artifact_id: artifactId ?? null, document_id: documentId ?? null, social_account_id: meta.auto && accountId ? accountId : null, remind_by_email: remind || !meta.auto || !accountId, }); onScheduled?.(item); onClose(); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } }; return (
{meta.auto && ( )}
{!meta.auto &&

Substack has no posting API, so we email you the note at send time, ready to paste.

} {meta.auto && !options.length && (

No {meta.label} account connected. Connect one on the Launchpad to auto post; until then you get an email reminder.

)} {texts.map((t, i) => (