SPB Git

Repository Public

page.tsx 3.7 KB 41 lines tsx

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
Files main
Menu

Theme