Repository Public
1import Link from "next/link";2import { AutoRefresh } from "@/components/studio/AutoRefresh";3import { A, Notice, PageHead, Status, ago, usd } from "@/components/studio/ui";4import { requireRole } from "@/lib/auth";5import { sql } from "@/lib/db";67export const metadata = { title: "Projects" };8export default async function Projects({ searchParams }: { searchParams: Promise<{ ok?: string; err?: string; all?: string }> }) {9 const sp = await searchParams;10 await requireRole("viewer");11 const rows = await sql<{ id: string; title: string; status: string; mode: string; language: string; running_stage: string | null; blocked_reason: string | null; error: string | null; updated_at: Date; series: string | null; usd: number; shots: number; thumb: string | null; ext: string | null; origin: string }[]>`12 SELECT p.id, p.title, p.status, p.mode, p.language, p.running_stage, p.blocked_reason, p.error, p.updated_at, p.origin, s.title_en AS series,13 COALESCE((SELECT SUM(usd) FROM cost_entries c WHERE c.project_id = p.id), 0)::float8 AS usd,14 (SELECT COUNT(*)::int FROM shots x WHERE x.project_id = p.id) AS shots,15 (SELECT a.sha256 FROM shots x JOIN assets a ON a.id = x.image_asset_id WHERE x.project_id = p.id ORDER BY x.idx LIMIT 1) AS thumb,16 (SELECT a.ext FROM shots x JOIN assets a ON a.id = x.image_asset_id WHERE x.project_id = p.id ORDER BY x.idx LIMIT 1) AS ext17 FROM projects p LEFT JOIN series s ON s.id = p.series_id18 WHERE ${sp.all ? sql`true` : sql`p.status <> 'archived'`} ORDER BY p.updated_at DESC LIMIT 100`;19 return (20 <>21 <AutoRefresh seconds={8} active={rows.some((r) => r.running_stage)} />22 <PageHead title="Projects" sub="Every film in development, production or release."><Link href={sp.all ? "/studio/projects" : "/studio/projects?all=1"} className="btn btn-ghost btn-sm">{sp.all ? "Hide archived" : "Show archived"}</Link><Link href="/studio/projects/new" className="btn btn-primary">New film</Link></PageHead>23 <Notice ok={sp.ok} err={sp.err} />24 {rows.length ? (25 <div className="card overflow-x-auto"><table className="studio-table"><thead><tr><th></th><th>Film</th><th>Status</th><th>Mode</th><th>Shots</th><th>Cost</th><th>Updated</th></tr></thead><tbody>26 {rows.map((r) => (27 <tr key={r.id}>28 <td className="w-12">{r.thumb ? <img src={`/media/${r.thumb}.${r.ext}`} alt="" className="h-16 w-9 rounded object-cover" loading="lazy" /> : <div className="h-16 w-9 rounded bg-[var(--color-stage)]" />}</td>29 <td><A href={`/studio/projects/${r.id}`}>{r.title}</A><p className="mt-0.5 text-xs text-[var(--color-faint)]">{r.series ?? "Standalone"} · {r.language} · {r.origin}</p>{(r.error || r.blocked_reason) && <p className="mt-1 max-w-md break-words text-xs text-[var(--color-bad)]">{r.blocked_reason ?? r.error}</p>}</td>30 <td>{r.running_stage ? <Status value="running" label={`running: ${r.running_stage}`} /> : r.blocked_reason ? <Status value="blocked" /> : r.error ? <Status value="failed" label={`${r.status.replace(/_/g, " ")} · error`} /> : <Status value={r.status} />}</td>31 <td className="text-[var(--color-muted)]">{r.mode}</td><td className="tabular-nums">{r.shots || "—"}</td><td className="tabular-nums">{usd(r.usd)}</td><td className="whitespace-nowrap text-[var(--color-muted)]">{ago(r.updated_at)}</td>32 </tr>33 ))}34 </tbody></table></div>35 ) : (36 <div className="card p-10 text-center"><p className="display text-3xl">No project yet</p><p className="mt-2 text-sm text-[var(--color-faint)]">Start in the Concept Lab: one theme is enough.</p><Link href="/studio/projects/new" className="btn btn-primary mt-6">Open the Concept Lab</Link></div>37 )}38 </>39 );40}41