mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 02:45: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() {
|
||||
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
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
The person you're looking for doesn't exist or may have been
|
||||
removed.
|
||||
</p>
|
||||
<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"
|
||||
<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}
|
||||
>
|
||||
Explore titles
|
||||
</Link>
|
||||
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">
|
||||
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
|
||||
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="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">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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
The title you're looking for doesn't exist or may have been
|
||||
removed.
|
||||
</p>
|
||||
<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"
|
||||
<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}
|
||||
>
|
||||
Explore titles
|
||||
</Link>
|
||||
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
|
||||
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="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">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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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