Skip to content

Commit 284b003

Browse files
Miriadupgrade
andcommitted
feat: merge all Phase 1 branches into dev — complete content engine
Merges 5 Phase 1 branches + Phase 0 foundation into dev: - phase0/complete: Next.js 16, Sanity v5, React 19.2, Supabase, CI - phase1a/content-pipeline: 4 Sanity schemas (contentIdea, automatedVideo, sponsorLead, sponsorPool) - phase1b/video-pipeline: ElevenLabs TTS, GCS upload, Pexels B-roll, Remotion Lambda - phase1c/distribution-pipeline: YouTube uploads, Resend email, Sanity webhook - phase1d/sponsor-pipeline: Gemini intent extraction, outbound cron, Stripe stub - phase1e/dashboard: Supabase auth, Content Ops dashboard, video pipeline UI Post-merge fixes: - Created shared libs: lib/gemini.ts, lib/sanity-write-client.ts, lib/youtube-upload.ts, lib/resend-notify.ts - Fixed package.json with correct Phase 0 versions + Phase 1 additions - Added serverExternalPackages for Remotion/rspack native binaries - Fixed distribute route notifySubscribers call signature - Added @supabase/ssr and @supabase/supabase-js dependencies Build: TypeScript passes (tsc --noEmit), Next.js 16.1.6 webpack compilation succeeds. Note: Sanity typegen prebuild needs Sanity Studio config fix (separate issue). Note: middleware.ts deprecated warning — should migrate to proxy pattern. Co-authored-by: upgrade <upgrade@miriad.systems>
1 parent 9918316 commit 284b003

File tree

9 files changed

+18343
-27315
lines changed

9 files changed

+18343
-27315
lines changed

app/api/webhooks/sanity-distribute/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
108108
// Step 4: Email (non-fatal)
109109
const ytUrl = youtubeVideoId ? `https://www.youtube.com/watch?v=${youtubeVideoId}` : payload.videoUrl || "";
110110
try {
111-
await notifySubscribers({ videoTitle: metadata.title, videoUrl: ytUrl, shortDescription: metadata.description.slice(0, 280), thumbnailUrl: `https://img.youtube.com/vi/${youtubeVideoId}/maxresdefault.jpg` });
111+
await notifySubscribers({ subject: `New Video: ${metadata.title}`, videoTitle: metadata.title, videoUrl: ytUrl, description: metadata.description.slice(0, 280) });
112112
} catch (e) { console.warn("[sanity-distribute] Email error:", e); }
113113

114114
// Step 5: Mark published

lib/gemini.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { GoogleGenerativeAI } from "@google/generative-ai";
2+
3+
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || "");
4+
5+
/**
6+
* Generate text content using Gemini Flash.
7+
* Used across the content engine for script generation, metadata, and intent extraction.
8+
*/
9+
export async function generateWithGemini(prompt: string): Promise<string> {
10+
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
11+
const result = await model.generateContent(prompt);
12+
const response = result.response;
13+
return response.text();
14+
}

lib/resend-notify.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Notify email subscribers about new content via Resend.
3+
* Stubbed — requires RESEND_API_KEY to be configured.
4+
*/
5+
export async function notifySubscribers(opts: {
6+
subject: string;
7+
videoTitle: string;
8+
videoUrl: string;
9+
description: string;
10+
}): Promise<{ sent: boolean; error?: string }> {
11+
const apiKey = process.env.RESEND_API_KEY;
12+
if (!apiKey) {
13+
console.warn("[resend-notify] RESEND_API_KEY not set — skipping email notification");
14+
return { sent: false, error: "RESEND_API_KEY not configured" };
15+
}
16+
17+
try {
18+
const { Resend } = await import("resend");
19+
const resend = new Resend(apiKey);
20+
21+
await resend.emails.send({
22+
from: "CodingCat.dev <noreply@codingcat.dev>",
23+
to: ["subscribers@codingcat.dev"], // TODO: fetch subscriber list
24+
subject: opts.subject,
25+
html: `
26+
<h1>${opts.videoTitle}</h1>
27+
<p>${opts.description}</p>
28+
<a href="${opts.videoUrl}">Watch Now</a>
29+
`,
30+
});
31+
32+
return { sent: true };
33+
} catch (error) {
34+
console.error("[resend-notify] Failed to send:", error);
35+
return { sent: false, error: String(error) };
36+
}
37+
}

lib/sanity-write-client.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createClient } from "next-sanity";
2+
3+
import { apiVersion, dataset, projectId } from "@/sanity/lib/api";
4+
5+
// Server-side Sanity client with write access (uses SANITY_API_TOKEN)
6+
export const sanityWriteClient = createClient({
7+
projectId,
8+
dataset,
9+
apiVersion,
10+
useCdn: false,
11+
token: process.env.SANITY_API_TOKEN,
12+
});
13+
14+
// Alias for compatibility
15+
export const writeClient = sanityWriteClient;

lib/youtube-upload.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { google } from "googleapis";
2+
3+
const oauth2Client = new google.auth.OAuth2(
4+
process.env.YOUTUBE_CLIENT_ID,
5+
process.env.YOUTUBE_CLIENT_SECRET,
6+
);
7+
oauth2Client.setCredentials({
8+
refresh_token: process.env.YOUTUBE_REFRESH_TOKEN,
9+
});
10+
11+
const youtube = google.youtube({ version: "v3", auth: oauth2Client });
12+
13+
/**
14+
* Upload a video to YouTube (main channel video, 16:9).
15+
*/
16+
export async function uploadVideo(opts: {
17+
title: string;
18+
description: string;
19+
tags: string[];
20+
videoUrl: string;
21+
}): Promise<{ videoId: string; url: string }> {
22+
// Fetch the video from GCS URL
23+
const response = await fetch(opts.videoUrl);
24+
if (!response.ok) throw new Error(`Failed to fetch video: ${response.statusText}`);
25+
26+
const res = await youtube.videos.insert({
27+
part: ["snippet", "status"],
28+
requestBody: {
29+
snippet: {
30+
title: opts.title,
31+
description: opts.description,
32+
tags: opts.tags,
33+
categoryId: "28", // Science & Technology
34+
},
35+
status: {
36+
privacyStatus: "public",
37+
selfDeclaredMadeForKids: false,
38+
},
39+
},
40+
media: {
41+
body: response.body as unknown as NodeJS.ReadableStream,
42+
},
43+
});
44+
45+
const videoId = res.data.id || "";
46+
return { videoId, url: `https://youtube.com/watch?v=${videoId}` };
47+
}
48+
49+
/**
50+
* Upload a Short to YouTube (9:16 vertical).
51+
*/
52+
export async function uploadShort(opts: {
53+
title: string;
54+
description: string;
55+
tags: string[];
56+
videoUrl: string;
57+
}): Promise<{ videoId: string; url: string }> {
58+
// Shorts are just regular uploads with #Shorts in title/description
59+
return uploadVideo({
60+
...opts,
61+
title: `${opts.title} #Shorts`,
62+
description: `${opts.description}\n\n#Shorts`,
63+
});
64+
}

next.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ const nextConfig: NextConfig = {
1111
},
1212
],
1313
},
14+
serverExternalPackages: [
15+
"@remotion/lambda",
16+
"@remotion/bundler",
17+
"@remotion/cli",
18+
"@rspack/core",
19+
"@rspack/binding",
20+
],
1421
};
1522

1623
export default nextConfig;

0 commit comments

Comments
 (0)