diff --git a/src/app/dock/error.tsx b/src/app/dock/error.tsx new file mode 100644 index 0000000..8514a1a --- /dev/null +++ b/src/app/dock/error.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { useEffect } from 'react'; +import Link from 'next/link'; +import { GiSinkingShip } from 'react-icons/gi'; +import { cn } from '@/lib/utils/cn'; + +interface DockErrorProps { + error: Error & { digest?: string }; + unstable_retry: () => void; +} + +function DockError({ error, unstable_retry }: DockErrorProps) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( +
+
+ ); +} + +const styles = { + root: cn( + 'text-gd-text flex min-h-[70vh] flex-col items-center justify-center gap-4 px-6 text-center', + ), + icon: cn('text-gd-error text-7xl'), + title: cn('text-3xl font-semibold'), + description: cn('text-gd-muted max-w-md text-balance'), + actions: cn('mt-2 flex flex-wrap items-center justify-center gap-3'), + retry: cn( + 'bg-gd-accent text-gd-bg hover:bg-gd-accent-glow active:translate-y-px', + 'focus-visible:outline-gd-accent focus-visible:outline-2 focus-visible:outline-offset-2', + 'rounded-lg px-5 py-2.5 text-sm font-medium transition-colors', + ), + back: cn( + 'text-gd-muted hover:text-gd-text active:translate-y-px', + 'focus-visible:outline-gd-accent focus-visible:outline-2 focus-visible:outline-offset-2', + 'rounded-lg px-5 py-2.5 text-sm font-medium transition-colors', + ), +}; + +export default DockError; diff --git a/src/app/dock/not-found.tsx b/src/app/dock/not-found.tsx new file mode 100644 index 0000000..1c0c3c0 --- /dev/null +++ b/src/app/dock/not-found.tsx @@ -0,0 +1,39 @@ +import Link from 'next/link'; +import { GiBoatHorizon } from 'react-icons/gi'; +import { cn } from '@/lib/utils/cn'; + +function DockNotFound() { + return ( +
+
+ ); +} + +const styles = { + root: cn( + 'text-gd-text flex min-h-[70vh] flex-col items-center justify-center gap-4 px-6 text-center', + ), + icon: cn('text-gd-muted text-7xl'), + heading: cn('flex flex-col items-center gap-1'), + code: cn('text-gd-accent text-sm font-semibold tracking-[0.2em]'), + title: cn('text-3xl font-semibold'), + description: cn('text-gd-muted max-w-md text-balance'), + action: cn( + 'bg-gd-accent text-gd-bg hover:bg-gd-accent-glow active:translate-y-px', + 'focus-visible:outline-gd-accent focus-visible:outline-2 focus-visible:outline-offset-2', + 'mt-2 rounded-lg px-5 py-2.5 text-sm font-medium transition-colors', + ), +}; + +export default DockNotFound; diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..9e388bc --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { useEffect } from 'react'; +import { GiSinkingShip } from 'react-icons/gi'; +import { cn } from '@/lib/utils/cn'; + +interface RootErrorProps { + error: Error & { digest?: string }; + unstable_retry: () => void; +} + +function RootError({ error, unstable_retry }: RootErrorProps) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( +
+
+ ); +} + +const styles = { + root: cn( + 'bg-gd-bg text-gd-text flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center', + ), + icon: cn('text-gd-error text-7xl'), + title: cn('text-3xl font-semibold'), + description: cn('text-gd-muted max-w-md text-balance'), + action: cn( + 'bg-gd-accent text-gd-bg hover:bg-gd-accent-glow active:translate-y-px', + 'focus-visible:outline-gd-accent focus-visible:outline-2 focus-visible:outline-offset-2', + 'mt-2 rounded-lg px-5 py-2.5 text-sm font-medium transition-colors', + ), +}; + +export default RootError; diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx new file mode 100644 index 0000000..b37b558 --- /dev/null +++ b/src/app/global-error.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useEffect } from 'react'; +import { GiSinkingShip } from 'react-icons/gi'; +import { cn } from '@/lib/utils/cn'; +import './globals.css'; + +interface GlobalErrorProps { + error: Error & { digest?: string }; + unstable_retry: () => void; +} + +function GlobalError({ error, unstable_retry }: GlobalErrorProps) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( + + + Something went wrong · GhostDock +
+
+ + + ); +} + +const styles = { + body: cn('bg-gd-bg text-gd-text'), + root: cn('flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center'), + icon: cn('text-gd-error text-7xl'), + title: cn('text-3xl font-semibold'), + description: cn('text-gd-muted max-w-md text-balance'), + action: cn( + 'bg-gd-accent text-gd-bg hover:bg-gd-accent-glow active:translate-y-px', + 'focus-visible:outline-gd-accent focus-visible:outline-2 focus-visible:outline-offset-2', + 'mt-2 rounded-lg px-5 py-2.5 text-sm font-medium transition-colors', + ), +}; + +export default GlobalError; diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..a490b72 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,39 @@ +import Link from 'next/link'; +import { GiBoatHorizon } from 'react-icons/gi'; +import { cn } from '@/lib/utils/cn'; + +function RootNotFound() { + return ( +
+
+ ); +} + +const styles = { + root: cn( + 'bg-gd-bg text-gd-text flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center', + ), + icon: cn('text-gd-muted text-7xl'), + heading: cn('flex flex-col items-center gap-1'), + code: cn('text-gd-accent text-sm font-semibold tracking-[0.2em]'), + title: cn('text-3xl font-semibold'), + description: cn('text-gd-muted max-w-md text-balance'), + action: cn( + 'bg-gd-accent text-gd-bg hover:bg-gd-accent-glow active:translate-y-px', + 'focus-visible:outline-gd-accent focus-visible:outline-2 focus-visible:outline-offset-2', + 'mt-2 rounded-lg px-5 py-2.5 text-sm font-medium transition-colors', + ), +}; + +export default RootNotFound; diff --git a/src/app/p/[slug]/error.tsx b/src/app/p/[slug]/error.tsx new file mode 100644 index 0000000..136401e --- /dev/null +++ b/src/app/p/[slug]/error.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useEffect } from 'react'; +import { cn } from '@/lib/utils/cn'; + +// Applies the visitor's saved theme before first paint, so there is no light/dark flash. +const themeScript = `(function(){try{var t=localStorage.getItem('pp-theme');if(t==='light'||t==='dark'){document.documentElement.setAttribute('data-pp-theme',t);}}catch(e){}})();`; + +interface PublicErrorProps { + error: Error & { digest?: string }; + unstable_retry: () => void; +} + +function PublicError({ error, unstable_retry }: PublicErrorProps) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( + <> +