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
+23
View File
@@ -0,0 +1,23 @@
export default function EpisodeLoading() {
return (
<div className="animate-pulse space-y-6">
<div className="space-y-2">
<div className="h-9 w-72 rounded-xl bg-secondary" />
<div className="h-4 w-48 rounded-lg bg-secondary/70" />
</div>
<div className="grid gap-6 lg:grid-cols-3">
<div className="space-y-6 lg:col-span-1">
<div className="aspect-square rounded-2xl border border-border bg-secondary" />
<div className="h-32 rounded-2xl border border-border bg-card" />
<div className="h-11 rounded-full bg-secondary" />
</div>
<div className="space-y-4 lg:col-span-2">
<div className="h-14 rounded-2xl border border-border bg-card" />
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-40 rounded-2xl border border-border bg-card" />
))}
</div>
</div>
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
import Link from "next/link";
import { MicOff } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function EpisodeNotFound() {
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center gap-5 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-secondary text-muted-foreground">
<MicOff className="h-7 w-7" />
</div>
<div className="space-y-1.5">
<h1 className="font-display text-2xl font-extrabold tracking-tight">Episode not found</h1>
<p className="max-w-sm text-sm text-muted-foreground">
This episode doesn&apos;t exist, or you don&apos;t have access to it.
</p>
</div>
<Button asChild>
<Link href="/episodes">Back to episodes</Link>
</Button>
</div>
);
}
+6 -1
View File
@@ -32,7 +32,11 @@ export default async function EpisodePage({ params }: { params: Promise<{ id: st
<PageHeader
title={episode.title}
description={`${episode.format.replace("_", "-").toLowerCase()} · ${episode.language.toUpperCase()} · ${episode.targetLengthMin} min`}
action={!inProgress ? <EpisodeActions episodeId={episode.id} /> : undefined}
action={
!inProgress ? (
<EpisodeActions episodeId={episode.id} initialShareId={episode.shareId} />
) : undefined
}
/>
{episode.status === "FAILED" || inProgress ? (
@@ -68,6 +72,7 @@ export default async function EpisodePage({ params }: { params: Promise<{ id: st
<AudioPlayer
storageKey={episode.audioAsset.storageKey}
durationSec={episode.audioAsset.durationSec}
episodeId={episode.id}
/>
</CardContent>
</Card>