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
+8
View File
@@ -10,6 +10,7 @@ import { enforceLimit, LimitExceededError } from "@/lib/usage/limits";
import { enqueueEpisodeGeneration } from "@/lib/queue/pgboss";
import { FORMAT_SPEAKERS } from "@/lib/episodes/options";
import { DEFAULT_VOICE_IDS, VOICE_CATALOG } from "@/lib/ai/voices";
import { isFlagEnabled } from "@/lib/flags";
const createSchema = z.object({
theme: z.string().min(5).max(500),
@@ -27,6 +28,9 @@ export async function createSeriesAction(
if (!(await subjectHasFeature(session.user.id, "series_generator", session.session.activeOrganizationId))) {
return { ok: false, error: "The series generator requires the Pro plan." };
}
if (!(await isFlagEnabled("episode_generation_enabled"))) {
return { ok: false, error: "Generation is temporarily paused. Please try again shortly." };
}
const parsed = createSchema.safeParse(input);
if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input." };
@@ -54,6 +58,10 @@ export async function generateFromSeriesAction(
const session = await getServerSession();
if (!session) return { ok: false, error: "You must be signed in." };
if (!(await isFlagEnabled("episode_generation_enabled"))) {
return { ok: false, error: "Episode generation is temporarily paused. Please try again shortly." };
}
const series = await prisma.series.findUnique({ where: { id: seriesId } });
if (!series || series.userId !== session.user.id) return { ok: false, error: "Not allowed." };
+15
View File
@@ -0,0 +1,15 @@
import { HeaderSkeleton, Skeleton } from "@/components/ui/skeleton";
export default function SeriesLoading() {
return (
<>
<HeaderSkeleton />
<Skeleton className="h-72 max-w-2xl rounded-2xl" />
<div className="mt-8 grid gap-3 sm:grid-cols-2">
{Array.from({ length: 2 }).map((_, i) => (
<Skeleton key={i} className="h-20 rounded-2xl" />
))}
</div>
</>
);
}
+13 -6
View File
@@ -8,6 +8,7 @@ import { PageHeader } from "@/components/app/page-header";
import { UpgradeGate } from "@/components/app/upgrade-gate";
import { SeriesCreateForm } from "@/components/app/series-create-form";
import { Card, CardContent } from "@/components/ui/card";
import { EmptyState } from "@/components/ui/empty-state";
export const metadata: Metadata = { title: "Series" };
@@ -47,13 +48,13 @@ export default async function SeriesPage() {
<SeriesCreateForm />
{series.length > 0 && (
<div className="mt-8 space-y-3">
<h2 className="text-sm font-semibold text-muted-foreground">Your seasons</h2>
<div className="mt-8 space-y-3">
<h2 className="text-sm font-semibold text-muted-foreground">Your seasons</h2>
{series.length > 0 ? (
<div className="grid gap-3 sm:grid-cols-2">
{series.map((s) => (
<Link key={s.id} href={`/series/${s.id}`}>
<Card className="transition-shadow hover:shadow-md">
<Card className="transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardContent className="flex items-center gap-3 py-4">
<span className="flex h-11 w-11 items-center justify-center rounded-xl bg-brand/10 text-brand">
<ListMusic className="h-5 w-5" />
@@ -69,8 +70,14 @@ export default async function SeriesPage() {
</Link>
))}
</div>
</div>
)}
) : (
<EmptyState
icon={ListMusic}
title="No seasons yet"
description="Plan a cohesive season above, then generate each episode in a couple of clicks."
/>
)}
</div>
</>
);
}