portfolio_v2/app/error.tsx
franck ec005b1622 Refonte du portfolio : profil GitHub, CV, SEO, export statique
- Accueil façon profil GitHub (bio, homelab, projets épinglés, parcours)
  avec fond dégradé et panneaux liquid glass
- /cv : consultable en ligne (thème sombre), imprimable en PDF propre
  (thème clair, 1 page) ; numéro de téléphone non scrapable
- Pages d'erreur 404 / 500 / global-error animées (GSAP)
- Métadonnées : Open Graph, Twitter, JSON-LD Person, sitemap, robots
  (allowlist Google + bots d'aperçu, reste bloqué)
- Images OG et favicon en PNG statiques
- Contenu du CV : Saint-Paul, langues, expériences détaillées
- Export statique (output: export) + DEPLOY.md
- Retrait de l'indicateur de dev Next.js et des fichiers du starter

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3hzMzvei419MwkKFZkvMX
2026-09-08 19:33:47 +04:00

101 lines
2.7 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import Link from "next/link";
import gsap from "gsap";
export default function RouteError({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
const root = useRef<HTMLElement>(null);
useEffect(() => {
console.error(error);
}, [error]);
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
const ctx = gsap.context(() => {
gsap.set("[data-fade]", { autoAlpha: 0, y: 14 });
gsap
.timeline({ defaults: { ease: "power3.out" } })
.from("[data-icon]", { autoAlpha: 0, scale: 0.6, duration: 0.4, ease: "back.out(2)" })
.to("[data-icon]", {
keyframes: { rotate: [0, -9, 8, -6, 4, 0] },
duration: 0.55,
ease: "power1.inOut",
})
.to("[data-fade]", { autoAlpha: 1, y: 0, duration: 0.5, stagger: 0.08 }, "-=0.3")
.to("[data-icon]", {
scale: 1.05,
duration: 1.6,
ease: "sine.inOut",
repeat: -1,
yoyo: true,
});
}, root);
return () => ctx.revert();
}, []);
return (
<main
ref={root}
className="flex min-h-screen flex-col items-center justify-center gap-6 px-6 text-center"
>
<svg
data-icon
viewBox="0 0 24 24"
className="size-20 text-danger"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
<div data-fade className="space-y-2">
<h1 className="text-2xl font-semibold">Quelque chose a planté</h1>
<p className="max-w-[46ch] text-muted">
Une erreur inattendue s&apos;est produite pendant le rendu de cette
page.
</p>
</div>
<div data-fade className="flex flex-wrap justify-center gap-3">
<button
type="button"
onClick={() => retry()}
className="rounded-md border border-line bg-surface px-4 py-2 text-sm font-semibold hover:border-muted"
>
Réessayer
</button>
<Link
href="/"
className="rounded-md border border-line px-4 py-2 text-sm font-semibold text-accent hover:border-muted"
>
Retour à l&apos;accueil
</Link>
</div>
{error.digest && (
<p data-fade className="font-mono text-xs text-muted">
ref: {error.digest}
</p>
)}
</main>
);
}