"use client"; import { useRef, useState } from "react"; import { SiteConfigSchema } from "@/lib/schemas/site-config"; import type { ProjectSnapshot } from "@/lib/project-store"; import type { EbookProject } from "@/lib/ebook-project-store"; type ProjectsPanelProps = { projects: ProjectSnapshot[]; suggestedName: string; canSave: boolean; onSave: (name: string) => void; onLoad: (id: string) => void; onDelete: (id: string) => void; onImport: (snapshot: ProjectSnapshot) => void; /** Publish an ebook project to the Library — returns the slug on success */ onPublish?: (project: ProjectSnapshot) => Promise; /** Unpublish (remove from library) a published project */ onUnpublish?: (project: ProjectSnapshot) => Promise; /** Called after a cover or author image is uploaded, to persist the new URL */ onUpdateImages?: (id: string, coverImageUrl?: string, authorImageUrl?: string) => Promise; }; function exportProject(p: ProjectSnapshot) { const json = JSON.stringify(p, null, 2); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${p.name.replace(/[^a-z0-9]/gi, "_").toLowerCase()}_nexus.json`; a.click(); URL.revokeObjectURL(url); } export function ProjectsPanel({ projects: allProjects, suggestedName, canSave, onSave, onLoad, onDelete, onImport, onPublish, onUnpublish, onUpdateImages, }: ProjectsPanelProps) { const projects = allProjects; const [name, setName] = useState(suggestedName); const [confirmDelete, setConfirmDelete] = useState(null); const [confirmUnpublish, setConfirmUnpublish] = useState(null); const [importError, setImportError] = useState(null); const [publishingId, setPublishingId] = useState(null); const [unpublishingId, setUnpublishingId] = useState(null); const [imageUploading, setImageUploading] = useState<{ id: string; type: "cover" | "author" } | null>(null); const imageTargetRef = useRef<{ id: string; type: "cover" | "author" } | null>(null); const imageInputRef = useRef(null); const fileInputRef = useRef(null); async function handleImageFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; e.target.value = ""; const target = imageTargetRef.current; if (!file || !target || !onUpdateImages) return; setImageUploading(target); try { const presignRes = await fetch("/api/r2-presign", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename: file.name, contentType: file.type, prefix: "images" }), }); if (!presignRes.ok) throw new Error("Could not get upload URL."); const { presignedUrl, publicUrl } = await presignRes.json() as { presignedUrl: string; publicUrl: string | null }; if (!presignedUrl || !publicUrl) throw new Error("Storage not configured."); const uploadRes = await fetch(presignedUrl, { method: "PUT", headers: { "Content-Type": file.type }, body: file, }); if (!uploadRes.ok) throw new Error("Upload failed."); await onUpdateImages( target.id, target.type === "cover" ? publicUrl : undefined, target.type === "author" ? publicUrl : undefined, ); } catch { /* silently ignore — user can retry */ } finally { setImageUploading(null); imageTargetRef.current = null; } } function handleFileImport(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const parsed = JSON.parse(ev.target?.result as string) as Record; const freshId = `proj-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; const defaultSiteConfig = SiteConfigSchema.parse({}); let snapshot: ProjectSnapshot; if ("jobState" in parsed && parsed.jobState && typeof parsed.jobState === "object") { // EbookProject format — exported from the /ebook Projects tab const ep = parsed as unknown as EbookProject; snapshot = { id: freshId, name: (ep.name || ep.bookTitle || "Imported Ebook") as string, createdAt: (ep.createdAt ?? new Date().toISOString()) as string, updatedAt: new Date().toISOString(), academy: null, siteConfig: defaultSiteConfig, deliveryInstructions: "", chatHistory: [], blueprint: null, logicResult: null, uiResult: null, ebookManifest: null, ebookJobState: ep.jobState, }; } else if ( // Raw EbookJobState — e.g. pasted from localStorage "status" in parsed && "transcripts" in parsed ) { const rawJob = parsed as unknown as EbookProject["jobState"]; snapshot = { id: freshId, name: "Imported Ebook", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), academy: null, siteConfig: defaultSiteConfig, deliveryInstructions: "", chatHistory: [], blueprint: null, logicResult: null, uiResult: null, ebookManifest: null, ebookJobState: rawJob, }; } else if (parsed.id && parsed.name) { // Standard ProjectSnapshot format snapshot = { ...(parsed as unknown as ProjectSnapshot), id: freshId, updatedAt: new Date().toISOString(), }; } else { throw new Error("Unrecognised file format."); } onImport(snapshot); setImportError(null); } catch (err) { const detail = err instanceof Error ? ` (${err.message})` : ""; setImportError(`Could not read file — make sure it's a valid Nexus project or ebook export.${detail}`); } }; reader.readAsText(file); // reset so the same file can be re-selected if needed e.target.value = ""; } return (

Projects

Saved Workspaces

{/* Import button */}
{importError && (

{importError}

)} {/* Save current project */} {canSave && (
setName(e.target.value)} placeholder="Project name…" className="min-h-12 flex-1 rounded-xl border border-slate-600 bg-slate-800/60 px-4 text-base text-slate-100 placeholder-slate-500 focus:border-cyan-500 focus:outline-none" />
)} {/* Project list */} {projects.length === 0 ? (

No saved projects yet.

Run the pipeline, then save your work here to come back to it later.

) : (
{projects.map((p) => (

{p.name}

{p.ebookManifest ? `${p.ebookManifest.chapters.length} chapter${p.ebookManifest.chapters.length !== 1 ? "s" : ""} · ${p.ebookManifest.totalWordCount.toLocaleString()} words` : p.ebookJobState ? `Ebook in progress · ${p.ebookJobState.status ?? ""}` : p.academy ? `${p.academy.curriculum.length} module${p.academy.curriculum.length !== 1 ? "s" : ""} · ${p.academy.curriculum.flatMap((m) => m.lessons).length} lessons` : null} {" · "} {new Date(p.updatedAt).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric", })}

{confirmDelete === p.id ? (
) : ( )}
{/* Book images — only for ebook projects */} {onUpdateImages && (p.ebookJobState || p.ebookManifest) && (
{/* Cover image slot */} {/* Author photo slot */}

{p.coverImageUrl ? "Cover uploaded ✓" : "Tap to add cover"}

{p.authorImageUrl ? "Author photo set ✓" : "Tap ○ for author photo"}

)} {/* Publish / Published row — shown when project has ebook content */} {onPublish && (p.ebookJobState || p.ebookManifest) && (
{p.publishedSlug ? ( <> View in Library {/* Unpublish */} {onUnpublish && ( confirmUnpublish === p.id ? (
) : ( ) )} ) : ( )}
)}
))}
)}
); }