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.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "1.36.3",
|
"version": "1.37.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet, Component } from 'svelte';
|
||||||
|
import { Lock, Ban, SearchX, TriangleAlert, CircleAlert } from '@lucide/svelte';
|
||||||
|
|
||||||
|
interface BibleQuote {
|
||||||
|
text: string;
|
||||||
|
reference: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
status: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
details?: string;
|
||||||
|
bibleQuote?: BibleQuote | null;
|
||||||
|
isEnglish?: boolean;
|
||||||
|
icon?: Component;
|
||||||
|
actions?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
status,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
details,
|
||||||
|
bibleQuote,
|
||||||
|
isEnglish = true,
|
||||||
|
icon,
|
||||||
|
actions
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
function defaultIcon(status: number): Component {
|
||||||
|
switch (status) {
|
||||||
|
case 401: return Lock;
|
||||||
|
case 403: return Ban;
|
||||||
|
case 404: return SearchX;
|
||||||
|
case 500: return TriangleAlert;
|
||||||
|
default: return CircleAlert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Icon = $derived(icon ?? defaultIcon(status));
|
||||||
|
let openQuote = $derived(isEnglish ? '\u201C' : '\u201E');
|
||||||
|
let closeQuote = $derived(isEnglish ? '\u201D' : '\u201C');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="error-page">
|
||||||
|
<article class="error-article">
|
||||||
|
<header class="eyebrow">
|
||||||
|
<Icon size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
<span class="eyebrow-label">
|
||||||
|
{isEnglish ? 'Error' : 'Fehler'}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="code" aria-hidden="true">{status}</div>
|
||||||
|
|
||||||
|
<h1 class="title">{title}</h1>
|
||||||
|
<p class="description">{description}</p>
|
||||||
|
|
||||||
|
{#if details}
|
||||||
|
<p class="details">{details}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if actions}
|
||||||
|
<nav class="actions">
|
||||||
|
{@render actions()}
|
||||||
|
</nav>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if bibleQuote}
|
||||||
|
<hr class="rule" />
|
||||||
|
<figure class="quote">
|
||||||
|
<blockquote class="quote-text">
|
||||||
|
{openQuote}{bibleQuote.text}{closeQuote}
|
||||||
|
</blockquote>
|
||||||
|
<figcaption class="quote-reference">{bibleQuote.reference}</figcaption>
|
||||||
|
</figure>
|
||||||
|
{/if}
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.error-page {
|
||||||
|
min-height: calc(100vh - 6rem);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: clamp(3rem, 10vh, 8rem) 1.5rem 4rem;
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-article {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
font-size: clamp(7rem, 22vw, 14rem);
|
||||||
|
font-weight: 200;
|
||||||
|
line-height: 0.9;
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
font-variant-numeric: lining-nums tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1.0625rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
max-width: 44ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
margin: 1.25rem 0 0;
|
||||||
|
padding-left: 0.875rem;
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
max-width: 44ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.75rem;
|
||||||
|
margin: 2.25rem 0 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.error-article .link) {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
position: relative;
|
||||||
|
transition: color var(--transition-normal, 200ms ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.error-article .link::after) {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: -2px;
|
||||||
|
height: 1px;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.35;
|
||||||
|
transition: opacity var(--transition-normal, 200ms ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.error-article .link:hover) { color: var(--color-text-primary); }
|
||||||
|
:global(.error-article .link:hover::after) { opacity: 1; }
|
||||||
|
|
||||||
|
:global(.error-article .link-primary) { color: var(--color-primary); }
|
||||||
|
:global(.error-article .link-primary:hover) {
|
||||||
|
color: var(--color-primary-hover, var(--color-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
margin: 4rem 0 2.5rem;
|
||||||
|
width: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-text {
|
||||||
|
font-family: Georgia, "Times New Roman", Cambria, serif;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: clamp(1.25rem, 2.2vw, 1.625rem);
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
text-wrap: balance;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-reference {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.actions { gap: 1.25rem; }
|
||||||
|
.rule { margin: 3rem 0 2rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import ErrorView from './ErrorView.svelte';
|
||||||
|
import { getErrorTitle, getErrorDescription, errorLabels, pick } from '$lib/js/errorStrings';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Destination of the section's primary "home" link, e.g. "/rezepte" */
|
||||||
|
sectionHref: string;
|
||||||
|
/** Label for that link in both languages */
|
||||||
|
sectionLabel: { en: string; de: string };
|
||||||
|
/** Override language detection (defaults to error.lang from handleError) */
|
||||||
|
isEnglish?: boolean;
|
||||||
|
/** Extra action buttons rendered before the defaults */
|
||||||
|
extraActions?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { sectionHref, sectionLabel, isEnglish: isEnglishProp, extraActions }: Props = $props();
|
||||||
|
|
||||||
|
let status = $derived($page.status);
|
||||||
|
let error = $derived($page.error as any);
|
||||||
|
let bibleQuote = $derived(error?.bibleQuote);
|
||||||
|
let detectedEnglish = $derived(error?.lang === 'en');
|
||||||
|
let isEnglish = $derived(isEnglishProp ?? detectedEnglish);
|
||||||
|
let details = $derived(error?.details);
|
||||||
|
|
||||||
|
let label = $derived(pick(sectionLabel, isEnglish));
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (window.history.length > 1) window.history.back();
|
||||||
|
else goto(sectionHref);
|
||||||
|
}
|
||||||
|
function login() { goto('/login'); }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ErrorView
|
||||||
|
{status}
|
||||||
|
title={getErrorTitle(status, isEnglish)}
|
||||||
|
description={getErrorDescription(status, isEnglish)}
|
||||||
|
{details}
|
||||||
|
{bibleQuote}
|
||||||
|
{isEnglish}
|
||||||
|
>
|
||||||
|
{#snippet actions()}
|
||||||
|
{#if extraActions}{@render extraActions()}{/if}
|
||||||
|
|
||||||
|
{#if status === 401}
|
||||||
|
<button class="link link-primary" onclick={login}>{pick(errorLabels.login, isEnglish)}</button>
|
||||||
|
<a class="link" href={sectionHref}>{label}</a>
|
||||||
|
{:else if status === 500}
|
||||||
|
<button class="link link-primary" onclick={goBack}>{pick(errorLabels.tryAgain, isEnglish)}</button>
|
||||||
|
<a class="link" href={sectionHref}>{label}</a>
|
||||||
|
{:else}
|
||||||
|
<a class="link link-primary" href={sectionHref}>{label}</a>
|
||||||
|
<button class="link" onclick={goBack}>{pick(errorLabels.goBack, isEnglish)}</button>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</ErrorView>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+33
-411
@@ -1,431 +1,53 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import Header from '$lib/components/Header.svelte';
|
import Header from '$lib/components/Header.svelte';
|
||||||
|
import ErrorView from '$lib/components/ErrorView.svelte';
|
||||||
|
import { getErrorTitle, getErrorDescription, errorLabels, pick } from '$lib/js/errorStrings';
|
||||||
|
|
||||||
let status = $derived($page.status);
|
let status = $derived($page.status);
|
||||||
let error = $derived($page.error);
|
let error = $derived($page.error as any);
|
||||||
|
|
||||||
// Get session data if available (may not be available in error context)
|
let bibleQuote = $derived(error?.bibleQuote);
|
||||||
let session = $derived($page.data?.session);
|
let isEnglish = $derived(error?.lang === 'en');
|
||||||
let user = $derived(session?.user);
|
let details = $derived(error?.details);
|
||||||
|
|
||||||
// Get Bible quote and language from SSR via handleError hook
|
|
||||||
let bibleQuote = $derived(/** @type {any} */ ($page.error)?.bibleQuote);
|
|
||||||
let isEnglish = $derived(/** @type {any} */ ($page.error)?.lang === 'en');
|
|
||||||
|
|
||||||
/** @param {number} status */
|
|
||||||
function getErrorTitle(status) {
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number} status */
|
|
||||||
function getErrorDescription(status) {
|
|
||||||
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.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number} status */
|
|
||||||
function getErrorIcon(status) {
|
|
||||||
switch (status) {
|
|
||||||
case 401:
|
|
||||||
return '🔐';
|
|
||||||
case 403:
|
|
||||||
return '🚫';
|
|
||||||
case 404:
|
|
||||||
return '🔍';
|
|
||||||
case 500:
|
|
||||||
return '⚠️';
|
|
||||||
default:
|
|
||||||
return '❌';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goHome() {
|
|
||||||
goto('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
function goHome() { goto('/'); }
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) window.history.back();
|
||||||
window.history.back();
|
else goto('/');
|
||||||
} else {
|
|
||||||
goto('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function login() {
|
|
||||||
goto('/login');
|
|
||||||
}
|
}
|
||||||
|
function login() { goto('/login'); }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{getErrorTitle(status)} - Alexander's Website</title>
|
<title>{getErrorTitle(status, isEnglish)} — Alexander's Website</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Header>
|
<Header>
|
||||||
{#snippet links()}
|
{#snippet links()}
|
||||||
<ul class="site_header">
|
<ul class="site_header"></ul>
|
||||||
</ul>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<main class="error-page">
|
<ErrorView
|
||||||
<div class="error-container">
|
{status}
|
||||||
<div class="error-icon">
|
title={getErrorTitle(status, isEnglish)}
|
||||||
{getErrorIcon(status)}
|
description={getErrorDescription(status, isEnglish)}
|
||||||
</div>
|
{details}
|
||||||
|
{bibleQuote}
|
||||||
<h1 class="error-title">
|
{isEnglish}
|
||||||
{getErrorTitle(status)}
|
>
|
||||||
</h1>
|
{#snippet actions()}
|
||||||
|
{#if status === 401}
|
||||||
<div class="error-code">
|
<button class="link link-primary" onclick={login}>{pick(errorLabels.login, isEnglish)}</button>
|
||||||
{isEnglish ? 'Error' : 'Fehler'} {status}
|
<button class="link" onclick={goHome}>{pick(errorLabels.homepage, isEnglish)}</button>
|
||||||
</div>
|
{:else if status === 500}
|
||||||
|
<button class="link link-primary" onclick={goBack}>{pick(errorLabels.tryAgain, isEnglish)}</button>
|
||||||
<p class="error-description">
|
<button class="link" onclick={goHome}>{pick(errorLabels.homepage, isEnglish)}</button>
|
||||||
{getErrorDescription(status)}
|
{:else}
|
||||||
</p>
|
<button class="link link-primary" onclick={goHome}>{pick(errorLabels.homepage, isEnglish)}</button>
|
||||||
|
<button class="link" onclick={goBack}>{pick(errorLabels.goBack, isEnglish)}</button>
|
||||||
{#if /** @type {any} */ (error)?.details}
|
|
||||||
<div class="error-details">
|
|
||||||
{/** @type {any} */ (error).details}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{/snippet}
|
||||||
<div class="error-actions">
|
</ErrorView>
|
||||||
{#if status === 401}
|
|
||||||
<button class="btn btn-primary" onclick={login}>
|
|
||||||
{isEnglish ? 'Log In' : 'Anmelden'}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick={goHome}>
|
|
||||||
{isEnglish ? 'Go to Homepage' : 'Zur Startseite'}
|
|
||||||
</button>
|
|
||||||
{:else if status === 500}
|
|
||||||
<button class="btn btn-primary" onclick={goHome}>
|
|
||||||
{isEnglish ? 'Go to Homepage' : 'Zur Startseite'}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick={goBack}>
|
|
||||||
{isEnglish ? 'Try Again' : 'Erneut versuchen'}
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button class="btn btn-primary" onclick={goHome}>
|
|
||||||
{isEnglish ? 'Go to Homepage' : 'Zur Startseite'}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick={goBack}>
|
|
||||||
{isEnglish ? 'Go Back' : 'Zurück'}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bible Quote Section -->
|
|
||||||
{#if bibleQuote}
|
|
||||||
<div class="bible-quote">
|
|
||||||
<div class="quote-text">
|
|
||||||
{isEnglish ? '"' : '„'}{bibleQuote.text}{isEnglish ? '"' : '"'}
|
|
||||||
</div>
|
|
||||||
<div class="quote-reference">
|
|
||||||
— {bibleQuote.reference}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<style>
|
|
||||||
.error-page {
|
|
||||||
min-height: calc(100vh - 6rem);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: #fbf9f3;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-page {
|
|
||||||
background: var(--background-dark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-page {
|
|
||||||
background: var(--background-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-container {
|
|
||||||
background: var(--nord5);
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding: 3rem;
|
|
||||||
max-width: 600px;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
||||||
border: 1px solid var(--nord4);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-container {
|
|
||||||
background: var(--nord1);
|
|
||||||
border-color: var(--nord2);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-container {
|
|
||||||
background: var(--nord1);
|
|
||||||
border-color: var(--nord2);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-icon {
|
|
||||||
font-size: 4rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
color: var(--nord0);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-title {
|
|
||||||
color: var(--nord6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-title {
|
|
||||||
color: var(--nord6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-code {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--nord3);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-code {
|
|
||||||
color: var(--nord4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-code {
|
|
||||||
color: var(--nord4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-description {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--nord2);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-description {
|
|
||||||
color: var(--nord5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-description {
|
|
||||||
color: var(--nord5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-details {
|
|
||||||
background: var(--nord4);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--nord0);
|
|
||||||
border-left: 4px solid var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-details {
|
|
||||||
background: var(--nord2);
|
|
||||||
color: var(--nord6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-details {
|
|
||||||
background: var(--nord2);
|
|
||||||
color: var(--nord6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 2rem 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
font-size: 1rem;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, var(--blue), var(--lightblue));
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: linear-gradient(135deg, var(--lightblue), var(--blue));
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(94, 129, 172, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: var(--nord4);
|
|
||||||
color: var(--nord0);
|
|
||||||
border: 1px solid var(--nord3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: var(--nord3);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .btn-secondary {
|
|
||||||
background: var(--nord2);
|
|
||||||
color: var(--nord6);
|
|
||||||
border-color: var(--nord3);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(:root:not([data-theme="light"])) .btn-secondary:hover {
|
|
||||||
background: var(--nord3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .btn-secondary {
|
|
||||||
background: var(--nord2);
|
|
||||||
color: var(--nord6);
|
|
||||||
border-color: var(--nord3);
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .btn-secondary:hover {
|
|
||||||
background: var(--nord3);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.bible-quote {
|
|
||||||
margin: 2.5rem 0;
|
|
||||||
padding: 2rem;
|
|
||||||
background: linear-gradient(135deg, var(--nord5), var(--nord4));
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
border-left: 4px solid var(--blue);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .bible-quote {
|
|
||||||
background: linear-gradient(135deg, var(--nord2), var(--nord3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .bible-quote {
|
|
||||||
background: linear-gradient(135deg, var(--nord2), var(--nord3));
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-text {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--nord0);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .quote-text {
|
|
||||||
color: var(--nord6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .quote-text {
|
|
||||||
color: var(--nord6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-reference {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--nord2);
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: right;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .quote-reference {
|
|
||||||
color: var(--nord4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .quote-reference {
|
|
||||||
color: var(--nord4);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.error-container {
|
|
||||||
padding: 2rem;
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.bible-quote {
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-text {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import SectionError from '$lib/components/SectionError.svelte';
|
||||||
|
import { detectCospendLang, cospendRoot } from '$lib/js/cospendI18n';
|
||||||
|
|
||||||
|
let lang = $derived(detectCospendLang($page.url.pathname));
|
||||||
|
let isEnglish = $derived(lang === 'en');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SectionError
|
||||||
|
sectionHref={cospendRoot(lang)}
|
||||||
|
sectionLabel={{ en: 'Expenses', de: 'Kosten' }}
|
||||||
|
{isEnglish}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load = () => {
|
||||||
|
error(404, 'Not found');
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import SectionError from '$lib/components/SectionError.svelte';
|
||||||
|
|
||||||
|
let faithLang = $derived($page.params.faithLang);
|
||||||
|
let isEnglish = $derived(faithLang === 'faith');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SectionError
|
||||||
|
sectionHref="/{faithLang}"
|
||||||
|
sectionLabel={{ en: 'Faith', de: 'Glaube' }}
|
||||||
|
{isEnglish}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load = () => {
|
||||||
|
error(404, 'Not found');
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import SectionError from '$lib/components/SectionError.svelte';
|
||||||
|
|
||||||
|
let recipeLang = $derived($page.params.recipeLang);
|
||||||
|
let isEnglish = $derived(recipeLang === 'recipes');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SectionError
|
||||||
|
sectionHref="/{recipeLang}"
|
||||||
|
sectionLabel={{ en: 'Recipes', de: 'Rezepte' }}
|
||||||
|
{isEnglish}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load = () => {
|
||||||
|
error(404, 'Not found');
|
||||||
|
};
|
||||||
@@ -2,28 +2,29 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
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 status = $derived($page.status);
|
||||||
let error = $derived($page.error);
|
let error = $derived($page.error as any);
|
||||||
let recipeLang = $derived($page.params.recipeLang);
|
let recipeLang = $derived($page.params.recipeLang);
|
||||||
let recipeName = $derived($page.params.name);
|
let recipeName = $derived($page.params.name);
|
||||||
let session = $derived($page.data?.session);
|
let user = $derived($page.data?.session?.user);
|
||||||
let user = $derived(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 germanRecipeExists = $state(false);
|
||||||
let checkingGermanRecipe = $state(false);
|
let checkingGermanRecipe = $state(false);
|
||||||
|
|
||||||
// Check if German recipe exists when on English route with 404
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const isEnglishRoute = recipeLang === 'recipes';
|
|
||||||
if (isEnglishRoute && status === 404) {
|
if (isEnglishRoute && status === 404) {
|
||||||
checkingGermanRecipe = true;
|
checkingGermanRecipe = true;
|
||||||
try {
|
try {
|
||||||
// Check if German recipe exists
|
|
||||||
const response = await fetch(`/api/rezepte/items/${recipeName}`);
|
const response = await fetch(`/api/rezepte/items/${recipeName}`);
|
||||||
germanRecipeExists = response.ok;
|
germanRecipeExists = response.ok;
|
||||||
} catch (e) {
|
} catch {
|
||||||
germanRecipeExists = false;
|
germanRecipeExists = false;
|
||||||
} finally {
|
} finally {
|
||||||
checkingGermanRecipe = false;
|
checkingGermanRecipe = false;
|
||||||
@@ -31,384 +32,61 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function getErrorTitle(status: number) {
|
let showGermanFallback = $derived(
|
||||||
switch (status) {
|
status === 404 && isEnglishRoute && germanRecipeExists && !checkingGermanRecipe
|
||||||
case 401:
|
);
|
||||||
return 'Authentication Required';
|
|
||||||
case 403:
|
|
||||||
return 'Access Denied';
|
|
||||||
case 404:
|
|
||||||
return 'Recipe Not Found';
|
|
||||||
case 500:
|
|
||||||
return 'Server Error';
|
|
||||||
default:
|
|
||||||
return 'Error';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getErrorDescription(status: number) {
|
let title = $derived(
|
||||||
const isEnglishRoute = recipeLang === 'recipes';
|
status === 404 ? (isEnglish ? 'Recipe Not Found' : 'Rezept nicht gefunden')
|
||||||
|
: getErrorTitle(status, isEnglish)
|
||||||
|
);
|
||||||
|
|
||||||
switch (status) {
|
let description = $derived(
|
||||||
case 401:
|
showGermanFallback
|
||||||
return 'You must be logged in to access this page.';
|
? 'This recipe has not been translated to English yet, but the German version is available.'
|
||||||
case 403:
|
: status === 404
|
||||||
return 'You do not have permission to access this area.';
|
? (isEnglish ? 'The requested recipe could not be found.' : 'Das angeforderte Rezept konnte nicht gefunden werden.')
|
||||||
case 404:
|
: getErrorDescription(status, isEnglish)
|
||||||
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.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getErrorIcon(status: number) {
|
let details = $derived(
|
||||||
switch (status) {
|
checkingGermanRecipe
|
||||||
case 401:
|
? (isEnglish ? 'Checking for German version…' : 'Suche nach deutscher Version…')
|
||||||
return '🔐';
|
: error?.details
|
||||||
case 403:
|
);
|
||||||
return '🚫';
|
|
||||||
case 404:
|
|
||||||
return '🔍';
|
|
||||||
case 500:
|
|
||||||
return '⚠️';
|
|
||||||
default:
|
|
||||||
return '❌';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function viewGermanRecipe() {
|
let recipesHref = $derived(isEnglishRoute ? '/recipes' : '/rezepte');
|
||||||
goto(`/rezepte/${recipeName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function editToTranslate() {
|
|
||||||
goto(`/rezepte/edit/${recipeName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToRecipes() {
|
|
||||||
const lang = recipeLang === 'recipes' ? 'recipes' : 'rezepte';
|
|
||||||
goto(`/${lang}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
function viewGermanRecipe() { goto(`/rezepte/${recipeName}`); }
|
||||||
|
function editToTranslate() { goto(`/rezepte/edit/${recipeName}`); }
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) window.history.back();
|
||||||
window.history.back();
|
else goto(recipesHref);
|
||||||
} else {
|
|
||||||
goToRecipes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function login() {
|
|
||||||
goto('/login');
|
|
||||||
}
|
}
|
||||||
|
function login() { goto('/login'); }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{getErrorTitle(status)} - Alexander's Website</title>
|
<title>{title} — Alexander's Website</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main class="error-page">
|
<ErrorView {status} {title} {description} {details} {bibleQuote} {isEnglish}>
|
||||||
<div class="error-container">
|
{#snippet actions()}
|
||||||
<div class="error-icon">
|
{#if status === 401}
|
||||||
{getErrorIcon(status)}
|
<button class="link link-primary" onclick={login}>{pick(errorLabels.login, isEnglish)}</button>
|
||||||
</div>
|
<a class="link" href={recipesHref}>{isEnglish ? 'Recipes' : 'Rezepte'}</a>
|
||||||
|
{:else if showGermanFallback}
|
||||||
<h1 class="error-title">
|
<button class="link link-primary" onclick={viewGermanRecipe}>View German recipe</button>
|
||||||
{getErrorTitle(status)}
|
{#if user}
|
||||||
</h1>
|
<button class="link" onclick={editToTranslate}>Edit to translate</button>
|
||||||
|
|
||||||
<div class="error-code">
|
|
||||||
Error {status}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="error-description">
|
|
||||||
{getErrorDescription(status)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{#if error?.message && !checkingGermanRecipe}
|
|
||||||
<div class="error-details">
|
|
||||||
{error.message}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if checkingGermanRecipe}
|
|
||||||
<div class="checking-message">
|
|
||||||
Checking for German version...
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="error-actions">
|
|
||||||
{#if status === 401}
|
|
||||||
<button class="btn btn-primary" onclick={login}>
|
|
||||||
Log In
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick={goToRecipes}>
|
|
||||||
Go to Recipes
|
|
||||||
</button>
|
|
||||||
{:else if status === 404 && recipeLang === 'recipes' && germanRecipeExists && !checkingGermanRecipe}
|
|
||||||
<!-- Special case: English recipe not found but German exists -->
|
|
||||||
<button class="btn btn-primary" onclick={viewGermanRecipe}>
|
|
||||||
View German Recipe
|
|
||||||
</button>
|
|
||||||
{#if user}
|
|
||||||
<button class="btn btn-secondary" onclick={editToTranslate}>
|
|
||||||
Edit to Translate
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<button class="btn btn-secondary" onclick={goToRecipes}>
|
|
||||||
Go to Recipes
|
|
||||||
</button>
|
|
||||||
{:else if status === 404}
|
|
||||||
<button class="btn btn-primary" onclick={goToRecipes}>
|
|
||||||
Go to Recipes
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick={goBack}>
|
|
||||||
Go Back
|
|
||||||
</button>
|
|
||||||
{:else if status === 403}
|
|
||||||
<button class="btn btn-primary" onclick={goToRecipes}>
|
|
||||||
Go to Recipes
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick={goBack}>
|
|
||||||
Go Back
|
|
||||||
</button>
|
|
||||||
{:else if status === 500}
|
|
||||||
<button class="btn btn-primary" onclick={goToRecipes}>
|
|
||||||
Go to Recipes
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick={goBack}>
|
|
||||||
Try Again
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button class="btn btn-primary" onclick={goToRecipes}>
|
|
||||||
Go to Recipes
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick={goBack}>
|
|
||||||
Go Back
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
<a class="link" href={recipesHref}>Recipes</a>
|
||||||
</div>
|
{:else if status === 500}
|
||||||
</main>
|
<button class="link link-primary" onclick={goBack}>{pick(errorLabels.tryAgain, isEnglish)}</button>
|
||||||
|
<a class="link" href={recipesHref}>{isEnglish ? 'Recipes' : 'Rezepte'}</a>
|
||||||
<style>
|
{:else}
|
||||||
.error-page {
|
<a class="link link-primary" href={recipesHref}>{isEnglish ? 'Recipes' : 'Rezepte'}</a>
|
||||||
min-height: calc(100vh - 6rem);
|
<button class="link" onclick={goBack}>{pick(errorLabels.goBack, isEnglish)}</button>
|
||||||
display: flex;
|
{/if}
|
||||||
align-items: center;
|
{/snippet}
|
||||||
justify-content: center;
|
</ErrorView>
|
||||||
background: var(--color-bg-primary);
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-container {
|
|
||||||
background: var(--nord5);
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding: 3rem;
|
|
||||||
max-width: 600px;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
||||||
border: 1px solid var(--nord4);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-container {
|
|
||||||
background: var(--nord1);
|
|
||||||
border-color: var(--nord2);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-container {
|
|
||||||
background: var(--nord1);
|
|
||||||
border-color: var(--nord2);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-icon {
|
|
||||||
font-size: 4rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
color: var(--nord0);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-title {
|
|
||||||
color: var(--nord6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-title {
|
|
||||||
color: var(--nord6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-code {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--nord3);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-code {
|
|
||||||
color: var(--nord4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-code {
|
|
||||||
color: var(--nord4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-description {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--nord2);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-description {
|
|
||||||
color: var(--nord5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-description {
|
|
||||||
color: var(--nord5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-details {
|
|
||||||
background: var(--nord4);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--nord0);
|
|
||||||
border-left: 4px solid var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .error-details {
|
|
||||||
background: var(--nord2);
|
|
||||||
color: var(--nord6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .error-details {
|
|
||||||
background: var(--nord2);
|
|
||||||
color: var(--nord6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-message {
|
|
||||||
background: var(--nord4);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--nord2);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .checking-message {
|
|
||||||
background: var(--nord2);
|
|
||||||
color: var(--nord4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .checking-message {
|
|
||||||
background: var(--nord2);
|
|
||||||
color: var(--nord4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 2rem 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
font-size: 1rem;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, var(--blue), var(--lightblue));
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: linear-gradient(135deg, var(--lightblue), var(--blue));
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(94, 129, 172, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: var(--nord4);
|
|
||||||
color: var(--nord0);
|
|
||||||
border: 1px solid var(--nord3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: var(--nord3);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:global(:root:not([data-theme="light"])) .btn-secondary {
|
|
||||||
background: var(--nord2);
|
|
||||||
color: var(--nord6);
|
|
||||||
border-color: var(--nord3);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(:root:not([data-theme="light"])) .btn-secondary:hover {
|
|
||||||
background: var(--nord3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .btn-secondary {
|
|
||||||
background: var(--nord2);
|
|
||||||
color: var(--nord6);
|
|
||||||
border-color: var(--nord3);
|
|
||||||
}
|
|
||||||
:global(:root[data-theme="dark"]) .btn-secondary:hover {
|
|
||||||
background: var(--nord3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.error-container {
|
|
||||||
padding: 2rem;
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 250px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import SectionError from '$lib/components/SectionError.svelte';
|
||||||
|
import { detectFitnessLang } from '$lib/js/fitnessI18n';
|
||||||
|
|
||||||
|
let lang = $derived(detectFitnessLang($page.url.pathname));
|
||||||
|
let isEnglish = $derived(lang === 'en');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SectionError
|
||||||
|
sectionHref={isEnglish ? '/fitness/workout' : '/fitness/training'}
|
||||||
|
sectionLabel={{ en: 'Fitness', de: 'Fitness' }}
|
||||||
|
{isEnglish}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load = () => {
|
||||||
|
error(404, 'Not found');
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SectionError from '$lib/components/SectionError.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SectionError
|
||||||
|
sectionHref="/tasks"
|
||||||
|
sectionLabel={{ en: 'Tasks', de: 'Aufgaben' }}
|
||||||
|
isEnglish={false}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load = () => {
|
||||||
|
error(404, 'Not found');
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user