Comprehensive admin + user dashboards (production-ready)
This commit is contained in:
@@ -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." };
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user