import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { launchpadApi } from "../api/endpoints"; import type { CalendarItem, PostableArtifact, SocialAccounts } from "../api/types"; import { FilmIcon } from "../components/icons/Icons"; import { AttachmentChip, AttachPicker, LibraryPicker } from "../components/launchpad/Attachment"; import { PLATFORMS, ScheduleModal } from "../components/ScheduleModal"; import { monthGrid, SlotPicker, toWall, userTimeZone, wallToDate } from "../components/launchpad/SlotPicker"; import { BackArrow, ConfirmButton, CopyButton, errorMessage, formatDate, Loading, Modal, PageHeader, StatusPill, Tabs } from "../components/ui"; const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; /** A small mark on a calendar item that carries a video or images. */ function MediaMark({ item }: { item: CalendarItem }) { if (!item.artifact || item.artifact.media === "text") return null; return ( {item.artifact.media === "video" ? : ( )} ); } const sameDay = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); function ItemModal({ item, accounts, onClose, onChanged, onReconnect, }: { item: CalendarItem; accounts: SocialAccounts | null; onClose: () => void; onChanged: (item: CalendarItem | null) => void; onReconnect: (platform: "x" | "linkedin") => void; }) { const [posts, setPosts] = useState([item.content, ...item.thread]); // Its time on the user's clock; one made before half-hour slots stays as it is until a new slot is picked. const [when, setWhen] = useState(() => toWall(new Date(item.scheduled_at))); const moved = when !== toWall(new Date(item.scheduled_at)); const [accountId, setAccountId] = useState(item.social_account_id ?? ""); const [attached, setAttached] = useState(item.artifact); const [picking, setPicking] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const meta = PLATFORMS.find((p) => p.id === item.platform)!; const locked = item.status === "posted" || item.status === "publishing"; const options = (accounts?.accounts ?? []).filter((a) => a.platform === item.platform); // Not on the calendar to go out (a draft, failed, reminded or paused post): Save puts it back, at the time shown. const reschedules = item.status !== "scheduled"; const save = async () => { if ((moved || reschedules) && wallToDate(when) <= new Date()) return setError("Pick a time that is still ahead."); setBusy(true); setError(null); try { const clean = posts.map((p) => p.trim()).filter(Boolean); onChanged( await launchpadApi.update(item.id, { content: clean[0], thread: clean.slice(1), ...(moved || reschedules ? { local_time: when, timezone: userTimeZone() } : {}), ...(reschedules ? { status: "scheduled" as const } : {}), social_account_id: accountId || null, ...((attached?.id ?? null) !== item.artifact_id ? { artifact_id: attached?.id ?? null } : {}), }), ); onClose(); } catch (e) { setError(errorMessage(e)); } finally { setBusy(false); } }; return (
{item.auto_post ? `Auto posts as ${item.account_handle}` : "Email reminder"} · {formatDate(item.scheduled_at, true)}
{item.kit_id && (

Written from the launch kit: {item.kit_title ?? "a launch kit"}

)} {item.status === "paused" ? (
{item.error ?? "Paused: it won't go out until resumed."} {(item.platform === "x" || item.platform === "linkedin") && accounts?.accounts.some((a) => a.platform === item.platform && a.needs_reconnect) && ( )} Pick a time still ahead and Save to resume it.
) : item.error &&

{item.error}

} {item.external_url && (

View the live post

)} {(item.status === "posted" || item.clicks > 0) && (
{Object.entries(item.metrics) .filter(([k]) => k !== "synced_at") .map(([k, v]) => ( {v} {k} ))} {item.clicks} link clicks
)} {(attached || !locked) && (
Attachment {attached ? ( setPicking(true)} onRemove={locked ? undefined : () => setAttached(null)} /> ) : ( )}
)} {posts.map((p, i) => (