SPB Git

Repository Public

page.tsx 3.8 KB 39 lines tsx

1import { A, Notice, PageHead, Status, usd, when } from "@/components/studio/ui";2import { can, requireRole } from "@/lib/auth";3import { sql } from "@/lib/db";4import { featureFilm, filmStatus } from "../../actions";56export const metadata = { title: "Films" };7export default async function Films({ searchParams }: { searchParams: Promise<{ ok?: string; err?: string }> }) {8  const sp = await searchParams;9  const user = await requireRole("viewer");10  const editor = can(user, "editor");11  const films = await sql<{ id: string; slug: string; project_id: string | null; title_en: string; status: string; featured: boolean; published_at: Date | null; scheduled_for: Date | null; duration: number; views: number; likes: number; quality_score: number | null; thumb_sha: string | null; poster_sha: string; cost: number; series: string | null }[]>`12    SELECT f.*, s.title_en AS series, COALESCE((SELECT SUM(usd) FROM cost_entries c WHERE c.project_id = f.project_id), 0)::float8 AS cost13    FROM films f LEFT JOIN series s ON s.id = f.series_id ORDER BY COALESCE(f.published_at, f.scheduled_for, f.created_at) DESC`;14  return (15    <>16      <PageHead title="Films" sub="Release state of every film. Unpublishing or holding a film removes it from every public page and stops its media from being served within seconds." />17      <Notice ok={sp.ok} err={sp.err} />18      <div className="card overflow-x-auto"><table className="studio-table"><thead><tr><th></th><th>Film</th><th>State</th><th>Released</th><th>Views</th><th>Likes</th><th>Critic</th><th>Cost</th><th>Actions</th></tr></thead><tbody>19        {films.map((f) => (20          <tr key={f.id}>21            <td className="w-12"><img src={`/media/${f.thumb_sha ?? f.poster_sha}.jpg`} alt="" className="h-16 w-9 rounded object-cover" loading="lazy" /></td>22            <td>{f.status === "published" ? <a href={`/film/${f.slug}`} className="underline decoration-[var(--color-line)]">{f.title_en}</a> : f.title_en}{f.featured && <span className="ml-2 text-xs text-[var(--color-accent)]">hero</span>}<p className="text-xs text-[var(--color-faint)]">{f.series ?? "Standalone"} · {Number(f.duration).toFixed(0)} s{f.project_id && <> · <A href={`/studio/projects/${f.project_id}`}>project</A></>}</p></td>23            <td><Status value={f.status} /></td><td className="whitespace-nowrap text-[var(--color-muted)]">{f.status === "scheduled" ? `→ ${when(f.scheduled_for)}` : when(f.published_at)}</td>24            <td className="tabular-nums">{f.views}</td><td className="tabular-nums">{f.likes}</td><td className="tabular-nums">{f.quality_score?.toFixed(1) ?? "—"}</td><td className="tabular-nums">{usd(f.cost)}</td>25            <td>{editor && (26              <div className="flex flex-wrap gap-1.5">27                {f.status === "published" && !f.featured && <form action={featureFilm}><input type="hidden" name="film" value={f.id} /><button className="btn btn-ghost btn-sm">Make hero</button></form>}28                {f.status === "published" && <form action={filmStatus}><input type="hidden" name="film" value={f.id} /><input type="hidden" name="status" value="hold" /><button className="btn btn-ghost btn-sm">Hold</button></form>}29                {f.status !== "unpublished" && <form action={filmStatus}><input type="hidden" name="film" value={f.id} /><input type="hidden" name="status" value="unpublished" /><button className="btn btn-danger btn-sm">Unpublish</button></form>}30                {(f.status === "hold" || f.status === "unpublished") && <form action={filmStatus}><input type="hidden" name="film" value={f.id} /><input type="hidden" name="status" value="published" /><button className="btn btn-ghost btn-sm">Restore</button></form>}31              </div>)}</td>32          </tr>33        ))}34        {!films.length && <tr><td colSpan={9} className="text-[var(--color-faint)]">No film has been released yet.</td></tr>}35      </tbody></table></div>36    </>37  );38}39
Files main
Menu

Theme