Skip to content

Commit 00af3f8

Browse files
Miriaddashboard
andcommitted
fix: align schemas with @content types + add Supabase auth boundary
Schema fixes: - automatedVideo status: draft/script_ready/audio_gen/video_gen/flagged/uploading/published - flagged is a status value, not a boolean field - contentIdea uses sourceUrl/topics/collectedAt (not source/category) - section cards query uses status == 'flagged' Auth boundary: - Supabase middleware protecting /dashboard/* routes - Login page with email/password at /dashboard/login - Auth callback route for email confirmation - Dashboard layout conditionally renders sidebar based on auth - Sign-out action wired to NavUser dropdown - AppSidebar accepts real user data from Supabase Co-authored-by: dashboard <dashboard@miriad.systems>
1 parent 48f44b9 commit 00af3f8

File tree

13 files changed

+451
-197
lines changed

13 files changed

+451
-197
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use server";
2+
3+
import { redirect } from "next/navigation";
4+
import { createClient } from "@/lib/supabase/server";
5+
6+
export async function signOut() {
7+
const supabase = await createClient();
8+
await supabase.auth.signOut();
9+
redirect("/dashboard/login");
10+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createClient } from "@/lib/supabase/server";
2+
import { NextResponse } from "next/server";
3+
4+
export async function GET(request: Request) {
5+
const { searchParams, origin } = new URL(request.url);
6+
const code = searchParams.get("code");
7+
const next = searchParams.get("next") ?? "/dashboard";
8+
9+
if (code) {
10+
const supabase = await createClient();
11+
const { error } = await supabase.auth.exchangeCodeForSession(code);
12+
if (!error) {
13+
return NextResponse.redirect(`${origin}${next}`);
14+
}
15+
}
16+
17+
return NextResponse.redirect(
18+
`${origin}/dashboard/login?error=Could+not+authenticate`,
19+
);
20+
}

app/(dashboard)/dashboard/content/content-ideas-table.tsx

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,45 +10,40 @@ import {
1010
} from "@/components/ui/table";
1111
import { Badge } from "@/components/ui/badge";
1212
import { Button } from "@/components/ui/button";
13-
import { Lightbulb } from "lucide-react";
13+
import { Lightbulb, ExternalLink } from "lucide-react";
1414
import { approveIdea, rejectIdea } from "./actions";
1515

1616
interface ContentIdea {
1717
_id: string;
1818
_createdAt: string;
1919
title: string;
20-
status: "new" | "approved" | "rejected" | "published";
21-
source?: string;
22-
category?: string;
20+
status: "new" | "approved" | "rejected";
21+
sourceUrl?: string;
22+
summary?: string;
23+
topics?: string[];
24+
collectedAt?: string;
2325
}
2426

25-
const statusConfig: Record<
26-
string,
27-
{ label: string; className: string }
28-
> = {
27+
const statusConfig: Record<string, { label: string; className: string }> = {
2928
new: {
3029
label: "New",
31-
className: "bg-blue-100 text-blue-800 hover:bg-blue-100 dark:bg-blue-900 dark:text-blue-300",
30+
className:
31+
"bg-blue-100 text-blue-800 hover:bg-blue-100 dark:bg-blue-900 dark:text-blue-300",
3232
},
3333
approved: {
3434
label: "Approved",
35-
className: "bg-green-100 text-green-800 hover:bg-green-100 dark:bg-green-900 dark:text-green-300",
35+
className:
36+
"bg-green-100 text-green-800 hover:bg-green-100 dark:bg-green-900 dark:text-green-300",
3637
},
3738
rejected: {
3839
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",
40+
className:
41+
"bg-red-100 text-red-800 hover:bg-red-100 dark:bg-red-900 dark:text-red-300",
4442
},
4543
};
4644

4745
function StatusBadge({ status }: { status: string }) {
48-
const config = statusConfig[status] ?? {
49-
label: status,
50-
className: "",
51-
};
46+
const config = statusConfig[status] ?? { label: status, className: "" };
5247
return (
5348
<Badge variant="outline" className={config.className}>
5449
{config.label}
@@ -64,15 +59,27 @@ function formatDate(dateString: string) {
6459
});
6560
}
6661

62+
function truncateUrl(url: string, maxLength = 30) {
63+
try {
64+
const parsed = new URL(url);
65+
const display = parsed.hostname + parsed.pathname;
66+
return display.length > maxLength
67+
? display.slice(0, maxLength) + "\u2026"
68+
: display;
69+
} catch {
70+
return url.length > maxLength ? url.slice(0, maxLength) + "\u2026" : url;
71+
}
72+
}
73+
6774
export function ContentIdeasTable({ ideas }: { ideas: ContentIdea[] }) {
6875
if (ideas.length === 0) {
6976
return (
7077
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
7178
<Lightbulb className="h-12 w-12 text-muted-foreground/50" />
7279
<h3 className="mt-4 text-lg font-semibold">No content ideas yet</h3>
7380
<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.
81+
Content ideas will appear here once the ideation cron starts
82+
running.
7683
</p>
7784
</div>
7885
);
@@ -86,8 +93,8 @@ export function ContentIdeasTable({ ideas }: { ideas: ContentIdea[] }) {
8693
<TableHead>Title</TableHead>
8794
<TableHead>Status</TableHead>
8895
<TableHead>Source</TableHead>
89-
<TableHead>Category</TableHead>
90-
<TableHead>Created</TableHead>
96+
<TableHead>Topics</TableHead>
97+
<TableHead>Collected</TableHead>
9198
<TableHead className="text-right">Actions</TableHead>
9299
</TableRow>
93100
</TableHeader>
@@ -100,25 +107,60 @@ export function ContentIdeasTable({ ideas }: { ideas: ContentIdea[] }) {
100107
<TableCell>
101108
<StatusBadge status={idea.status} />
102109
</TableCell>
103-
<TableCell className="text-muted-foreground">
104-
{idea.source || "\u2014"}
110+
<TableCell>
111+
{idea.sourceUrl ? (
112+
<a
113+
href={idea.sourceUrl}
114+
target="_blank"
115+
rel="noopener noreferrer"
116+
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
117+
>
118+
{truncateUrl(idea.sourceUrl)}
119+
<ExternalLink className="h-3 w-3" />
120+
</a>
121+
) : (
122+
<span className="text-muted-foreground">\u2014</span>
123+
)}
105124
</TableCell>
106-
<TableCell className="text-muted-foreground">
107-
{idea.category || "\u2014"}
125+
<TableCell>
126+
{idea.topics && idea.topics.length > 0 ? (
127+
<div className="flex flex-wrap gap-1">
128+
{idea.topics.slice(0, 3).map((topic) => (
129+
<Badge key={topic} variant="secondary" className="text-xs">
130+
{topic}
131+
</Badge>
132+
))}
133+
{idea.topics.length > 3 && (
134+
<Badge variant="secondary" className="text-xs">
135+
+{idea.topics.length - 3}
136+
</Badge>
137+
)}
138+
</div>
139+
) : (
140+
<span className="text-muted-foreground">\u2014</span>
141+
)}
108142
</TableCell>
109143
<TableCell className="text-muted-foreground">
110-
{formatDate(idea._createdAt)}
144+
{formatDate(idea.collectedAt || idea._createdAt)}
111145
</TableCell>
112146
<TableCell className="text-right">
113147
{idea.status === "new" && (
114148
<div className="flex justify-end gap-2">
115149
<form action={approveIdea.bind(null, idea._id)}>
116-
<Button size="sm" variant="outline" className="text-green-600 hover:text-green-700">
150+
<Button
151+
size="sm"
152+
variant="outline"
153+
className="text-green-600 hover:text-green-700"
154+
>
117155
Approve
118156
</Button>
119157
</form>
120158
<form action={rejectIdea.bind(null, idea._id)}>
121-
<Button size="sm" variant="outline" className="text-red-600 hover:text-red-700">
159+
<Button
160+
size="sm"
161+
variant="outline"
162+
className="text-red-600 hover:text-red-700"
163+
>
122164
Reject
123165
</Button>
124166
</form>

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@ interface ContentIdea {
55
_id: string;
66
_createdAt: string;
77
title: string;
8-
status: "new" | "approved" | "rejected" | "published";
9-
source?: string;
10-
category?: string;
8+
status: "new" | "approved" | "rejected";
9+
sourceUrl?: string;
10+
summary?: string;
11+
topics?: string[];
12+
collectedAt?: string;
1113
}
1214

1315
const CONTENT_IDEAS_QUERY = `*[_type == "contentIdea"] | order(_createdAt desc) {
1416
_id,
1517
_createdAt,
1618
title,
1719
status,
18-
source,
19-
category
20+
sourceUrl,
21+
summary,
22+
topics,
23+
collectedAt
2024
}`;
2125

2226
export default async function ContentPage() {
@@ -35,7 +39,7 @@ export default async function ContentPage() {
3539
Content Ideas
3640
</h1>
3741
<p className="text-muted-foreground">
38-
Manage content ideas \u2014 approve, reject, or review incoming topics.
42+
Manage content ideas approve, reject, or review incoming topics.
3943
</p>
4044
</div>
4145

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { redirect } from "next/navigation";
2+
import { createClient } from "@/lib/supabase/server";
3+
import {
4+
Card,
5+
CardContent,
6+
CardDescription,
7+
CardHeader,
8+
CardTitle,
9+
} from "@/components/ui/card";
10+
import { Input } from "@/components/ui/input";
11+
import { Label } from "@/components/ui/label";
12+
import { Button } from "@/components/ui/button";
13+
14+
export default async function LoginPage(props: {
15+
searchParams: Promise<{ error?: string }>;
16+
}) {
17+
const { error } = await props.searchParams;
18+
19+
async function signIn(formData: FormData) {
20+
"use server";
21+
22+
const email = formData.get("email") as string;
23+
const password = formData.get("password") as string;
24+
25+
const supabase = await createClient();
26+
const { error } = await supabase.auth.signInWithPassword({
27+
email,
28+
password,
29+
});
30+
31+
if (error) {
32+
redirect(`/dashboard/login?error=${encodeURIComponent(error.message)}`);
33+
}
34+
35+
redirect("/dashboard");
36+
}
37+
38+
return (
39+
<div className="flex min-h-screen items-center justify-center bg-background">
40+
<Card className="w-full max-w-sm">
41+
<CardHeader>
42+
<CardTitle className="text-2xl">CodingCat.dev</CardTitle>
43+
<CardDescription>
44+
Sign in to the Content Ops Dashboard
45+
</CardDescription>
46+
</CardHeader>
47+
<CardContent>
48+
<form action={signIn} className="flex flex-col gap-4">
49+
{error && (
50+
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
51+
{error}
52+
</div>
53+
)}
54+
<div className="flex flex-col gap-2">
55+
<Label htmlFor="email">Email</Label>
56+
<Input
57+
id="email"
58+
name="email"
59+
type="email"
60+
placeholder="admin@codingcat.dev"
61+
required
62+
/>
63+
</div>
64+
<div className="flex flex-col gap-2">
65+
<Label htmlFor="password">Password</Label>
66+
<Input
67+
id="password"
68+
name="password"
69+
type="password"
70+
required
71+
/>
72+
</div>
73+
<Button type="submit" className="w-full">
74+
Sign In
75+
</Button>
76+
</form>
77+
</CardContent>
78+
</Card>
79+
</div>
80+
);
81+
}

app/(dashboard)/dashboard/videos/actions.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,19 @@ import { revalidatePath } from "next/cache";
44
import { dashboardClient } from "@/lib/sanity/dashboard";
55

66
export async function regenerateScript(id: string) {
7-
await dashboardClient
8-
.patch(id)
9-
.set({ status: "script_gen" })
10-
.commit();
7+
await dashboardClient.patch(id).set({ status: "draft" }).commit();
118
revalidatePath("/dashboard/videos");
129
}
1310

1411
export async function retryRender(id: string) {
15-
await dashboardClient
16-
.patch(id)
17-
.set({ status: "video_gen" })
18-
.commit();
12+
await dashboardClient.patch(id).set({ status: "video_gen" }).commit();
1913
revalidatePath("/dashboard/videos");
2014
}
2115

2216
export async function publishAnyway(id: string) {
2317
await dashboardClient
2418
.patch(id)
25-
.set({ status: "published", flagged: false })
19+
.set({ status: "published" })
2620
.unset(["flaggedReason"])
2721
.commit();
2822
revalidatePath("/dashboard/videos");

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,29 @@ interface AutomatedVideo {
55
_id: string;
66
_createdAt: string;
77
title: string;
8-
status: "script_gen" | "audio_gen" | "video_gen" | "published" | "failed";
9-
flagged: boolean;
8+
status:
9+
| "draft"
10+
| "script_ready"
11+
| "audio_gen"
12+
| "video_gen"
13+
| "flagged"
14+
| "uploading"
15+
| "published";
1016
flaggedReason?: string;
11-
duration?: number;
17+
scriptQualityScore?: number;
18+
scheduledPublishAt?: string;
19+
youtubeId?: string;
1220
}
1321

1422
const VIDEOS_QUERY = `*[_type == "automatedVideo"] | order(_createdAt desc) {
1523
_id,
1624
_createdAt,
1725
title,
1826
status,
19-
flagged,
2027
flaggedReason,
21-
duration
28+
scriptQualityScore,
29+
scheduledPublishAt,
30+
youtubeId
2231
}`;
2332

2433
export default async function VideosPage() {
@@ -37,7 +46,7 @@ export default async function VideosPage() {
3746
Automated Videos
3847
</h1>
3948
<p className="text-muted-foreground">
40-
Monitor the video pipeline \u2014 from script generation to publishing.
49+
Monitor the video pipeline from script generation to publishing.
4150
</p>
4251
</div>
4352

0 commit comments

Comments
 (0)