179 lines
5.4 KiB
JavaScript
179 lines
5.4 KiB
JavaScript
import { useEffect, useRef } from 'react';
|
|
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
|
import { ArrowLeft, Home, FileQuestion } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
|
|
// Animated digit — cycles through random numbers before settling
|
|
function GlitchDigit({ value, delay = 0 }) {
|
|
const ref = useRef(null);
|
|
|
|
useEffect(() => {
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
|
|
const frames = 14;
|
|
const digits = '0123456789';
|
|
let frame = 0;
|
|
let start = null;
|
|
|
|
function tick(ts) {
|
|
if (!start) start = ts + delay;
|
|
const elapsed = ts - start;
|
|
if (elapsed < 0) { requestAnimationFrame(tick); return; }
|
|
|
|
if (frame < frames) {
|
|
el.textContent = digits[Math.floor(Math.random() * 10)];
|
|
frame++;
|
|
setTimeout(() => requestAnimationFrame(tick), 40 + frame * 6);
|
|
} else {
|
|
el.textContent = value;
|
|
el.classList.add('settled');
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(tick);
|
|
}, [value, delay]);
|
|
|
|
return (
|
|
<span
|
|
ref={ref}
|
|
style={{
|
|
display: 'inline-block',
|
|
fontVariantNumeric: 'tabular-nums',
|
|
transition: 'opacity 0.15s',
|
|
}}
|
|
>
|
|
{value}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export default function NotFoundPage() {
|
|
const { user } = useAuth();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
const canGoBack = window.history.length > 1;
|
|
const badPath = location.pathname;
|
|
|
|
return (
|
|
<div
|
|
className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-background text-foreground"
|
|
style={{
|
|
background: `
|
|
radial-gradient(ellipse 80% 60% at 50% -10%, oklch(var(--primary) / 0.12), transparent),
|
|
radial-gradient(ellipse 60% 40% at 100% 100%, oklch(var(--primary) / 0.06), transparent),
|
|
oklch(var(--background))
|
|
`,
|
|
}}
|
|
>
|
|
{/* Grid overlay */}
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute inset-0"
|
|
style={{
|
|
backgroundImage: `
|
|
linear-gradient(oklch(var(--border) / 0.35) 1px, transparent 1px),
|
|
linear-gradient(90deg, oklch(var(--border) / 0.35) 1px, transparent 1px)
|
|
`,
|
|
backgroundSize: '48px 48px',
|
|
maskImage: 'radial-gradient(ellipse 80% 80% at 50% 50%, black 30%, transparent 100%)',
|
|
WebkitMaskImage: 'radial-gradient(ellipse 80% 80% at 50% 50%, black 30%, transparent 100%)',
|
|
}}
|
|
/>
|
|
|
|
{/* Glow orb */}
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full blur-3xl"
|
|
style={{
|
|
width: '36rem',
|
|
height: '20rem',
|
|
background: 'oklch(var(--primary) / 0.07)',
|
|
}}
|
|
/>
|
|
|
|
{/* Content */}
|
|
<div className="relative z-10 flex flex-col items-center gap-6 px-6 text-center">
|
|
|
|
{/* Icon badge */}
|
|
<div
|
|
className="flex h-14 w-14 items-center justify-center rounded-2xl border"
|
|
style={{
|
|
background: 'oklch(var(--primary) / 0.08)',
|
|
borderColor: 'oklch(var(--primary) / 0.25)',
|
|
}}
|
|
>
|
|
<FileQuestion
|
|
className="h-6 w-6"
|
|
style={{ color: 'oklch(var(--primary))' }}
|
|
/>
|
|
</div>
|
|
|
|
{/* 404 */}
|
|
<div
|
|
className="select-none font-mono font-black leading-none tracking-tighter"
|
|
style={{
|
|
fontSize: 'clamp(6rem, 22vw, 14rem)',
|
|
background: `linear-gradient(
|
|
135deg,
|
|
oklch(var(--foreground)) 0%,
|
|
oklch(var(--foreground) / 0.55) 50%,
|
|
oklch(var(--primary) / 0.5) 100%
|
|
)`,
|
|
WebkitBackgroundClip: 'text',
|
|
WebkitTextFillColor: 'transparent',
|
|
backgroundClip: 'text',
|
|
letterSpacing: '-0.06em',
|
|
}}
|
|
>
|
|
<GlitchDigit value="4" delay={0} />
|
|
<GlitchDigit value="0" delay={180} />
|
|
<GlitchDigit value="4" delay={360} />
|
|
</div>
|
|
|
|
{/* Headline */}
|
|
<div className="space-y-2">
|
|
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
|
Nothing here but debt
|
|
</h1>
|
|
<p className="max-w-sm text-sm leading-relaxed text-muted-foreground">
|
|
{badPath !== '/'
|
|
? <>The page <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">{badPath}</code> doesn't exist.</>
|
|
: <>That page doesn't exist.</>
|
|
}
|
|
{' '}Check the URL or head back somewhere useful.
|
|
</p>
|
|
</div>
|
|
|
|
{/* CTAs */}
|
|
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
<Button asChild size="default">
|
|
<Link to={user ? '/' : '/login'}>
|
|
<Home className="h-4 w-4" />
|
|
{user ? 'Dashboard' : 'Sign in'}
|
|
</Link>
|
|
</Button>
|
|
|
|
{canGoBack && (
|
|
<Button
|
|
variant="outline"
|
|
size="default"
|
|
onClick={() => navigate(-1)}
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Go back
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Path hint */}
|
|
<p className="text-xs text-muted-foreground/40">
|
|
error 404 · page not found
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|