Skip to content

Commit 20bdd0d

Browse files
Miriaddashboard
andcommitted
feat: add real-time dashboard polling + activity feed + refresh buttons
- SectionCardsLive: client component that polls /api/dashboard/metrics every 30s - RecentActivity: live feed of latest content ideas, videos, and sponsor leads - PageRefreshButton: manual refresh for Content, Videos, Sponsors pages - API routes: /api/dashboard/metrics and /api/dashboard/activity - Dashboard home page updated with live components Co-authored-by: dashboard <dashboard@miriad.systems>
1 parent 2597e92 commit 20bdd0d

File tree

9 files changed

+412
-53
lines changed

9 files changed

+412
-53
lines changed

app/(dashboard)/dashboard/content/page.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const dynamic = "force-dynamic";
22

33
import { dashboardQuery } from "@/lib/sanity/dashboard";
44
import { ContentIdeasTable } from "./content-ideas-table";
5+
import { PageRefreshButton } from "@/components/page-refresh-button";
56

67
interface ContentIdea {
78
_id: string;
@@ -36,13 +37,17 @@ export default async function ContentPage() {
3637

3738
return (
3839
<div className="flex flex-col gap-6">
39-
<div>
40-
<h1 className="text-3xl font-bold tracking-tight">
41-
Content Ideas
42-
</h1>
43-
<p className="text-muted-foreground">
44-
Manage content ideas — approve, reject, or review incoming topics.
45-
</p>
40+
<div className="flex items-center justify-between">
41+
<div>
42+
<h1 className="text-3xl font-bold tracking-tight">
43+
Content Ideas
44+
</h1>
45+
<p className="text-muted-foreground">
46+
Manage content ideas — approve, reject, or review incoming
47+
topics.
48+
</p>
49+
</div>
50+
<PageRefreshButton />
4651
</div>
4752

4853
<ContentIdeasTable ideas={ideas} />

app/(dashboard)/dashboard/page.tsx

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,33 @@
11
export const dynamic = "force-dynamic";
22

3-
import { SectionCards } from "@/components/section-cards"
3+
import { SectionCardsLive } from "@/components/section-cards-live";
4+
import { RecentActivity } from "@/components/recent-activity";
45

56
export default function DashboardPage() {
6-
return (
7-
<div className="flex flex-col gap-6">
8-
<div>
9-
<h1 className="text-3xl font-bold tracking-tight">
10-
Content Ops Dashboard
11-
</h1>
12-
<p className="text-muted-foreground">
13-
Overview of your automated content engine videos, sponsors, and
14-
pipeline health.
15-
</p>
16-
</div>
7+
return (
8+
<div className="flex flex-col gap-6">
9+
<div>
10+
<h1 className="text-3xl font-bold tracking-tight">
11+
Content Ops Dashboard
12+
</h1>
13+
<p className="text-muted-foreground">
14+
Overview of your automated content engine \u2014 videos, sponsors,
15+
and pipeline health.
16+
</p>
17+
</div>
1718

18-
<SectionCards />
19+
<SectionCardsLive />
1920

20-
<div className="grid gap-4 md:grid-cols-2">
21-
<div className="rounded-lg border p-6">
22-
<h2 className="text-lg font-semibold">Recent Activity</h2>
23-
<p className="mt-2 text-sm text-muted-foreground">
24-
Activity feed will show recent content publications, sponsor
25-
updates, and pipeline events.
26-
</p>
27-
</div>
28-
<div className="rounded-lg border p-6">
29-
<h2 className="text-lg font-semibold">Upcoming Schedule</h2>
30-
<p className="mt-2 text-sm text-muted-foreground">
31-
Cadence calendar showing scheduled content drops and sponsor
32-
deliverables.
33-
</p>
34-
</div>
35-
</div>
36-
</div>
37-
)
21+
<div className="grid gap-4 md:grid-cols-2">
22+
<RecentActivity />
23+
<div className="rounded-lg border p-6">
24+
<h2 className="text-lg font-semibold">Pipeline Status</h2>
25+
<p className="mt-2 text-sm text-muted-foreground">
26+
Real-time view of content moving through the pipeline.
27+
</p>
28+
{/* Pipeline status will be added here */}
29+
</div>
30+
</div>
31+
</div>
32+
);
3833
}

app/(dashboard)/dashboard/sponsors/page.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { dashboardQuery } from "@/lib/sanity/dashboard";
44
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
55
import { SponsorLeadsTable } from "./sponsor-leads-table";
66
import { SponsorPoolTable } from "./sponsor-pool-table";
7+
import { PageRefreshButton } from "@/components/page-refresh-button";
78

89
interface SponsorLead {
910
_id: string;
@@ -74,14 +75,17 @@ export default async function SponsorsPage() {
7475

7576
return (
7677
<div className="flex flex-col gap-6">
77-
<div>
78-
<h1 className="text-3xl font-bold tracking-tight">
79-
Sponsor Pipeline
80-
</h1>
81-
<p className="text-muted-foreground">
82-
Manage sponsor leads, track deals through the pipeline, and browse the
83-
sponsor pool.
84-
</p>
78+
<div className="flex items-center justify-between">
79+
<div>
80+
<h1 className="text-3xl font-bold tracking-tight">
81+
Sponsor Pipeline
82+
</h1>
83+
<p className="text-muted-foreground">
84+
Manage sponsor leads, track deals through the pipeline, and
85+
browse the sponsor pool.
86+
</p>
87+
</div>
88+
<PageRefreshButton />
8589
</div>
8690

8791
<Tabs defaultValue="pipeline">

app/(dashboard)/dashboard/videos/page.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const dynamic = "force-dynamic";
22

33
import { dashboardQuery } from "@/lib/sanity/dashboard";
44
import { VideosTable } from "./videos-table";
5+
import { PageRefreshButton } from "@/components/page-refresh-button";
56

67
interface AutomatedVideo {
78
_id: string;
@@ -43,13 +44,17 @@ export default async function VideosPage() {
4344

4445
return (
4546
<div className="flex flex-col gap-6">
46-
<div>
47-
<h1 className="text-3xl font-bold tracking-tight">
48-
Automated Videos
49-
</h1>
50-
<p className="text-muted-foreground">
51-
Monitor the video pipeline — from script generation to publishing.
52-
</p>
47+
<div className="flex items-center justify-between">
48+
<div>
49+
<h1 className="text-3xl font-bold tracking-tight">
50+
Automated Videos
51+
</h1>
52+
<p className="text-muted-foreground">
53+
Monitor the video pipeline — from script generation to
54+
publishing.
55+
</p>
56+
</div>
57+
<PageRefreshButton />
5358
</div>
5459

5560
<VideosTable videos={videos} />
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { NextResponse } from "next/server";
2+
import { dashboardQuery } from "@/lib/sanity/dashboard";
3+
4+
export const dynamic = "force-dynamic";
5+
6+
interface ActivityItem {
7+
_id: string;
8+
_type: string;
9+
_updatedAt: string;
10+
title?: string;
11+
companyName?: string;
12+
status?: string;
13+
}
14+
15+
export async function GET() {
16+
try {
17+
const items = await dashboardQuery<ActivityItem[]>(`
18+
*[_type in ["contentIdea", "automatedVideo", "sponsorLead"]] | order(_updatedAt desc) [0..9] {
19+
_id,
20+
_type,
21+
_updatedAt,
22+
title,
23+
companyName,
24+
status
25+
}
26+
`);
27+
28+
return NextResponse.json(items ?? []);
29+
} catch (error) {
30+
console.error("Failed to fetch activity:", error);
31+
return NextResponse.json([]);
32+
}
33+
}

app/api/dashboard/metrics/route.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { NextResponse } from "next/server";
2+
import { dashboardQuery } from "@/lib/sanity/dashboard";
3+
4+
export const dynamic = "force-dynamic";
5+
6+
export async function GET() {
7+
try {
8+
const [videosPublished, flaggedVideos, newIdeas, sponsorPipeline] =
9+
await Promise.all([
10+
dashboardQuery<number>(
11+
`count(*[_type == "automatedVideo" && status == "published"])`,
12+
),
13+
dashboardQuery<number>(
14+
`count(*[_type == "automatedVideo" && status == "flagged"])`,
15+
),
16+
dashboardQuery<number>(
17+
`count(*[_type == "contentIdea" && status == "new"])`,
18+
),
19+
dashboardQuery<number>(
20+
`count(*[_type == "sponsorLead" && status != "paid"])`,
21+
),
22+
]);
23+
24+
return NextResponse.json({
25+
videosPublished: videosPublished ?? 0,
26+
flaggedForReview: (flaggedVideos ?? 0) + (newIdeas ?? 0),
27+
sponsorPipeline: sponsorPipeline ?? 0,
28+
revenue: null,
29+
});
30+
} catch (error) {
31+
console.error("Failed to fetch dashboard metrics:", error);
32+
return NextResponse.json(
33+
{ videosPublished: 0, flaggedForReview: 0, sponsorPipeline: 0, revenue: null },
34+
{ status: 500 },
35+
);
36+
}
37+
}

components/page-refresh-button.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use client";
2+
3+
import { useRouter } from "next/navigation";
4+
import { useState } from "react";
5+
import { Button } from "@/components/ui/button";
6+
import { RefreshCw } from "lucide-react";
7+
8+
export function PageRefreshButton() {
9+
const router = useRouter();
10+
const [isRefreshing, setIsRefreshing] = useState(false);
11+
12+
const handleRefresh = () => {
13+
setIsRefreshing(true);
14+
router.refresh();
15+
setTimeout(() => setIsRefreshing(false), 1000);
16+
};
17+
18+
return (
19+
<Button
20+
variant="outline"
21+
size="sm"
22+
className="gap-2"
23+
onClick={handleRefresh}
24+
disabled={isRefreshing}
25+
>
26+
<RefreshCw
27+
className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
28+
/>
29+
Refresh
30+
</Button>
31+
);
32+
}

components/recent-activity.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"use client";
2+
3+
import { useEffect, useState, useCallback } from "react";
4+
import { Badge } from "@/components/ui/badge";
5+
import { Lightbulb, FileVideo, Handshake, Clock } from "lucide-react";
6+
7+
interface ActivityItem {
8+
_id: string;
9+
_type: string;
10+
_updatedAt: string;
11+
title?: string;
12+
companyName?: string;
13+
status?: string;
14+
}
15+
16+
const typeConfig: Record<
17+
string,
18+
{ icon: typeof Lightbulb; label: string; color: string }
19+
> = {
20+
contentIdea: {
21+
icon: Lightbulb,
22+
label: "Idea",
23+
color:
24+
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
25+
},
26+
automatedVideo: {
27+
icon: FileVideo,
28+
label: "Video",
29+
color:
30+
"bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
31+
},
32+
sponsorLead: {
33+
icon: Handshake,
34+
label: "Sponsor",
35+
color:
36+
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
37+
},
38+
};
39+
40+
function formatTimeAgo(dateString: string) {
41+
const seconds = Math.floor(
42+
(Date.now() - new Date(dateString).getTime()) / 1000,
43+
);
44+
if (seconds < 60) return "just now";
45+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
46+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
47+
return `${Math.floor(seconds / 86400)}d ago`;
48+
}
49+
50+
export function RecentActivity() {
51+
const [items, setItems] = useState<ActivityItem[]>([]);
52+
53+
const fetchActivity = useCallback(async () => {
54+
try {
55+
const res = await fetch("/api/dashboard/activity");
56+
if (res.ok) {
57+
const data = await res.json();
58+
setItems(data);
59+
}
60+
} catch (error) {
61+
console.error("Failed to fetch activity:", error);
62+
}
63+
}, []);
64+
65+
useEffect(() => {
66+
fetchActivity();
67+
const interval = setInterval(fetchActivity, 30000);
68+
return () => clearInterval(interval);
69+
}, [fetchActivity]);
70+
71+
return (
72+
<div className="rounded-lg border p-6">
73+
<h2 className="text-lg font-semibold">Recent Activity</h2>
74+
{items.length === 0 ? (
75+
<p className="mt-4 text-sm text-muted-foreground">
76+
No activity yet \u2014 content will appear here as the pipeline
77+
runs.
78+
</p>
79+
) : (
80+
<div className="mt-4 space-y-3">
81+
{items.map((item) => {
82+
const config =
83+
typeConfig[item._type] ?? typeConfig.contentIdea;
84+
const Icon = config.icon;
85+
const name =
86+
item.title || item.companyName || "Untitled";
87+
return (
88+
<div
89+
key={item._id}
90+
className="flex items-center gap-3"
91+
>
92+
<Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
93+
<div className="min-w-0 flex-1">
94+
<p className="truncate text-sm font-medium">
95+
{name}
96+
</p>
97+
<div className="flex items-center gap-2">
98+
<Badge
99+
variant="outline"
100+
className={`text-xs ${config.color}`}
101+
>
102+
{config.label}
103+
</Badge>
104+
{item.status && (
105+
<span className="text-xs text-muted-foreground">
106+
{item.status}
107+
</span>
108+
)}
109+
</div>
110+
</div>
111+
<div className="flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
112+
<Clock className="h-3 w-3" />
113+
{formatTimeAgo(item._updatedAt)}
114+
</div>
115+
</div>
116+
);
117+
})}
118+
</div>
119+
)}
120+
</div>
121+
);
122+
}

0 commit comments

Comments
 (0)