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",
|
||||
"version": "1.36.3",
|
||||
"version": "1.37.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"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;
|
||||
}
|
||||
+30
-408
@@ -1,431 +1,53 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
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 error = $derived($page.error);
|
||||
let error = $derived($page.error as any);
|
||||
|
||||
// Get session data if available (may not be available in error context)
|
||||
let session = $derived($page.data?.session);
|
||||
let user = $derived(session?.user);
|
||||
|
||||
// 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('/');
|
||||
}
|
||||
let bibleQuote = $derived(error?.bibleQuote);
|
||||
let isEnglish = $derived(error?.lang === 'en');
|
||||
let details = $derived(error?.details);
|
||||
|
||||
function goHome() { goto('/'); }
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
goto('/');
|
||||
}
|
||||
}
|
||||
|
||||
function login() {
|
||||
goto('/login');
|
||||
if (window.history.length > 1) window.history.back();
|
||||
else goto('/');
|
||||
}
|
||||
function login() { goto('/login'); }
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{getErrorTitle(status)} - Alexander's Website</title>
|
||||
<title>{getErrorTitle(status, isEnglish)} — Alexander's Website</title>
|
||||
</svelte:head>
|
||||
|
||||
<Header>
|
||||
{#snippet links()}
|
||||
<ul class="site_header">
|
||||
</ul>
|
||||
<ul class="site_header"></ul>
|
||||
{/snippet}
|
||||
|
||||
<main class="error-page">
|
||||
<div class="error-container">
|
||||
<div class="error-icon">
|
||||
{getErrorIcon(status)}
|
||||
</div>
|
||||
|
||||
<h1 class="error-title">
|
||||
{getErrorTitle(status)}
|
||||
</h1>
|
||||
|
||||
<div class="error-code">
|
||||
{isEnglish ? 'Error' : 'Fehler'} {status}
|
||||
</div>
|
||||
|
||||
<p class="error-description">
|
||||
{getErrorDescription(status)}
|
||||
</p>
|
||||
|
||||
{#if /** @type {any} */ (error)?.details}
|
||||
<div class="error-details">
|
||||
{/** @type {any} */ (error).details}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="error-actions">
|
||||
<ErrorView
|
||||
{status}
|
||||
title={getErrorTitle(status, isEnglish)}
|
||||
description={getErrorDescription(status, isEnglish)}
|
||||
{details}
|
||||
{bibleQuote}
|
||||
{isEnglish}
|
||||
>
|
||||
{#snippet actions()}
|
||||
{#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>
|
||||
<button class="link link-primary" onclick={login}>{pick(errorLabels.login, isEnglish)}</button>
|
||||
<button class="link" onclick={goHome}>{pick(errorLabels.homepage, isEnglish)}</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>
|
||||
<button class="link link-primary" onclick={goBack}>{pick(errorLabels.tryAgain, isEnglish)}</button>
|
||||
<button class="link" onclick={goHome}>{pick(errorLabels.homepage, isEnglish)}</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>
|
||||
<button class="link link-primary" onclick={goHome}>{pick(errorLabels.homepage, isEnglish)}</button>
|
||||
<button class="link" onclick={goBack}>{pick(errorLabels.goBack, isEnglish)}</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>
|
||||
{/snippet}
|
||||
</ErrorView>
|
||||
</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 { 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'); }
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{getErrorTitle(status)} - Alexander's Website</title>
|
||||
<title>{title} — Alexander's Website</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="error-page">
|
||||
<div class="error-container">
|
||||
<div class="error-icon">
|
||||
{getErrorIcon(status)}
|
||||
</div>
|
||||
|
||||
<h1 class="error-title">
|
||||
{getErrorTitle(status)}
|
||||
</h1>
|
||||
|
||||
<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">
|
||||
<ErrorView {status} {title} {description} {details} {bibleQuote} {isEnglish}>
|
||||
{#snippet 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>
|
||||
<button class="link link-primary" onclick={login}>{pick(errorLabels.login, isEnglish)}</button>
|
||||
<a class="link" href={recipesHref}>{isEnglish ? 'Recipes' : 'Rezepte'}</a>
|
||||
{:else if showGermanFallback}
|
||||
<button class="link link-primary" onclick={viewGermanRecipe}>View German recipe</button>
|
||||
{#if user}
|
||||
<button class="btn btn-secondary" onclick={editToTranslate}>
|
||||
Edit to Translate
|
||||
</button>
|
||||
<button class="link" 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>
|
||||
<a class="link" href={recipesHref}>Recipes</a>
|
||||
{: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>
|
||||
<button class="link link-primary" onclick={goBack}>{pick(errorLabels.tryAgain, isEnglish)}</button>
|
||||
<a class="link" href={recipesHref}>{isEnglish ? 'Recipes' : 'Rezepte'}</a>
|
||||
{:else}
|
||||
<button class="btn btn-primary" onclick={goToRecipes}>
|
||||
Go to Recipes
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick={goBack}>
|
||||
Go Back
|
||||
</button>
|
||||
<a class="link link-primary" href={recipesHref}>{isEnglish ? 'Recipes' : 'Rezepte'}</a>
|
||||
<button class="link" onclick={goBack}>{pick(errorLabels.goBack, isEnglish)}</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.error-page {
|
||||
min-height: calc(100vh - 6rem);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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>
|
||||
{/snippet}
|
||||
</ErrorView>
|
||||
|
||||
@@ -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