Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/app/dock/error.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className={styles.root}>
<GiSinkingShip className={styles.icon} aria-hidden="true" />
<h1 className={styles.title}>We’ve sprung a leak</h1>
<p className={styles.description}>
We hit rough water loading your dock. Try again, or head back.
</p>
<div className={styles.actions}>
<button type="button" onClick={() => unstable_retry()} className={styles.retry}>
Try again
</button>
<Link href="/dock" className={styles.back}>
Back to your dock
</Link>
</div>
</main>
);
}

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;
39 changes: 39 additions & 0 deletions src/app/dock/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Link from 'next/link';
import { GiBoatHorizon } from 'react-icons/gi';
import { cn } from '@/lib/utils/cn';

function DockNotFound() {
return (
<main className={styles.root}>
<GiBoatHorizon className={styles.icon} aria-hidden="true" />
<div className={styles.heading}>
<p className={styles.code}>404</p>
<h1 className={styles.title}>Project not found</h1>
</div>
<p className={styles.description}>
That project drifted off the map — it may have been removed, or the link’s wrong.
</p>
<Link href="/dock" className={styles.action}>
Back to your dock
</Link>
</main>
);
}

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;
45 changes: 45 additions & 0 deletions src/app/error.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className={styles.root}>
<GiSinkingShip className={styles.icon} aria-hidden="true" />
<h1 className={styles.title}>We’ve sprung a leak</h1>
<p className={styles.description}>
Something went wrong on deck. Try again — if it keeps happening, check back later.
</p>
<button type="button" onClick={() => unstable_retry()} className={styles.action}>
Try again
</button>
</main>
);
}

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;
50 changes: 50 additions & 0 deletions src/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<html lang="en">
<body className={styles.body}>
<title>Something went wrong · GhostDock</title>
<main className={styles.root}>
<GiSinkingShip className={styles.icon} aria-hidden="true" />
<h1 className={styles.title}>We’ve sprung a leak</h1>
<p className={styles.description}>
GhostDock hit an unexpected error. Try again in a moment.
</p>
<button type="button" onClick={() => unstable_retry()} className={styles.action}>
Try again
</button>
</main>
</body>
</html>
);
}

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;
39 changes: 39 additions & 0 deletions src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Link from 'next/link';
import { GiBoatHorizon } from 'react-icons/gi';
import { cn } from '@/lib/utils/cn';

function RootNotFound() {
return (
<main className={styles.root}>
<GiBoatHorizon className={styles.icon} aria-hidden="true" />
<div className={styles.heading}>
<p className={styles.code}>404</p>
<h1 className={styles.title}>Lost in the fog</h1>
</div>
<p className={styles.description}>
This page drifted off the map. Let’s get you back to shore.
</p>
<Link href="/" className={styles.action}>
Return home
</Link>
</main>
);
}

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;
46 changes: 46 additions & 0 deletions src/app/p/[slug]/error.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<main className={styles.root}>
<h1 className={styles.title}>Something went wrong</h1>
<p className={styles.description}>This page couldn’t be loaded. Try again in a moment.</p>
<button type="button" onClick={() => unstable_retry()} className={styles.action}>
Try again
</button>
</main>
</>
);
}

const styles = {
root: cn(
'bg-pp-paper text-pp-ink flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center',
),
title: cn('text-pp-ink-strong text-3xl font-semibold'),
description: cn('text-pp-muted max-w-md text-balance'),
action: cn(
'border-pp-rule text-pp-ink hover:bg-pp-paper-2 active:translate-y-px',
'focus-visible:outline-pp-focus focus-visible:outline-2 focus-visible:outline-offset-2',
'mt-2 rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors',
),
};

export default PublicError;
40 changes: 40 additions & 0 deletions src/app/p/[slug]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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){}})();`;

function PublicLoading() {
return (
<>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<main className={styles.root} role="status" aria-label="Loading project">
<span className={styles.srOnly}>Loading project…</span>
<div className={styles.inner} aria-hidden="true">
<div className={styles.title} />
<div className={styles.lines}>
<div className={styles.line} />
<div className={cn(styles.line, 'w-2/3')} />
</div>
<div className={styles.badges}>
<div className={styles.badge} />
<div className={styles.badge} />
<div className={styles.badge} />
</div>
</div>
</main>
</>
);
}

const styles = {
root: cn('bg-pp-paper min-h-screen'),
srOnly: cn('sr-only'),
inner: cn('mx-auto flex max-w-2xl flex-col gap-6 px-5 py-16 motion-safe:animate-pulse sm:py-20'),
title: cn('bg-pp-rule h-10 w-2/3 rounded-md'),
lines: cn('flex flex-col gap-2.5'),
line: cn('bg-pp-rule h-4 w-full rounded'),
badges: cn('mt-2 flex gap-2'),
badge: cn('bg-pp-rule h-7 w-20 rounded-full'),
};

export default PublicLoading;
42 changes: 42 additions & 0 deletions src/app/p/[slug]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Link from 'next/link';
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){}})();`;

function PublicNotFound() {
return (
<>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<main className={styles.root}>
<div className={styles.heading}>
<p className={styles.code}>404</p>
<h1 className={styles.title}>Project not found</h1>
</div>
<p className={styles.description}>
This project page doesn’t exist, or it hasn’t been published yet.
</p>
<Link href="/" className={styles.action}>
Go to GhostDock
</Link>
</main>
</>
);
}

const styles = {
root: cn(
'bg-pp-paper text-pp-ink flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center',
),
heading: cn('flex flex-col items-center gap-1'),
code: cn('text-pp-muted text-sm font-semibold tracking-[0.2em]'),
title: cn('text-pp-ink-strong text-3xl font-semibold'),
description: cn('text-pp-muted max-w-md text-balance'),
action: cn(
'border-pp-rule text-pp-ink hover:bg-pp-paper-2 active:translate-y-px',
'focus-visible:outline-pp-focus focus-visible:outline-2 focus-visible:outline-offset-2',
'mt-2 rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors',
),
};

export default PublicNotFound;
Loading