import { useCallback, useEffect, useMemo, useState } from "react"; import { Archive, CalendarPlus, Check, FolderUp, Image as ImageIcon, MessageSquareWarning, RefreshCw, Upload } from "lucide-react"; import { api } from "../api.js"; import { useAuth } from "../App.jsx"; import { Badge, Button, cx, Dialog, Empty, ErrorNote, Field, Panel, Spinner, timeAgo } from "../ui.jsx"; import PlatformPicker, { defaultPlatforms, selectionFor, useClientPlatforms } from "../PlatformPicker.jsx"; const TONE = { submitted: "brand", changes: "warn", approved: "ok", scheduled: "neutral", archived: "neutral" }; const LABEL = { submitted: "To review", changes: "Changes requested", approved: "Ready to schedule", scheduled: "Scheduled", archived: "Archived" }; const FORMATS = [["IMAGE", "Single image"], ["REELS", "Reel (one video)"], ["CAROUSEL", "Carousel (2–10 files)"], ["STORY", "Story"]]; const fileUrl = (p) => `/api/library/file?path=${encodeURIComponent(p)}`; const isVid = (p) => /\.(mp4|mov|m4v)$/i.test(p || ""); const baseName = (p) => String(p).split(/[\\/]/).pop().replace(/^\d+-\w{4}-/, ""); async function uploadFiles(client, files, onProgress) { const paths = []; for (const [n, file] of [...files].entries()) { onProgress?.(`Uploading ${n + 1} of ${files.length}: ${file.name}`); const res = await fetch("/api/library/upload", { method: "POST", credentials: "same-origin", headers: { "X-Client": client, "X-Filename": encodeURIComponent(file.name), "Content-Type": "application/octet-stream" }, body: file }); const j = await res.json().catch(() => ({})); if (!res.ok) throw new Error(j.error || `Upload failed (${file.name})`); paths.push(j.path); } onProgress?.(null); return paths; } export default function Library() { const { isAdmin } = useAuth(); const [clients, setClients] = useState([]); const [client, setClient] = useState(""); const [tab, setTab] = useState(isAdmin ? "submitted" : "changes"); const [data, setData] = useState(null); const [error, setError] = useState(null); const [dialog, setDialog] = useState(null); // {kind:'upload'} | {kind:'review'|'schedule'|'version', item} | {kind:'bulk'} const [picked, setPicked] = useState([]); const canBulk = tab === "approved" && Boolean(client); useEffect(() => { setPicked([]); }, [client, tab]); useEffect(() => { api("/api/overview").then((r) => setClients(r.clients)).catch(setError); }, []); const load = useCallback(() => api(`/api/library${client ? `?client=${client}` : ""}`).then(setData).catch(setError), [client]); useEffect(() => { load(); }, [load]); const items = useMemo(() => (data?.items || []).filter((i) => tab === "all" ? i.status !== "archived" : i.status === tab), [data, tab]); const count = (s) => (data?.items || []).filter((i) => i.status === s).length; const name = (slug) => clients.find((c) => c.slug === slug)?.name || slug; const done = () => { setDialog(null); load(); }; return (

Content library

Designers and editors upload finished work here. Approved work is scheduled from here or picked up by the agent for the weekly plan.

{["submitted", "changes", "approved", "scheduled", "all"].map((k) => ( ))} {canBulk && } {tab === "approved" && !client && Pick a client to schedule several at once.}
{!data ? : !items.length ? ( {tab === "submitted" ? "No work waiting for review." : tab === "approved" ? "No approved work waiting to be scheduled." : "No items in this view."} ) : (
{items.map((i) => (
{canBulk && ( )}
{LABEL[i.status]} {i.format.toLowerCase()}{i.files.length > 1 ? ` · ${i.files.length} files` : ""}{i.version > 1 ? ` · v${i.version}` : ""}
{i.title}
{name(i.client)} · {i.uploadedBy} · {timeAgo(i.updatedAt)}{i.taskRef ? ` · task #${i.taskRef}` : ""}{i.forDate ? ` · for ${i.forDate}` : ""}
{i.captionIdea &&

{i.captionIdea}

} {i.notes &&

Note: {i.notes}

} {i.status === "changes" && i.reviewNote &&

{i.reviewedBy}: {i.reviewNote}

}
{isAdmin && ["submitted", "changes"].includes(i.status) && } {i.status === "changes" && } {["approved", "scheduled"].includes(i.status) && } {i.status !== "archived" && }
))}
)} {dialog?.kind === "upload" && setDialog(null)} onDone={() => { setTab("submitted"); done(); }} />} {dialog?.kind === "version" && setDialog(null)} onDone={done} />} {dialog?.kind === "review" && setDialog(null)} onDone={done} />} {dialog?.kind === "bulk" && picked.includes(i.id))} onClose={() => setDialog(null)} onDone={() => { setPicked([]); done(); }} />} {dialog?.kind === "schedule" && setDialog(null)} onDone={done} />}
); } function Preview({ item, big }) { const [idx, setIdx] = useState(0); const f = item.files[idx]; return (
{isVid(f) ?
); } function FilePicker({ files, setFiles, accept = "image/jpeg,image/png,video/mp4,video/quicktime" }) { return (
{files.map((f, n) => {n + 1}. {f.name} {(f.size / 1048576).toFixed(1)} MB )}
); } function UploadDialog({ clients, defaultClient, onClose, onDone }) { const [f, setF] = useState({ client: defaultClient || clients[0]?.slug || "", title: "", format: "IMAGE", captionIdea: "", notes: "", taskRef: "", forDate: "", planItem: "" }); const [planned, setPlanned] = useState([]); const [files, setFiles] = useState([]); const [tasks, setTasks] = useState([]); const [progress, setProgress] = useState(null); const [error, setError] = useState(null); const set = (k, v) => setF((s) => ({ ...s, [k]: v })); useEffect(() => { api("/api/team").then((r) => setTasks(r.tasks || [])).catch(() => {}); }, []); useEffect(() => { if (!f.client) return; const from = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10), to = new Date(Date.now() + 60 * 86400000).toISOString().slice(0, 10); api(`/api/calendar?client=${f.client}&from=${from}&to=${to}`).then((r) => setPlanned(r.items.filter((i) => i.kind === "plan" && ["idea", "brief", "production"].includes(i.stage)))).catch(() => setPlanned([])); }, [f.client]); const pickPlan = (id) => { const p = planned.find((x) => x.id === id); setF((s) => ({ ...s, planItem: id, ...(p ? { title: s.title || p.title, format: p.format === "TEXT" ? s.format : p.format, forDate: p.date, captionIdea: s.captionIdea || p.caption || "", taskRef: p.taskRef || s.taskRef } : {}) })); }; useEffect(() => { if (files.length > 1 && f.format !== "CAROUSEL") set("format", "CAROUSEL"); else if (files.length === 1 && isVid(files[0].name) && f.format === "IMAGE") set("format", "REELS"); }, [files]); // eslint-disable-line const submit = async () => { setError(null); try { const paths = await uploadFiles(f.client, files, setProgress); await api("/api/library", { method: "POST", body: { ...f, files: paths } }); onDone(); } catch (e) { setError(e); setProgress(null); } }; return ( }> {planned.length > 0 && ( )}
set("title", e.target.value)} /> set("forDate", e.target.value)} />
{progress &&

{progress}

}