mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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't exist or may have been moved. Try searching for
|
||||||
|
what you'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,20 +2,45 @@ import Link from "next/link";
|
|||||||
|
|
||||||
export default function PersonNotFound() {
|
export default function PersonNotFound() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-4 py-24 text-center">
|
<div className="flex flex-col items-center gap-6 py-24 text-center">
|
||||||
<h1 className="font-display text-4xl tracking-tight text-balance">
|
<h1
|
||||||
Person not found
|
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>
|
</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're looking for doesn't exist or may have been
|
The person you're looking for doesn't exist or may have been
|
||||||
removed.
|
removed from the database.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="animate-stagger-item flex items-center gap-3"
|
||||||
|
style={{ "--stagger-index": 2 } as React.CSSProperties}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
href="/explore"
|
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>
|
||||||
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,45 @@ import Link from "next/link";
|
|||||||
|
|
||||||
export default function TitleNotFound() {
|
export default function TitleNotFound() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-4 py-24 text-center">
|
<div className="flex flex-col items-center gap-6 py-24 text-center">
|
||||||
<h1 className="font-display text-4xl tracking-tight">Title not found</h1>
|
<h1
|
||||||
<p className="text-muted-foreground">
|
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're looking for doesn't exist or may have been
|
The title you're looking for doesn't exist or may have been
|
||||||
removed.
|
removed from the database.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="animate-stagger-item flex items-center gap-3"
|
||||||
|
style={{ "--stagger-index": 2 } as React.CSSProperties}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
href="/explore"
|
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>
|
||||||
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 —
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user