Add error and not-found pages across all route segments

- app/not-found.tsx: root 404 with cinematic atmosphere
- app/global-error.tsx: self-contained global error boundary
- app/(pages)/error.tsx: authenticated error boundary with retry
- app/(pages)/not-found.tsx: authenticated 404 with navigation
- Upgrade existing title and person not-found pages for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 18:23:56 -05:00
co-authored by Claude Opus 4.6
parent fc982bee1b
commit 4773243cda
6 changed files with 415 additions and 24 deletions
+60
View File
@@ -0,0 +1,60 @@
"use client";
import { IconAlertTriangle, IconRefresh } from "@tabler/icons-react";
import Link from "next/link";
import { useEffect } from "react";
export default function PagesError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("[pages error boundary]", error);
}, [error]);
return (
<div className="flex flex-col items-center gap-6 py-24 text-center">
{/* Icon ring */}
<div className="flex size-14 items-center justify-center rounded-full border border-destructive/20 bg-destructive/5 text-destructive">
<IconAlertTriangle className="size-6" strokeWidth={1.5} />
</div>
<div className="space-y-2">
<h1 className="font-display text-2xl tracking-tight sm:text-3xl">
Something went wrong
</h1>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground">
An unexpected error occurred while loading this page. You can try
again or head back to the dashboard.
</p>
</div>
{error.digest && (
<p className="font-mono text-[11px] text-muted-foreground/40">
Error ID: {error.digest}
</p>
)}
<div className="flex items-center gap-3">
<button
type="button"
onClick={reset}
className="group relative inline-flex h-10 items-center justify-center gap-2 overflow-hidden rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20"
>
<IconRefresh className="size-4" />
<span className="relative z-10">Try again</span>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
</button>
<Link
href="/dashboard"
className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 text-sm font-medium transition-colors hover:border-primary/40 hover:bg-primary/5"
>
Dashboard
</Link>
</div>
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
import Link from "next/link";
export default function PagesNotFound() {
return (
<div className="flex flex-col items-center gap-6 py-24 text-center">
{/* Ghosted 404 */}
<h1 className="animate-stagger-item font-display text-[6rem] leading-[0.85] tracking-tight text-foreground/[0.06] sm:text-[8rem]">
404
</h1>
<div
className="animate-stagger-item -mt-4 space-y-2"
style={{ "--stagger-index": 1 } as React.CSSProperties}
>
<h2 className="font-display text-2xl tracking-tight sm:text-3xl">
Page not found
</h2>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground">
This page doesn&apos;t exist or may have been moved. Try searching for
what you&apos;re looking for instead.
</p>
</div>
<div
className="animate-stagger-item flex items-center gap-3"
style={{ "--stagger-index": 2 } as React.CSSProperties}
>
<Link
href="/dashboard"
className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20"
>
<span className="relative z-10">Dashboard</span>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
</Link>
<Link
href="/explore"
className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 text-sm font-medium transition-colors hover:border-primary/40 hover:bg-primary/5"
>
Explore
</Link>
</div>
</div>
);
}
+32 -7
View File
@@ -2,20 +2,45 @@ import Link from "next/link";
export default function PersonNotFound() {
return (
<div className="flex flex-col items-center gap-4 py-24 text-center">
<h1 className="font-display text-4xl tracking-tight text-balance">
Person not found
<div className="flex flex-col items-center gap-6 py-24 text-center">
<h1
className="animate-stagger-item font-display text-[6rem] leading-[0.85] tracking-tight text-foreground/[0.06] sm:text-[8rem]"
style={{ "--stagger-index": 0 } as React.CSSProperties}
>
404
</h1>
<p className="text-muted-foreground">
<div
className="animate-stagger-item -mt-4 space-y-2"
style={{ "--stagger-index": 1 } as React.CSSProperties}
>
<h2 className="font-display text-2xl tracking-tight sm:text-3xl">
Person not found
</h2>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground">
The person you&apos;re looking for doesn&apos;t exist or may have been
removed.
removed from the database.
</p>
</div>
<div
className="animate-stagger-item flex items-center gap-3"
style={{ "--stagger-index": 2 } as React.CSSProperties}
>
<Link
href="/explore"
className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-md hover:shadow-primary/20"
className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20"
>
Explore titles
<span className="relative z-10">Explore titles</span>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
</Link>
<Link
href="/dashboard"
className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 text-sm font-medium transition-colors hover:border-primary/40 hover:bg-primary/5"
>
Dashboard
</Link>
</div>
</div>
);
}
+33 -6
View File
@@ -2,18 +2,45 @@ import Link from "next/link";
export default function TitleNotFound() {
return (
<div className="flex flex-col items-center gap-4 py-24 text-center">
<h1 className="font-display text-4xl tracking-tight">Title not found</h1>
<p className="text-muted-foreground">
<div className="flex flex-col items-center gap-6 py-24 text-center">
<h1
className="animate-stagger-item font-display text-[6rem] leading-[0.85] tracking-tight text-foreground/[0.06] sm:text-[8rem]"
style={{ "--stagger-index": 0 } as React.CSSProperties}
>
404
</h1>
<div
className="animate-stagger-item -mt-4 space-y-2"
style={{ "--stagger-index": 1 } as React.CSSProperties}
>
<h2 className="font-display text-2xl tracking-tight sm:text-3xl">
Title not found
</h2>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground">
The title you&apos;re looking for doesn&apos;t exist or may have been
removed.
removed from the database.
</p>
</div>
<div
className="animate-stagger-item flex items-center gap-3"
style={{ "--stagger-index": 2 } as React.CSSProperties}
>
<Link
href="/explore"
className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-all hover:shadow-md hover:shadow-primary/20"
className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20"
>
Explore titles
<span className="relative z-10">Explore titles</span>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
</Link>
<Link
href="/dashboard"
className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 text-sm font-medium transition-colors hover:border-primary/40 hover:bg-primary/5"
>
Dashboard
</Link>
</div>
</div>
);
}
+161
View File
@@ -0,0 +1,161 @@
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html lang="en" style={{ colorScheme: "dark" }}>
<head>
<style>{`
body {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'DM Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
background-color: oklch(0.13 0.006 55);
color: oklch(0.93 0.015 80);
padding: 1.5rem;
text-align: center;
overflow: hidden;
}
.ge-glow {
position: fixed;
left: 50%;
top: 40%;
width: 400px;
height: 400px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: oklch(0.8 0.14 65 / 0.04);
filter: blur(120px);
pointer-events: none;
}
.ge-icon-ring {
width: 48px;
height: 48px;
margin: 0 auto 1.5rem;
border-radius: 50%;
border: 1px solid oklch(0.8 0.14 65 / 0.2);
display: flex;
align-items: center;
justify-content: center;
color: oklch(0.8 0.14 65);
}
.ge-title {
font-family: 'DM Serif Display', 'Georgia', ui-serif, serif;
font-size: 1.75rem;
font-weight: 400;
letter-spacing: -0.01em;
margin: 0 0 0.75rem;
}
.ge-desc {
font-size: 0.875rem;
line-height: 1.6;
color: oklch(0.63 0.02 80);
max-width: 22rem;
margin: 0 auto 2rem;
}
.ge-digest {
font-size: 0.7rem;
font-family: monospace;
color: oklch(0.63 0.02 80 / 0.5);
margin-bottom: 1.5rem;
}
.ge-actions {
display: flex;
gap: 0.75rem;
justify-content: center;
}
.ge-btn {
height: 40px;
padding: 0 1.5rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: box-shadow 0.2s, border-color 0.2s, background 0.2s;
}
.ge-btn-primary {
border: none;
background: oklch(0.8 0.14 65);
color: oklch(0.13 0.006 55);
}
.ge-btn-primary:hover, .ge-btn-primary:focus-visible {
box-shadow: 0 10px 25px -5px oklch(0.8 0.14 65 / 0.2);
}
.ge-btn-secondary {
border: 1px solid oklch(1 0.015 65 / 0.1);
background: transparent;
color: oklch(0.93 0.015 80);
}
.ge-btn-secondary:hover, .ge-btn-secondary:focus-visible {
border-color: oklch(0.8 0.14 65 / 0.4);
background: oklch(0.8 0.14 65 / 0.05);
}
`}</style>
</head>
<body>
<div className="ge-glow" />
<div style={{ position: "relative", zIndex: 1 }}>
<div className="ge-icon-ring">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
role="img"
aria-label="Error"
>
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M13.41 10.59l2.59 -2.59" />
</svg>
</div>
<h1 className="ge-title">Intermission</h1>
<p className="ge-desc">
We hit a snag loading this page. This is usually temporary &mdash;
try refreshing or come back in a moment.
</p>
{error.digest && (
<p className="ge-digest">Error ID: {error.digest}</p>
)}
<div className="ge-actions">
<button
type="button"
onClick={reset}
className="ge-btn ge-btn-primary"
>
Try again
</button>
<button
type="button"
onClick={() => {
window.location.href = "/";
}}
className="ge-btn ge-btn-secondary"
>
Return home
</button>
</div>
</div>
</body>
</html>
);
}
+74
View File
@@ -0,0 +1,74 @@
import Link from "next/link";
import { SofaLogo } from "@/components/sofa-logo";
export default function NotFound() {
return (
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-6">
{/* Warm projector glow */}
<div className="pointer-events-none absolute left-1/2 top-[40%] h-[500px] w-[500px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/4 blur-[160px]" />
{/* Subtle vertical light beam */}
<div
className="pointer-events-none absolute left-1/2 top-0 h-[60vh] w-px -translate-x-1/2"
style={{
background:
"linear-gradient(to bottom, oklch(0.8 0.14 65 / 0.12), transparent)",
}}
/>
<div className="relative z-10 flex flex-col items-center text-center">
{/* Frame counter label */}
<p
className="animate-stagger-item font-mono text-[10px] tracking-[0.5em] uppercase text-muted-foreground/40"
style={{ "--stagger-index": 0 } as React.CSSProperties}
>
Scene not found
</p>
{/* Large ghosted 404 */}
<h1
className="animate-stagger-item font-display text-[8rem] leading-[0.85] tracking-tight text-foreground/[0.06] sm:text-[11rem] md:text-[14rem]"
style={{ "--stagger-index": 1 } as React.CSSProperties}
>
404
</h1>
{/* Message */}
<div
className="animate-stagger-item -mt-2 space-y-3 sm:-mt-4"
style={{ "--stagger-index": 2 } as React.CSSProperties}
>
<h2 className="font-display text-2xl tracking-tight sm:text-3xl">
Lost in the credits
</h2>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground">
This page was left on the cutting room floor. It may have been
moved, removed, or never made it past the screenplay.
</p>
</div>
{/* Actions */}
<div
className="animate-stagger-item mt-8 flex items-center gap-3"
style={{ "--stagger-index": 3 } as React.CSSProperties}
>
<Link
href="/"
className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-6 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20"
>
<span className="relative z-10">Return home</span>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
</Link>
</div>
{/* Logo watermark */}
<div
className="animate-stagger-item mt-16 text-muted-foreground/20"
style={{ "--stagger-index": 4 } as React.CSSProperties}
>
<SofaLogo className="size-8" />
</div>
</div>
</div>
);
}