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:
Derek Slenk 2025-06-27 14:56:31 -04:00
parent ddbd6c27d6
commit 40c2f0c263
13 changed files with 47206 additions and 47183 deletions

View file

@ -1,9 +1,9 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(ls:*)"
],
"deny": []
}
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(ls:*)"
],
"deny": []
}
}

100
.gitignore vendored
View file

@ -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
View 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: /

View file

@ -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,
},
});

View file

@ -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,
});

View file

@ -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>

View file

@ -1,3 +1,3 @@
{
"type": "module"
{
"type": "module"
}

View file

@ -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/*"
]
}
}
}

View file

@ -24,6 +24,9 @@ const nextConfig: NextConfig = {
},
],
},
env: {
YOUTUBE_API_KEY: process.env.YOUTUBE_API_KEY || '',
},
};
export default nextConfig;

93556
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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&#64;cheatingchelsea.com"
className="text-primary hover:underline"
>
notacheater&#64;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">&copy; 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&#64;cheatingchelsea.com"
className="text-primary hover:underline"
>
notacheater&#64;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">&copy; 2025 Cheating Chelsea Exposed. All Rights Reserved.</p>
</div>
</footer>
</div>
);
}

View file

@ -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>
);
}