Fix Amplify environment variable configuration
- Add amplify.yml for proper build configuration - Update next.config.ts to expose YOUTUBE_API_KEY env var - This should fix the issue with secrets not being available in production 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ddbd6c27d6
commit
40c2f0c263
13 changed files with 47206 additions and 47183 deletions
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(find:*)",
|
||||
"Bash(ls:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(find:*)",
|
||||
"Bash(ls:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
100
.gitignore
vendored
100
.gitignore
vendored
|
@ -1,50 +1,50 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.genkit/*
|
||||
.env*
|
||||
|
||||
# firebase
|
||||
firebase-debug.log
|
||||
firestore-debug.log
|
||||
|
||||
# amplify
|
||||
.amplify
|
||||
amplify_outputs*
|
||||
amplifyconfiguration*
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.genkit/*
|
||||
.env*
|
||||
|
||||
# firebase
|
||||
firebase-debug.log
|
||||
firestore-debug.log
|
||||
|
||||
# amplify
|
||||
.amplify
|
||||
amplify_outputs*
|
||||
amplifyconfiguration*
|
||||
|
|
20
amplify.yml
Normal file
20
amplify.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
version: 1
|
||||
frontend:
|
||||
phases:
|
||||
preBuild:
|
||||
commands:
|
||||
- npm ci
|
||||
build:
|
||||
commands:
|
||||
- env | grep YOUTUBE_API_KEY || echo "YOUTUBE_API_KEY not found in environment"
|
||||
- npm run build
|
||||
artifacts:
|
||||
baseDirectory: .next
|
||||
files:
|
||||
- '**/*'
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/**/*
|
||||
- .next/cache/**/*
|
||||
buildPath: /
|
||||
appRoot: /
|
|
@ -1,11 +1,11 @@
|
|||
import { defineAuth } from '@aws-amplify/backend';
|
||||
|
||||
/**
|
||||
* Define and configure your auth resource
|
||||
* @see https://docs.amplify.aws/gen2/build-a-backend/auth
|
||||
*/
|
||||
export const auth = defineAuth({
|
||||
loginWith: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
import { defineAuth } from '@aws-amplify/backend';
|
||||
|
||||
/**
|
||||
* Define and configure your auth resource
|
||||
* @see https://docs.amplify.aws/gen2/build-a-backend/auth
|
||||
*/
|
||||
export const auth = defineAuth({
|
||||
loginWith: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { defineBackend } from '@aws-amplify/backend';
|
||||
import { auth } from './auth/resource';
|
||||
import { data } from './data/resource';
|
||||
|
||||
/**
|
||||
* @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more
|
||||
*/
|
||||
defineBackend({
|
||||
auth,
|
||||
data,
|
||||
});
|
||||
import { defineBackend } from '@aws-amplify/backend';
|
||||
import { auth } from './auth/resource';
|
||||
import { data } from './data/resource';
|
||||
|
||||
/**
|
||||
* @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more
|
||||
*/
|
||||
defineBackend({
|
||||
auth,
|
||||
data,
|
||||
});
|
||||
|
|
|
@ -1,53 +1,53 @@
|
|||
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
|
||||
|
||||
/*== STEP 1 ===============================================================
|
||||
The section below creates a Todo database table with a "content" field. Try
|
||||
adding a new "isDone" field as a boolean. The authorization rule below
|
||||
specifies that any unauthenticated user can "create", "read", "update",
|
||||
and "delete" any "Todo" records.
|
||||
=========================================================================*/
|
||||
const schema = a.schema({
|
||||
Todo: a
|
||||
.model({
|
||||
content: a.string(),
|
||||
})
|
||||
.authorization((allow) => [allow.guest()]),
|
||||
});
|
||||
|
||||
export type Schema = ClientSchema<typeof schema>;
|
||||
|
||||
export const data = defineData({
|
||||
schema,
|
||||
authorizationModes: {
|
||||
defaultAuthorizationMode: 'identityPool',
|
||||
},
|
||||
});
|
||||
|
||||
/*== STEP 2 ===============================================================
|
||||
Go to your frontend source code. From your client-side code, generate a
|
||||
Data client to make CRUDL requests to your table. (THIS SNIPPET WILL ONLY
|
||||
WORK IN THE FRONTEND CODE FILE.)
|
||||
|
||||
Using JavaScript or Next.js React Server Components, Middleware, Server
|
||||
Actions or Pages Router? Review how to generate Data clients for those use
|
||||
cases: https://docs.amplify.aws/gen2/build-a-backend/data/connect-to-API/
|
||||
=========================================================================*/
|
||||
|
||||
/*
|
||||
"use client"
|
||||
import { generateClient } from "aws-amplify/data";
|
||||
import type { Schema } from "@/amplify/data/resource";
|
||||
|
||||
const client = generateClient<Schema>() // use this Data client for CRUDL requests
|
||||
*/
|
||||
|
||||
/*== STEP 3 ===============================================================
|
||||
Fetch records from the database and use them in your frontend component.
|
||||
(THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.)
|
||||
=========================================================================*/
|
||||
|
||||
/* For example, in a React component, you can use this snippet in your
|
||||
function's RETURN statement */
|
||||
// const { data: todos } = await client.models.Todo.list()
|
||||
|
||||
// return <ul>{todos.map(todo => <li key={todo.id}>{todo.content}</li>)}</ul>
|
||||
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
|
||||
|
||||
/*== STEP 1 ===============================================================
|
||||
The section below creates a Todo database table with a "content" field. Try
|
||||
adding a new "isDone" field as a boolean. The authorization rule below
|
||||
specifies that any unauthenticated user can "create", "read", "update",
|
||||
and "delete" any "Todo" records.
|
||||
=========================================================================*/
|
||||
const schema = a.schema({
|
||||
Todo: a
|
||||
.model({
|
||||
content: a.string(),
|
||||
})
|
||||
.authorization((allow) => [allow.guest()]),
|
||||
});
|
||||
|
||||
export type Schema = ClientSchema<typeof schema>;
|
||||
|
||||
export const data = defineData({
|
||||
schema,
|
||||
authorizationModes: {
|
||||
defaultAuthorizationMode: 'identityPool',
|
||||
},
|
||||
});
|
||||
|
||||
/*== STEP 2 ===============================================================
|
||||
Go to your frontend source code. From your client-side code, generate a
|
||||
Data client to make CRUDL requests to your table. (THIS SNIPPET WILL ONLY
|
||||
WORK IN THE FRONTEND CODE FILE.)
|
||||
|
||||
Using JavaScript or Next.js React Server Components, Middleware, Server
|
||||
Actions or Pages Router? Review how to generate Data clients for those use
|
||||
cases: https://docs.amplify.aws/gen2/build-a-backend/data/connect-to-API/
|
||||
=========================================================================*/
|
||||
|
||||
/*
|
||||
"use client"
|
||||
import { generateClient } from "aws-amplify/data";
|
||||
import type { Schema } from "@/amplify/data/resource";
|
||||
|
||||
const client = generateClient<Schema>() // use this Data client for CRUDL requests
|
||||
*/
|
||||
|
||||
/*== STEP 3 ===============================================================
|
||||
Fetch records from the database and use them in your frontend component.
|
||||
(THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.)
|
||||
=========================================================================*/
|
||||
|
||||
/* For example, in a React component, you can use this snippet in your
|
||||
function's RETURN statement */
|
||||
// const { data: todos } = await client.models.Todo.list()
|
||||
|
||||
// return <ul>{todos.map(todo => <li key={todo.id}>{todo.content}</li>)}</ul>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"type": "module"
|
||||
{
|
||||
"type": "module"
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"$amplify/*": [
|
||||
"../.amplify/generated/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"$amplify/*": [
|
||||
"../.amplify/generated/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,6 +24,9 @@ const nextConfig: NextConfig = {
|
|||
},
|
||||
],
|
||||
},
|
||||
env: {
|
||||
YOUTUBE_API_KEY: process.env.YOUTUBE_API_KEY || '',
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
93556
package-lock.json
generated
93556
package-lock.json
generated
File diff suppressed because it is too large
Load diff
150
package.json
150
package.json
|
@ -1,75 +1,75 @@
|
|||
{
|
||||
"name": "nextn",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 9002",
|
||||
"genkit:dev": "genkit start -- tsx src/ai/dev.ts",
|
||||
"genkit:watch": "genkit start -- tsx --watch src/ai/dev.ts",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@genkit-ai/googleai": "^1.13.0",
|
||||
"@genkit-ai/next": "^1.13.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"aws-amplify": "^6.15.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"firebase": "^11.9.1",
|
||||
"genkit": "^1.13.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"next": "15.3.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-amplify/backend": "^1.16.1",
|
||||
"@aws-amplify/backend-cli": "^1.8.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"aws-cdk-lib": "^2.189.1",
|
||||
"constructs": "^10.4.2",
|
||||
"esbuild": "^0.25.5",
|
||||
"genkit-cli": "^1.13.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "nextn",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 9002",
|
||||
"genkit:dev": "genkit start -- tsx src/ai/dev.ts",
|
||||
"genkit:watch": "genkit start -- tsx --watch src/ai/dev.ts",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@genkit-ai/googleai": "^1.13.0",
|
||||
"@genkit-ai/next": "^1.13.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"aws-amplify": "^6.15.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"firebase": "^11.9.1",
|
||||
"genkit": "^1.13.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"next": "15.3.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-amplify/backend": "^1.16.1",
|
||||
"@aws-amplify/backend-cli": "^1.8.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"aws-cdk-lib": "^2.189.1",
|
||||
"constructs": "^10.4.2",
|
||||
"esbuild": "^0.25.5",
|
||||
"genkit-cli": "^1.13.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,151 +1,151 @@
|
|||
|
||||
import type { Metadata } from 'next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Link from 'next/link';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Video Gallery - Community Coverage of Chelsea Smallwood",
|
||||
description: "Watch YouTube commentary and analysis on Chelsea Smallwood, The Other Woman and the Wife, and the ongoing controversy.",
|
||||
};
|
||||
|
||||
interface Video {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
// Replaced broken video IDs with working ones.
|
||||
const videoIds = [
|
||||
'MdTWPuNQ1B8', // Authentic Observer
|
||||
'e6rHHtq5K1k',
|
||||
'-6Zftd8C7NE', // Coop
|
||||
'AbVsR7XzNBc', // clout
|
||||
'lJ8zHiwfrqs',
|
||||
'q8zevCJ6TKw'
|
||||
// 'DK14VZ4Fyl4', // Lauren
|
||||
];
|
||||
|
||||
// Updated fallback data to be a reliable source of working videos.
|
||||
const fallbackData: Video[] = [
|
||||
{ id: 'DK14VZ4Fyl4', title: 'Life Coach CHELSEA SMALLWOOD Is SUING Her HUSBANDS Ex Wife... It Gets WORSE' },
|
||||
{ id: '-6Zftd8C7NE', title: "The Husband Stealing, Cheating, \"TikTok Life Coach\"" },
|
||||
];
|
||||
|
||||
async function getYouTubeVideos(ids: string[]): Promise<Video[]> {
|
||||
const apiKey = process.env.YOUTUBE_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
console.warn("YOUTUBE_API_KEY environment variable not set. Using hardcoded video titles as fallback.");
|
||||
return fallbackData;
|
||||
}
|
||||
|
||||
const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${ids.join(',')}&key=${apiKey}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { next: { revalidate: 3600 } }); // Revalidate every hour
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error("YouTube API Error:", errorData.error.message);
|
||||
console.log("Falling back to hardcoded video data.");
|
||||
return fallbackData;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.items || data.items.length === 0) {
|
||||
console.warn("YouTube API returned no items for the given video IDs. Falling back to hardcoded data.");
|
||||
return fallbackData;
|
||||
}
|
||||
|
||||
const fetchedVideos = data.items.map((item: any) => ({
|
||||
id: item.id,
|
||||
title: item.snippet.title,
|
||||
}));
|
||||
|
||||
if (fetchedVideos.length < ids.length) {
|
||||
console.warn(`YouTube API only returned ${fetchedVideos.length} videos out of ${ids.length} requested. Some videos may be private or deleted.`);
|
||||
}
|
||||
|
||||
return fetchedVideos;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch from YouTube API:", error);
|
||||
console.log("Falling back to hardcoded video data.");
|
||||
return fallbackData;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function GalleryPage() {
|
||||
const videos = await getYouTubeVideos(videoIds);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-background text-foreground">
|
||||
<main className="flex-grow">
|
||||
<div className="container mx-auto max-w-5xl py-12 px-4 sm:px-6 lg:px-8">
|
||||
<header className="text-center mb-12">
|
||||
<h1 className="text-4xl sm:text-5xl font-extrabold tracking-tight text-primary font-headline">
|
||||
YouTube Community Coverage
|
||||
</h1>
|
||||
<p className="mt-4 text-xl text-muted-foreground">
|
||||
Commentary and analysis from creators across the platform.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{videos.length > 0 ? videos.map((video) => (
|
||||
<div key={video.id} className="bg-card rounded-lg shadow-sm border overflow-hidden flex flex-col">
|
||||
<div className="relative w-full pt-[56.25%]"> {/* 16:9 Aspect Ratio */}
|
||||
<iframe
|
||||
className="absolute top-0 left-0 w-full h-full"
|
||||
src={`https://www.youtube.com/embed/${video.id}`}
|
||||
title={video.title}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
<div className="p-4 flex-grow">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">
|
||||
{video.title}
|
||||
</h2>
|
||||
<a href={`https://www.youtube.com/watch?v=${video.id}`} target="_blank" rel="noopener noreferrer" className="text-sm text-primary hover:underline flex items-center gap-1 mt-2">
|
||||
Watch on YouTube <ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<p className="text-center col-span-full">Could not load videos. Please try again later.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-12">
|
||||
<Button asChild>
|
||||
<Link href="/">Back to Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="text-center py-6 text-sm text-muted-foreground">
|
||||
<div className="container mx-auto">
|
||||
<p>
|
||||
This website, cheatingchelsea.com, is dedicated to raising
|
||||
awareness and supporting the victims.
|
||||
</p>
|
||||
<p>
|
||||
For questions, comments, or concerns, please email:{' '}
|
||||
<a
|
||||
href="mailto:notacheater@cheatingchelsea.com"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
notacheater@cheatingchelsea.com
|
||||
</a>
|
||||
</p>
|
||||
<p className="mt-4 italic">
|
||||
Disclaimer: This website is independently operated by a snarky
|
||||
Michigander and is not affiliated with or endorsed by Kristen
|
||||
Jacobs.
|
||||
</p>
|
||||
<p className="mt-2">© 2025 Cheating Chelsea Exposed. All Rights Reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Link from 'next/link';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Video Gallery - Community Coverage of Chelsea Smallwood",
|
||||
description: "Watch YouTube commentary and analysis on Chelsea Smallwood, The Other Woman and the Wife, and the ongoing controversy.",
|
||||
};
|
||||
|
||||
interface Video {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
// Replaced broken video IDs with working ones.
|
||||
const videoIds = [
|
||||
'MdTWPuNQ1B8', // Authentic Observer
|
||||
'e6rHHtq5K1k',
|
||||
'-6Zftd8C7NE', // Coop
|
||||
'AbVsR7XzNBc', // clout
|
||||
'lJ8zHiwfrqs',
|
||||
'q8zevCJ6TKw'
|
||||
// 'DK14VZ4Fyl4', // Lauren
|
||||
];
|
||||
|
||||
// Updated fallback data to be a reliable source of working videos.
|
||||
const fallbackData: Video[] = [
|
||||
{ id: 'DK14VZ4Fyl4', title: 'Life Coach CHELSEA SMALLWOOD Is SUING Her HUSBANDS Ex Wife... It Gets WORSE' },
|
||||
{ id: '-6Zftd8C7NE', title: "The Husband Stealing, Cheating, \"TikTok Life Coach\"" },
|
||||
];
|
||||
|
||||
async function getYouTubeVideos(ids: string[]): Promise<Video[]> {
|
||||
const apiKey = process.env.YOUTUBE_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
console.warn("YOUTUBE_API_KEY environment variable not set. Using hardcoded video titles as fallback.");
|
||||
return fallbackData;
|
||||
}
|
||||
|
||||
const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${ids.join(',')}&key=${apiKey}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { next: { revalidate: 3600 } }); // Revalidate every hour
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error("YouTube API Error:", errorData.error.message);
|
||||
console.log("Falling back to hardcoded video data.");
|
||||
return fallbackData;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.items || data.items.length === 0) {
|
||||
console.warn("YouTube API returned no items for the given video IDs. Falling back to hardcoded data.");
|
||||
return fallbackData;
|
||||
}
|
||||
|
||||
const fetchedVideos = data.items.map((item: any) => ({
|
||||
id: item.id,
|
||||
title: item.snippet.title,
|
||||
}));
|
||||
|
||||
if (fetchedVideos.length < ids.length) {
|
||||
console.warn(`YouTube API only returned ${fetchedVideos.length} videos out of ${ids.length} requested. Some videos may be private or deleted.`);
|
||||
}
|
||||
|
||||
return fetchedVideos;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch from YouTube API:", error);
|
||||
console.log("Falling back to hardcoded video data.");
|
||||
return fallbackData;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function GalleryPage() {
|
||||
const videos = await getYouTubeVideos(videoIds);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-background text-foreground">
|
||||
<main className="flex-grow">
|
||||
<div className="container mx-auto max-w-5xl py-12 px-4 sm:px-6 lg:px-8">
|
||||
<header className="text-center mb-12">
|
||||
<h1 className="text-4xl sm:text-5xl font-extrabold tracking-tight text-primary font-headline">
|
||||
YouTube Community Coverage
|
||||
</h1>
|
||||
<p className="mt-4 text-xl text-muted-foreground">
|
||||
Commentary and analysis from creators across the platform.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{videos.length > 0 ? videos.map((video) => (
|
||||
<div key={video.id} className="bg-card rounded-lg shadow-sm border overflow-hidden flex flex-col">
|
||||
<div className="relative w-full pt-[56.25%]"> {/* 16:9 Aspect Ratio */}
|
||||
<iframe
|
||||
className="absolute top-0 left-0 w-full h-full"
|
||||
src={`https://www.youtube.com/embed/${video.id}`}
|
||||
title={video.title}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
<div className="p-4 flex-grow">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">
|
||||
{video.title}
|
||||
</h2>
|
||||
<a href={`https://www.youtube.com/watch?v=${video.id}`} target="_blank" rel="noopener noreferrer" className="text-sm text-primary hover:underline flex items-center gap-1 mt-2">
|
||||
Watch on YouTube <ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<p className="text-center col-span-full">Could not load videos. Please try again later.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-12">
|
||||
<Button asChild>
|
||||
<Link href="/">Back to Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="text-center py-6 text-sm text-muted-foreground">
|
||||
<div className="container mx-auto">
|
||||
<p>
|
||||
This website, cheatingchelsea.com, is dedicated to raising
|
||||
awareness and supporting the victims.
|
||||
</p>
|
||||
<p>
|
||||
For questions, comments, or concerns, please email:{' '}
|
||||
<a
|
||||
href="mailto:notacheater@cheatingchelsea.com"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
notacheater@cheatingchelsea.com
|
||||
</a>
|
||||
</p>
|
||||
<p className="mt-4 italic">
|
||||
Disclaimer: This website is independently operated by a snarky
|
||||
Michigander and is not affiliated with or endorsed by Kristen
|
||||
Jacobs.
|
||||
</p>
|
||||
<p className="mt-2">© 2025 Cheating Chelsea Exposed. All Rights Reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-background text-foreground">
|
||||
<main className="text-center p-8 max-w-2xl mx-auto">
|
||||
<div className="bg-black p-4 inline-block rounded-lg border-2 border-gray-700 shadow-2xl">
|
||||
<Image
|
||||
src="/404-cat.jpg"
|
||||
alt="A calico cat's tail is sticking out from under a pile of papers, resembling the '404 Not Found' cat meme."
|
||||
width={750}
|
||||
height={600}
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-8 mb-8 text-xl">
|
||||
Oops! It looks like the page you're looking for has gone into hiding.
|
||||
</p>
|
||||
<Button asChild size="lg">
|
||||
<Link href="/">Go Back to Home</Link>
|
||||
</Button>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-background text-foreground">
|
||||
<main className="text-center p-8 max-w-2xl mx-auto">
|
||||
<div className="bg-black p-4 inline-block rounded-lg border-2 border-gray-700 shadow-2xl">
|
||||
<Image
|
||||
src="/404-cat.jpg"
|
||||
alt="A calico cat's tail is sticking out from under a pile of papers, resembling the '404 Not Found' cat meme."
|
||||
width={750}
|
||||
height={600}
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-8 mb-8 text-xl">
|
||||
Oops! It looks like the page you're looking for has gone into hiding.
|
||||
</p>
|
||||
<Button asChild size="lg">
|
||||
<Link href="/">Go Back to Home</Link>
|
||||
</Button>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue