Skip to content

Commit 48f44b9

Browse files
author
Miriad
committed
feat: Sanity-powered dashboard pages with real data
- Add dashboard Sanity client (lib/sanity/dashboard.ts) with write access - Replace content page placeholder with real Sanity-backed data table - Add content actions (approve/reject) as server actions - Create new Videos page with pipeline status tracking - Add video actions (regenerate script, retry render, publish anyway) - Update SectionCards to fetch live metrics from Sanity - Add Videos nav item to sidebar, update icons (Content=Lightbulb, Videos=FileVideo) - All pages gracefully handle empty state and query errors - Uses shadcn Table, Badge, Button, Tooltip components
1 parent 19a86d6 commit 48f44b9

File tree

9 files changed

+636
-152
lines changed

9 files changed

+636
-152
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"use server";
2+
3+
import { revalidatePath } from "next/cache";
4+
import { dashboardClient } from "@/lib/sanity/dashboard";
5+
6+
export async function approveIdea(id: string) {
7+
await dashboardClient.patch(id).set({ status: "approved" }).commit();
8+
revalidatePath("/dashboard/content");
9+
}
10+
11+
export async function rejectIdea(id: string) {
12+
await dashboardClient.patch(id).set({ status: "rejected" }).commit();
13+
revalidatePath("/dashboard/content");
14+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"use client";
2+
3+
import {
4+
Table,
5+
TableBody,
6+
TableCell,
7+
TableHead,
8+
TableHeader,
9+
TableRow,
10+
} from "@/components/ui/table";
11+
import { Badge } from "@/components/ui/badge";
12+
import { Button } from "@/components/ui/button";
13+
import { Lightbulb } from "lucide-react";
14+
import { approveIdea, rejectIdea } from "./actions";
15+
16+
interface ContentIdea {
17+
_id: string;
18+
_createdAt: string;
19+
title: string;
20+
status: "new" | "approved" | "rejected" | "published";
21+
source?: string;
22+
category?: string;
23+
}
24+
25+
const statusConfig: Record<
26+
string,
27+
{ label: string; className: string }
28+
> = {
29+
new: {
30+
label: "New",
31+
className: "bg-blue-100 text-blue-800 hover:bg-blue-100 dark:bg-blue-900 dark:text-blue-300",
32+
},
33+
approved: {
34+
label: "Approved",
35+
className: "bg-green-100 text-green-800 hover:bg-green-100 dark:bg-green-900 dark:text-green-300",
36+
},
37+
rejected: {
38+
label: "Rejected",
39+
className: "bg-red-100 text-red-800 hover:bg-red-100 dark:bg-red-900 dark:text-red-300",
40+
},
41+
published: {
42+
label: "Published",
43+
className: "bg-purple-100 text-purple-800 hover:bg-purple-100 dark:bg-purple-900 dark:text-purple-300",
44+
},
45+
};
46+
47+
function StatusBadge({ status }: { status: string }) {
48+
const config = statusConfig[status] ?? {
49+
label: status,
50+
className: "",
51+
};
52+
return (
53+
<Badge variant="outline" className={config.className}>
54+
{config.label}
55+
</Badge>
56+
);
57+
}
58+
59+
function formatDate(dateString: string) {
60+
return new Date(dateString).toLocaleDateString("en-US", {
61+
month: "short",
62+
day: "numeric",
63+
year: "numeric",
64+
});
65+
}
66+
67+
export function ContentIdeasTable({ ideas }: { ideas: ContentIdea[] }) {
68+
if (ideas.length === 0) {
69+
return (
70+
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
71+
<Lightbulb className="h-12 w-12 text-muted-foreground/50" />
72+
<h3 className="mt-4 text-lg font-semibold">No content ideas yet</h3>
73+
<p className="mt-2 text-sm text-muted-foreground">
74+
Content ideas will appear here once they&apos;re created in Sanity
75+
or generated by the automation pipeline.
76+
</p>
77+
</div>
78+
);
79+
}
80+
81+
return (
82+
<div className="rounded-lg border">
83+
<Table>
84+
<TableHeader>
85+
<TableRow>
86+
<TableHead>Title</TableHead>
87+
<TableHead>Status</TableHead>
88+
<TableHead>Source</TableHead>
89+
<TableHead>Category</TableHead>
90+
<TableHead>Created</TableHead>
91+
<TableHead className="text-right">Actions</TableHead>
92+
</TableRow>
93+
</TableHeader>
94+
<TableBody>
95+
{ideas.map((idea) => (
96+
<TableRow key={idea._id}>
97+
<TableCell className="font-medium">
98+
{idea.title || "Untitled"}
99+
</TableCell>
100+
<TableCell>
101+
<StatusBadge status={idea.status} />
102+
</TableCell>
103+
<TableCell className="text-muted-foreground">
104+
{idea.source || "\u2014"}
105+
</TableCell>
106+
<TableCell className="text-muted-foreground">
107+
{idea.category || "\u2014"}
108+
</TableCell>
109+
<TableCell className="text-muted-foreground">
110+
{formatDate(idea._createdAt)}
111+
</TableCell>
112+
<TableCell className="text-right">
113+
{idea.status === "new" && (
114+
<div className="flex justify-end gap-2">
115+
<form action={approveIdea.bind(null, idea._id)}>
116+
<Button size="sm" variant="outline" className="text-green-600 hover:text-green-700">
117+
Approve
118+
</Button>
119+
</form>
120+
<form action={rejectIdea.bind(null, idea._id)}>
121+
<Button size="sm" variant="outline" className="text-red-600 hover:text-red-700">
122+
Reject
123+
</Button>
124+
</form>
125+
</div>
126+
)}
127+
</TableCell>
128+
</TableRow>
129+
))}
130+
</TableBody>
131+
</Table>
132+
</div>
133+
);
134+
}
Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,45 @@
1-
export default function ContentPage() {
2-
return (
3-
<div className="flex flex-col gap-6">
4-
<div>
5-
<h1 className="text-3xl font-bold tracking-tight">
6-
Content Management
7-
</h1>
8-
<p className="text-muted-foreground">
9-
Manage content ideas and automated video pipeline.
10-
</p>
11-
</div>
1+
import { dashboardQuery } from "@/lib/sanity/dashboard";
2+
import { ContentIdeasTable } from "./content-ideas-table";
123

13-
<div className="grid gap-4 md:grid-cols-2">
14-
<div className="rounded-lg border p-6">
15-
<h2 className="text-lg font-semibold">Content Ideas</h2>
16-
<p className="mt-2 text-sm text-muted-foreground">
17-
List of contentIdea records from Sanity — topics, status, priority,
18-
and assigned categories. Will support filtering by status (draft,
19-
approved, published, flagged).
20-
</p>
21-
<div className="mt-4 rounded-md bg-muted p-4 text-sm text-muted-foreground">
22-
📋 Data table coming in Phase 1a
23-
</div>
24-
</div>
4+
interface ContentIdea {
5+
_id: string;
6+
_createdAt: string;
7+
title: string;
8+
status: "new" | "approved" | "rejected" | "published";
9+
source?: string;
10+
category?: string;
11+
}
12+
13+
const CONTENT_IDEAS_QUERY = `*[_type == "contentIdea"] | order(_createdAt desc) {
14+
_id,
15+
_createdAt,
16+
title,
17+
status,
18+
source,
19+
category
20+
}`;
21+
22+
export default async function ContentPage() {
23+
let ideas: ContentIdea[] = [];
24+
25+
try {
26+
ideas = await dashboardQuery<ContentIdea[]>(CONTENT_IDEAS_QUERY);
27+
} catch (error) {
28+
console.error("Failed to fetch content ideas:", error);
29+
}
30+
31+
return (
32+
<div className="flex flex-col gap-6">
33+
<div>
34+
<h1 className="text-3xl font-bold tracking-tight">
35+
Content Ideas
36+
</h1>
37+
<p className="text-muted-foreground">
38+
Manage content ideas \u2014 approve, reject, or review incoming topics.
39+
</p>
40+
</div>
2541

26-
<div className="rounded-lg border p-6">
27-
<h2 className="text-lg font-semibold">Automated Videos</h2>
28-
<p className="mt-2 text-sm text-muted-foreground">
29-
List of automatedVideo records — YouTube publish status, view
30-
counts, and linked content ideas. Will show pipeline progress from
31-
idea → script → video → published.
32-
</p>
33-
<div className="mt-4 rounded-md bg-muted p-4 text-sm text-muted-foreground">
34-
🎬 Video pipeline view coming in Phase 1a
35-
</div>
36-
</div>
37-
</div>
38-
</div>
39-
)
42+
<ContentIdeasTable ideas={ideas} />
43+
</div>
44+
);
4045
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"use server";
2+
3+
import { revalidatePath } from "next/cache";
4+
import { dashboardClient } from "@/lib/sanity/dashboard";
5+
6+
export async function regenerateScript(id: string) {
7+
await dashboardClient
8+
.patch(id)
9+
.set({ status: "script_gen" })
10+
.commit();
11+
revalidatePath("/dashboard/videos");
12+
}
13+
14+
export async function retryRender(id: string) {
15+
await dashboardClient
16+
.patch(id)
17+
.set({ status: "video_gen" })
18+
.commit();
19+
revalidatePath("/dashboard/videos");
20+
}
21+
22+
export async function publishAnyway(id: string) {
23+
await dashboardClient
24+
.patch(id)
25+
.set({ status: "published", flagged: false })
26+
.unset(["flaggedReason"])
27+
.commit();
28+
revalidatePath("/dashboard/videos");
29+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { dashboardQuery } from "@/lib/sanity/dashboard";
2+
import { VideosTable } from "./videos-table";
3+
4+
interface AutomatedVideo {
5+
_id: string;
6+
_createdAt: string;
7+
title: string;
8+
status: "script_gen" | "audio_gen" | "video_gen" | "published" | "failed";
9+
flagged: boolean;
10+
flaggedReason?: string;
11+
duration?: number;
12+
}
13+
14+
const VIDEOS_QUERY = `*[_type == "automatedVideo"] | order(_createdAt desc) {
15+
_id,
16+
_createdAt,
17+
title,
18+
status,
19+
flagged,
20+
flaggedReason,
21+
duration
22+
}`;
23+
24+
export default async function VideosPage() {
25+
let videos: AutomatedVideo[] = [];
26+
27+
try {
28+
videos = await dashboardQuery<AutomatedVideo[]>(VIDEOS_QUERY);
29+
} catch (error) {
30+
console.error("Failed to fetch videos:", error);
31+
}
32+
33+
return (
34+
<div className="flex flex-col gap-6">
35+
<div>
36+
<h1 className="text-3xl font-bold tracking-tight">
37+
Automated Videos
38+
</h1>
39+
<p className="text-muted-foreground">
40+
Monitor the video pipeline \u2014 from script generation to publishing.
41+
</p>
42+
</div>
43+
44+
<VideosTable videos={videos} />
45+
</div>
46+
);
47+
}

0 commit comments

Comments
 (0)