From fbd09fbdae97936f43751c983acaa730922138ce Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Mon, 20 Apr 2026 19:53:50 +0200 Subject: [PATCH] refactor(errors): redesign error pages in editorial style Replace the emoji/gradient card with an editorial layout: small lucide glyph, oversized error code, hairline-divided serif bible quote. Extract shared ErrorView + SectionError components and a bilingual string helper. Add +error.svelte at each section root (faith, recipes, fitness, tasks, cospend) so errors render inside the correct layout and inherit the section-specific header/nav. Catch-all [...rest]/+page.ts stubs route unmatched URLs through the section layout so the right error page catches them. --- package.json | 2 +- src/lib/components/ErrorView.svelte | 222 +++++++++ src/lib/components/SectionError.svelte | 59 +++ src/lib/js/errorStrings.ts | 48 ++ src/routes/+error.svelte | 444 ++---------------- .../[cospendRoot=cospendRoot]/+error.svelte | 14 + .../[...rest]/+page.ts | 5 + .../[faithLang=faithLang]/+error.svelte | 13 + .../[faithLang=faithLang]/[...rest]/+page.ts | 5 + .../[recipeLang=recipeLang]/+error.svelte | 13 + .../[...rest]/+page.ts | 5 + .../[name]/+error.svelte | 430 +++-------------- src/routes/fitness/+error.svelte | 14 + src/routes/fitness/[...rest]/+page.ts | 5 + src/routes/tasks/+error.svelte | 9 + src/routes/tasks/[...rest]/+page.ts | 5 + 16 files changed, 505 insertions(+), 788 deletions(-) create mode 100644 src/lib/components/ErrorView.svelte create mode 100644 src/lib/components/SectionError.svelte create mode 100644 src/lib/js/errorStrings.ts create mode 100644 src/routes/[cospendRoot=cospendRoot]/+error.svelte create mode 100644 src/routes/[cospendRoot=cospendRoot]/[...rest]/+page.ts create mode 100644 src/routes/[faithLang=faithLang]/+error.svelte create mode 100644 src/routes/[faithLang=faithLang]/[...rest]/+page.ts create mode 100644 src/routes/[recipeLang=recipeLang]/+error.svelte create mode 100644 src/routes/[recipeLang=recipeLang]/[...rest]/+page.ts create mode 100644 src/routes/fitness/+error.svelte create mode 100644 src/routes/fitness/[...rest]/+page.ts create mode 100644 src/routes/tasks/+error.svelte create mode 100644 src/routes/tasks/[...rest]/+page.ts diff --git a/package.json b/package.json index db61d541..a4dbbe77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.36.3", + "version": "1.37.0", "private": true, "type": "module", "scripts": { diff --git a/src/lib/components/ErrorView.svelte b/src/lib/components/ErrorView.svelte new file mode 100644 index 00000000..bfdd880f --- /dev/null +++ b/src/lib/components/ErrorView.svelte @@ -0,0 +1,222 @@ + + +
+
+
+
+ + + +

{title}

+

{description}

+ + {#if details} +

{details}

+ {/if} + + {#if actions} + + {/if} + + {#if bibleQuote} +
+
+
+ {openQuote}{bibleQuote.text}{closeQuote} +
+
{bibleQuote.reference}
+
+ {/if} +
+
+ + diff --git a/src/lib/components/SectionError.svelte b/src/lib/components/SectionError.svelte new file mode 100644 index 00000000..70f2d5f1 --- /dev/null +++ b/src/lib/components/SectionError.svelte @@ -0,0 +1,59 @@ + + + + {#snippet actions()} + {#if extraActions}{@render extraActions()}{/if} + + {#if status === 401} + + {label} + {:else if status === 500} + + {label} + {:else} + {label} + + {/if} + {/snippet} + diff --git a/src/lib/js/errorStrings.ts b/src/lib/js/errorStrings.ts new file mode 100644 index 00000000..328252d6 --- /dev/null +++ b/src/lib/js/errorStrings.ts @@ -0,0 +1,48 @@ +export function getErrorTitle(status: number, isEnglish: boolean): string { + if (isEnglish) { + switch (status) { + case 401: return 'Login Required'; + case 403: return 'Access Denied'; + case 404: return 'Page Not Found'; + case 500: return 'Server Error'; + default: return 'Error'; + } + } + switch (status) { + case 401: return 'Anmeldung erforderlich'; + case 403: return 'Zugriff verweigert'; + case 404: return 'Seite nicht gefunden'; + case 500: return 'Serverfehler'; + default: return 'Fehler'; + } +} + +export function getErrorDescription(status: number, isEnglish: boolean): string { + if (isEnglish) { + switch (status) { + case 401: return 'You must be logged in to access this page.'; + case 403: return 'You do not have permission for this area.'; + case 404: return 'The requested page could not be found.'; + case 500: return 'An unexpected error occurred. Please try again later.'; + default: return 'An unexpected error occurred.'; + } + } + switch (status) { + case 401: return 'Du musst angemeldet sein, um auf diese Seite zugreifen zu können.'; + case 403: return 'Du hast keine Berechtigung für diesen Bereich.'; + case 404: return 'Die angeforderte Seite konnte nicht gefunden werden.'; + case 500: return 'Es ist ein unerwarteter Fehler aufgetreten. Bitte versuche es später erneut.'; + default: return 'Es ist ein unerwarteter Fehler aufgetreten.'; + } +} + +export const errorLabels = { + login: { en: 'Log in', de: 'Anmelden' }, + tryAgain: { en: 'Try again', de: 'Erneut versuchen' }, + goBack: { en: 'Go back', de: 'Zurück' }, + homepage: { en: 'Homepage', de: 'Startseite' } +} as const; + +export function pick(pair: { en: string; de: string }, isEnglish: boolean): string { + return isEnglish ? pair.en : pair.de; +} diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index ae923a5d..5f934c6f 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -1,431 +1,53 @@ - - {getErrorTitle(status)} - Alexander's Website + {getErrorTitle(status, isEnglish)} — Alexander's Website
{#snippet links()} - + {/snippet} -
-
-
- {getErrorIcon(status)} -
- -

- {getErrorTitle(status)} -

- -
- {isEnglish ? 'Error' : 'Fehler'} {status} -
- -

- {getErrorDescription(status)} -

- - {#if /** @type {any} */ (error)?.details} -
- {/** @type {any} */ (error).details} -
+ + {#snippet actions()} + {#if status === 401} + + + {:else if status === 500} + + + {:else} + + {/if} - -
- {#if status === 401} - - - {:else if status === 500} - - - {:else} - - - {/if} -
- - - {#if bibleQuote} -
-
- {isEnglish ? '"' : '„'}{bibleQuote.text}{isEnglish ? '"' : '"'} -
-
- — {bibleQuote.reference} -
-
- {/if} -
-
+ {/snippet} +
- - diff --git a/src/routes/[cospendRoot=cospendRoot]/+error.svelte b/src/routes/[cospendRoot=cospendRoot]/+error.svelte new file mode 100644 index 00000000..db7bf737 --- /dev/null +++ b/src/routes/[cospendRoot=cospendRoot]/+error.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/[cospendRoot=cospendRoot]/[...rest]/+page.ts b/src/routes/[cospendRoot=cospendRoot]/[...rest]/+page.ts new file mode 100644 index 00000000..8ea3fd8e --- /dev/null +++ b/src/routes/[cospendRoot=cospendRoot]/[...rest]/+page.ts @@ -0,0 +1,5 @@ +import { error } from '@sveltejs/kit'; + +export const load = () => { + error(404, 'Not found'); +}; diff --git a/src/routes/[faithLang=faithLang]/+error.svelte b/src/routes/[faithLang=faithLang]/+error.svelte new file mode 100644 index 00000000..a62f50f2 --- /dev/null +++ b/src/routes/[faithLang=faithLang]/+error.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/routes/[faithLang=faithLang]/[...rest]/+page.ts b/src/routes/[faithLang=faithLang]/[...rest]/+page.ts new file mode 100644 index 00000000..8ea3fd8e --- /dev/null +++ b/src/routes/[faithLang=faithLang]/[...rest]/+page.ts @@ -0,0 +1,5 @@ +import { error } from '@sveltejs/kit'; + +export const load = () => { + error(404, 'Not found'); +}; diff --git a/src/routes/[recipeLang=recipeLang]/+error.svelte b/src/routes/[recipeLang=recipeLang]/+error.svelte new file mode 100644 index 00000000..cc557181 --- /dev/null +++ b/src/routes/[recipeLang=recipeLang]/+error.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/routes/[recipeLang=recipeLang]/[...rest]/+page.ts b/src/routes/[recipeLang=recipeLang]/[...rest]/+page.ts new file mode 100644 index 00000000..8ea3fd8e --- /dev/null +++ b/src/routes/[recipeLang=recipeLang]/[...rest]/+page.ts @@ -0,0 +1,5 @@ +import { error } from '@sveltejs/kit'; + +export const load = () => { + error(404, 'Not found'); +}; diff --git a/src/routes/[recipeLang=recipeLang]/[name]/+error.svelte b/src/routes/[recipeLang=recipeLang]/[name]/+error.svelte index 70c41923..d40211c6 100644 --- a/src/routes/[recipeLang=recipeLang]/[name]/+error.svelte +++ b/src/routes/[recipeLang=recipeLang]/[name]/+error.svelte @@ -2,28 +2,29 @@ import { page } from '$app/stores'; import { goto } from '$app/navigation'; import { onMount } from 'svelte'; + import ErrorView from '$lib/components/ErrorView.svelte'; + import { getErrorTitle, getErrorDescription, errorLabels, pick } from '$lib/js/errorStrings'; let status = $derived($page.status); - let error = $derived($page.error); + let error = $derived($page.error as any); let recipeLang = $derived($page.params.recipeLang); let recipeName = $derived($page.params.name); - let session = $derived($page.data?.session); - let user = $derived(session?.user); + let user = $derived($page.data?.session?.user); + + let isEnglishRoute = $derived(recipeLang === 'recipes'); + let isEnglish = $derived(error?.lang === 'en' || isEnglishRoute); + let bibleQuote = $derived(error?.bibleQuote); - // State to track if German recipe exists let germanRecipeExists = $state(false); let checkingGermanRecipe = $state(false); - // Check if German recipe exists when on English route with 404 onMount(async () => { - const isEnglishRoute = recipeLang === 'recipes'; if (isEnglishRoute && status === 404) { checkingGermanRecipe = true; try { - // Check if German recipe exists const response = await fetch(`/api/rezepte/items/${recipeName}`); germanRecipeExists = response.ok; - } catch (e) { + } catch { germanRecipeExists = false; } finally { checkingGermanRecipe = false; @@ -31,384 +32,61 @@ } }); - function getErrorTitle(status: number) { - switch (status) { - case 401: - return 'Authentication Required'; - case 403: - return 'Access Denied'; - case 404: - return 'Recipe Not Found'; - case 500: - return 'Server Error'; - default: - return 'Error'; - } - } + let showGermanFallback = $derived( + status === 404 && isEnglishRoute && germanRecipeExists && !checkingGermanRecipe + ); - function getErrorDescription(status: number) { - const isEnglishRoute = recipeLang === 'recipes'; + let title = $derived( + status === 404 ? (isEnglish ? 'Recipe Not Found' : 'Rezept nicht gefunden') + : getErrorTitle(status, isEnglish) + ); - switch (status) { - case 401: - return 'You must be logged in to access this page.'; - case 403: - return 'You do not have permission to access this area.'; - case 404: - if (isEnglishRoute && germanRecipeExists) { - return 'This recipe has not been translated to English yet, but the German version is available.'; - } - return 'The requested recipe could not be found.'; - case 500: - return 'An unexpected error occurred. Please try again later.'; - default: - return 'An unexpected error occurred.'; - } - } + let description = $derived( + showGermanFallback + ? 'This recipe has not been translated to English yet, but the German version is available.' + : status === 404 + ? (isEnglish ? 'The requested recipe could not be found.' : 'Das angeforderte Rezept konnte nicht gefunden werden.') + : getErrorDescription(status, isEnglish) + ); - function getErrorIcon(status: number) { - switch (status) { - case 401: - return '🔐'; - case 403: - return '🚫'; - case 404: - return '🔍'; - case 500: - return '⚠️'; - default: - return '❌'; - } - } + let details = $derived( + checkingGermanRecipe + ? (isEnglish ? 'Checking for German version…' : 'Suche nach deutscher Version…') + : error?.details + ); - function viewGermanRecipe() { - goto(`/rezepte/${recipeName}`); - } - - function editToTranslate() { - goto(`/rezepte/edit/${recipeName}`); - } - - function goToRecipes() { - const lang = recipeLang === 'recipes' ? 'recipes' : 'rezepte'; - goto(`/${lang}`); - } + let recipesHref = $derived(isEnglishRoute ? '/recipes' : '/rezepte'); + function viewGermanRecipe() { goto(`/rezepte/${recipeName}`); } + function editToTranslate() { goto(`/rezepte/edit/${recipeName}`); } function goBack() { - if (window.history.length > 1) { - window.history.back(); - } else { - goToRecipes(); - } - } - - function login() { - goto('/login'); + if (window.history.length > 1) window.history.back(); + else goto(recipesHref); } + function login() { goto('/login'); } - {getErrorTitle(status)} - Alexander's Website + {title} — Alexander's Website -
-
-
- {getErrorIcon(status)} -
- -

- {getErrorTitle(status)} -

- -
- Error {status} -
- -

- {getErrorDescription(status)} -

- - {#if error?.message && !checkingGermanRecipe} -
- {error.message} -
- {/if} - - {#if checkingGermanRecipe} -
- Checking for German version... -
- {/if} - -
- {#if status === 401} - - - {:else if status === 404 && recipeLang === 'recipes' && germanRecipeExists && !checkingGermanRecipe} - - - {#if user} - - {/if} - - {:else if status === 404} - - - {:else if status === 403} - - - {:else if status === 500} - - - {:else} - - + + {#snippet actions()} + {#if status === 401} + + {isEnglish ? 'Recipes' : 'Rezepte'} + {:else if showGermanFallback} + + {#if user} + {/if} -
-
-
- - + Recipes + {:else if status === 500} + + {isEnglish ? 'Recipes' : 'Rezepte'} + {:else} + {isEnglish ? 'Recipes' : 'Rezepte'} + + {/if} + {/snippet} + diff --git a/src/routes/fitness/+error.svelte b/src/routes/fitness/+error.svelte new file mode 100644 index 00000000..89bdb20c --- /dev/null +++ b/src/routes/fitness/+error.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/fitness/[...rest]/+page.ts b/src/routes/fitness/[...rest]/+page.ts new file mode 100644 index 00000000..8ea3fd8e --- /dev/null +++ b/src/routes/fitness/[...rest]/+page.ts @@ -0,0 +1,5 @@ +import { error } from '@sveltejs/kit'; + +export const load = () => { + error(404, 'Not found'); +}; diff --git a/src/routes/tasks/+error.svelte b/src/routes/tasks/+error.svelte new file mode 100644 index 00000000..7049a694 --- /dev/null +++ b/src/routes/tasks/+error.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/tasks/[...rest]/+page.ts b/src/routes/tasks/[...rest]/+page.ts new file mode 100644 index 00000000..8ea3fd8e --- /dev/null +++ b/src/routes/tasks/[...rest]/+page.ts @@ -0,0 +1,5 @@ +import { error } from '@sveltejs/kit'; + +export const load = () => { + error(404, 'Not found'); +};