Comprehensive admin + user dashboards (production-ready)

This commit is contained in:
Leon Serfaty
2026-06-07 17:54:30 -04:00
parent 155507f21a
commit f033f00379
122 changed files with 7878 additions and 805 deletions
+60 -16
View File
@@ -1,22 +1,55 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Mic2, Plus } from "lucide-react";
import type { Prisma, EpisodeStatus } from "@prisma/client";
import { requireAuth } from "@/lib/auth/guards";
import { prisma } from "@/lib/db";
import { PageHeader } from "@/components/app/page-header";
import { EpisodeCard } from "@/components/app/episode-card";
import { SearchInput, FilterSelect } from "@/components/admin/ui/table-controls";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
export const metadata: Metadata = { title: "Episodes" };
export default async function EpisodesPage() {
const STATUS_OPTIONS = [
{ value: "DRAFT", label: "Draft" },
{ value: "QUEUED", label: "Queued" },
{ value: "SCRIPTING", label: "Writing script" },
{ value: "SYNTHESIZING", label: "Recording audio" },
{ value: "STITCHING", label: "Mixing audio" },
{ value: "ART", label: "Designing art" },
{ value: "SAVING", label: "Finalizing" },
{ value: "READY", label: "Ready" },
{ value: "FAILED", label: "Failed" },
];
const VALID_STATUSES = new Set(STATUS_OPTIONS.map((o) => o.value));
export default async function EpisodesPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; status?: string }>;
}) {
const session = await requireAuth();
const { q, status } = await searchParams;
const query = q?.trim();
const statusFilter = status && VALID_STATUSES.has(status) ? status : undefined;
const where: Prisma.EpisodeWhereInput = {
userId: session.user.id,
...(query ? { title: { contains: query, mode: "insensitive" } } : {}),
...(statusFilter ? { status: statusFilter as EpisodeStatus } : {}),
};
const episodes = await prisma.episode.findMany({
where: { userId: session.user.id },
where,
orderBy: { createdAt: "desc" },
include: { coverArt: { select: { storageKey: true } } },
});
const filtering = Boolean(query || statusFilter);
return (
<>
<PageHeader
@@ -31,21 +64,32 @@ export default async function EpisodesPage() {
}
/>
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center">
<SearchInput placeholder="Search episodes…" />
<FilterSelect param="status" placeholder="Status" options={STATUS_OPTIONS} allLabel="All statuses" />
</div>
{episodes.length === 0 ? (
<div className="flex flex-col items-center gap-3 rounded-2xl border border-dashed border-border py-16 text-center">
<span className="flex h-14 w-14 items-center justify-center rounded-2xl bg-brand/10 text-brand">
<Mic2 className="h-6 w-6" />
</span>
<div>
<p className="font-medium">No episodes yet</p>
<p className="text-sm text-muted-foreground">Create your first AI-produced episode.</p>
</div>
<Button asChild>
<Link href="/episodes/new">
<Plus className="h-4 w-4" /> New episode
</Link>
</Button>
</div>
filtering ? (
<EmptyState
icon={Mic2}
title="No matching episodes"
description="Try a different search term or status filter."
/>
) : (
<EmptyState
icon={Mic2}
title="No episodes yet"
description="Create your first AI-produced episode."
action={
<Button asChild>
<Link href="/episodes/new">
<Plus className="h-4 w-4" /> New episode
</Link>
</Button>
}
/>
)
) : (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
{episodes.map((ep) => (