refactor: migrate hrefs to resolve()/asset() from $app/paths

Replace string-literal and template-literal hrefs across the codebase
with the modern SvelteKit 2.26+ resolve() and asset() APIs. Migration
makes route IDs explicit, type-checked against generated $app/types,
and base-path-aware. Two codemod scripts handle the bulk; remaining
ambiguous, query-bearing, and precomputed-href cases are converted
manually at the assignment sites.
This commit is contained in:
2026-04-29 22:14:29 +02:00
parent 70506e169a
commit e5d218820b
64 changed files with 669 additions and 161 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.50.0", "version": "1.51.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+268
View File
@@ -0,0 +1,268 @@
/**
* Bucket 2 codemod: replace template-literal hrefs that start with `/` and
* contain `{expr}` interpolations with `resolve(routeId, { ... })`.
*
* Skips:
* - tags: <link>, <image> (svg), <use>, <textPath>
* - hrefs not starting with `/`
* - hrefs containing `?` or `#` (query/fragment) — handle manually
* - mixed segments like `view-{id}`
* - paths matching 0 or >1 routes
*
* Run: pnpm exec vite-node scripts/codemod-href-resolve-bucket2.ts [--dry] [--verbose]
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import { join, extname } from 'node:path';
const SRC = 'src';
const ROUTES = 'src/routes';
const DRY = process.argv.includes('--dry');
const SKIP_TAGS = new Set(['link', 'image', 'use', 'textpath']);
// --- Route tree ---------------------------------------------------------
type Dir = { name: string; subdirs: Dir[] };
function loadTree(dir: string, name = ''): Dir {
const subdirs: Dir[] = [];
for (const e of readdirSync(dir, { withFileTypes: true })) {
if (!e.isDirectory()) continue;
if (e.name === 'api' || e.name.startsWith('.')) continue;
subdirs.push(loadTree(join(dir, e.name), e.name));
}
return { name, subdirs };
}
const ROUTE_TREE = loadTree(ROUTES);
// --- Path parsing -------------------------------------------------------
type HrefSeg = { kind: 'literal'; text: string } | { kind: 'param'; expr: string };
function hasUnbracedChar(path: string, chars: string): boolean {
let depth = 0;
for (const c of path) {
if (c === '{') depth++;
else if (c === '}') depth--;
else if (depth === 0 && chars.includes(c)) return true;
}
return false;
}
function parsePath(path: string): HrefSeg[] | null {
if (!path.startsWith('/')) return null;
if (hasUnbracedChar(path, '?#')) return null;
if (path.includes('//')) return null;
// Split on `/`, but only outside of {...}
const parts: string[] = [];
let buf = '';
let depth = 0;
for (const c of path.slice(1)) {
if (c === '{') { depth++; buf += c; }
else if (c === '}') { depth--; buf += c; }
else if (c === '/' && depth === 0) { parts.push(buf); buf = ''; }
else buf += c;
}
parts.push(buf);
if (parts.length === 1 && parts[0] === '') return [];
const segs: HrefSeg[] = [];
for (const p of parts) {
if (p === '') return null;
const m = p.match(/^\{([^}]+)\}$/);
if (m) {
segs.push({ kind: 'param', expr: m[1] });
} else if (!p.includes('{') && !p.includes('}')) {
segs.push({ kind: 'literal', text: p });
} else {
return null; // mixed segment
}
}
return segs;
}
function paramInfo(
name: string
): { paramName: string; isRest: boolean } | null {
let body = name;
if (body.startsWith('[[') && body.endsWith(']]')) {
body = body.slice(2, -2);
} else if (body.startsWith('[') && body.endsWith(']')) {
body = body.slice(1, -1);
} else return null;
const isRest = body.startsWith('...');
if (isRest) body = body.slice(3);
const eq = body.indexOf('=');
const paramName = eq >= 0 ? body.slice(0, eq) : body;
return { paramName, isRest };
}
// --- Tree matching ------------------------------------------------------
type Match = { routeId: string; params: Array<[string, string]> };
function matchTree(
dir: Dir,
segs: HrefSeg[],
routePath: string[],
params: Array<[string, string]>
): Match[] {
if (segs.length === 0) {
const id = routePath.length === 0 ? '/' : '/' + routePath.join('/');
return [{ routeId: id, params }];
}
const [seg, ...rest] = segs;
const out: Match[] = [];
for (const sub of dir.subdirs) {
// Route groups are transparent — they don't consume a URL segment
// but DO appear in the route ID.
if (sub.name.startsWith('(') && sub.name.endsWith(')')) {
out.push(...matchTree(sub, segs, [...routePath, sub.name], params));
continue;
}
if (seg.kind === 'literal') {
if (sub.name === seg.text) {
out.push(
...matchTree(sub, rest, [...routePath, sub.name], params)
);
}
} else {
const info = paramInfo(sub.name);
if (info && !info.isRest) {
out.push(
...matchTree(sub, rest, [...routePath, sub.name], [
...params,
[info.paramName, seg.expr]
])
);
}
}
}
return out;
}
// --- Output formatting --------------------------------------------------
function isIdentifier(s: string): boolean {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
}
function formatParams(params: Array<[string, string]>): string {
if (params.length === 0) return '';
const items = params.map(([name, expr]) => {
const trimmed = expr.trim();
if (isIdentifier(trimmed) && trimmed === name) return name;
return `${name}: ${trimmed}`;
});
return `, { ${items.join(', ')} }`;
}
// --- Rewrite ------------------------------------------------------------
const HREF_RE =
/(<([A-Za-z][\w.-]*)\b[^>]*?\s)href="(\/[^"]*\{[^"]*\}[^"]*)"/gs;
type Skip = { path: string; reason: string };
function rewriteHrefs(src: string): {
code: string;
changed: number;
skipped: Skip[];
} {
let changed = 0;
const skipped: Skip[] = [];
const code = src.replace(HREF_RE, (full, prefix, tag, path) => {
if (SKIP_TAGS.has(tag.toLowerCase())) return full;
const segs = parsePath(path);
if (!segs) {
skipped.push({ path, reason: 'unparsable (mixed/query/fragment)' });
return full;
}
const matches = matchTree(ROUTE_TREE, segs, [], []);
if (matches.length === 0) {
skipped.push({ path, reason: 'no route match' });
return full;
}
if (matches.length > 1) {
skipped.push({
path,
reason: `${matches.length} ambiguous matches: ${matches.map((m) => m.routeId).join(' | ')}`
});
return full;
}
const { routeId, params } = matches[0];
changed++;
return `${prefix}href={resolve('${routeId}'${formatParams(params)})}`;
});
return { code, changed, skipped };
}
// --- Import injection ---------------------------------------------------
const SCRIPT_RE = /<script\b([^>]*)>([\s\S]*?)<\/script>/;
const PATHS_IMPORT_RE =
/import\s*\{([^}]*)\}\s*from\s*['"]\$app\/paths['"]\s*;?/;
function ensureResolveImport(src: string): string {
const m = SCRIPT_RE.exec(src);
if (!m) {
return `<script lang="ts">\n\timport { resolve } from '$app/paths';\n</script>\n\n${src}`;
}
const [scriptFull, attrs, body] = m;
const pm = PATHS_IMPORT_RE.exec(body);
if (pm) {
const inner = pm[1];
if (/\bresolve\b/.test(inner)) return src;
const merged = inner.trim().replace(/,?\s*$/, '') + ', resolve';
const newImport = `import { ${merged} } from '$app/paths';`;
const newBody = body.replace(PATHS_IMPORT_RE, newImport);
return src.replace(scriptFull, `<script${attrs}>${newBody}</script>`);
}
const im = body.match(/^([ \t]*)import\b/m);
const indent = im ? im[1] : '\t';
const opening = `<script${attrs}>`;
return src.replace(
scriptFull,
`${opening}\n${indent}import { resolve } from '$app/paths';${body}</script>`
);
}
// --- Driver -------------------------------------------------------------
function walk(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
const s = statSync(p);
if (s.isDirectory()) walk(p, out);
else if (extname(p) === '.svelte') out.push(p);
}
return out;
}
const files = walk(SRC);
let totalFiles = 0;
let totalReplacements = 0;
const allSkipped: Array<{ file: string } & Skip> = [];
for (const f of files) {
const orig = readFileSync(f, 'utf8');
const { code, changed, skipped } = rewriteHrefs(orig);
for (const s of skipped) allSkipped.push({ file: f, ...s });
if (changed === 0) continue;
const final = ensureResolveImport(code);
if (!DRY) writeFileSync(f, final);
totalFiles++;
totalReplacements += changed;
console.log(`${changed.toString().padStart(3)} ${f}`);
}
console.log(
`\n${DRY ? '[dry] ' : ''}${totalReplacements} replacements across ${totalFiles} files`
);
if (allSkipped.length > 0) {
console.log(`\n--- ${allSkipped.length} skipped hrefs ---`);
for (const s of allSkipped) {
console.log(` ${s.file}\n ${s.path} [${s.reason}]`);
}
}
+105
View File
@@ -0,0 +1,105 @@
/**
* Bucket 1 codemod: replace literal href="/path" with href={resolve('/path')}
* in .svelte files, and inject `import { resolve } from '$app/paths'`.
*
* Skips:
* - non-anchor tags: <link>, <image> (svg), <use>
* - external/protocol URLs: http(s)://, //host, mailto:, tel:
* - fragments (#...) and empty values
* - existing dynamic hrefs ({...})
*
* Run: pnpm exec vite-node scripts/codemod-href-resolve.ts [--dry]
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import { join, extname } from 'node:path';
const ROOT = 'src';
const DRY = process.argv.includes('--dry');
const SKIP_TAGS = new Set(['link', 'image', 'use']);
function walk(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
const s = statSync(p);
if (s.isDirectory()) walk(p, out);
else if (extname(p) === '.svelte') out.push(p);
}
return out;
}
/**
* Match: opening of element, then its attributes, then href="/...".
* Group 1 = full prefix incl. tag-name, Group 2 = tag name, Group 3 = path.
*/
// Excludes `{` and `}` so Svelte template interpolations inside the
// attribute value (e.g. href="/{lang}/foo") are NOT treated as literals.
const HREF_RE =
/(<([A-Za-z][\w.-]*)\b[^>]*?\s)href="(\/[^"{}]*)"/gs;
function rewriteHrefs(src: string): { code: string; changed: number } {
let changed = 0;
const code = src.replace(HREF_RE, (full, prefix, tag, path) => {
if (SKIP_TAGS.has(tag.toLowerCase())) return full;
// Skip protocol-relative just in case
if (path.startsWith('//')) return full;
changed++;
return `${prefix}href={resolve('${path}')}`;
});
return { code, changed };
}
const SCRIPT_RE = /<script\b([^>]*)>([\s\S]*?)<\/script>/;
const PATHS_IMPORT_RE =
/import\s*\{([^}]*)\}\s*from\s*['"]\$app\/paths['"]\s*;?/;
function ensureResolveImport(src: string): string {
const scriptMatch = SCRIPT_RE.exec(src);
if (!scriptMatch) {
// No script tag — prepend a TS one.
return `<script lang="ts">\n\timport { resolve } from '$app/paths';\n</script>\n\n${src}`;
}
const [scriptFull, attrs, body] = scriptMatch;
const pathsMatch = PATHS_IMPORT_RE.exec(body);
if (pathsMatch) {
const inner = pathsMatch[1];
if (/\bresolve\b/.test(inner)) return src; // already imported
const merged = inner.trim().replace(/,?\s*$/, '') + ', resolve';
const newImport = `import { ${merged} } from '$app/paths';`;
const newBody = body.replace(PATHS_IMPORT_RE, newImport);
return src.replace(scriptFull, `<script${attrs}>${newBody}</script>`);
}
// Inject new import line. Detect indent from first import line if present.
const importMatch = body.match(/^([ \t]*)import\b/m);
const indent = importMatch ? importMatch[1] : '\t';
// Insert right after the opening script tag's newline.
const opening = `<script${attrs}>`;
const insertion = `\n${indent}import { resolve } from '$app/paths';`;
const newScript = opening + insertion + body + '</script>';
return src.replace(scriptFull, newScript);
}
function processFile(path: string): { changed: number } {
const orig = readFileSync(path, 'utf8');
const { code: rewritten, changed } = rewriteHrefs(orig);
if (changed === 0) return { changed: 0 };
const final = ensureResolveImport(rewritten);
if (!DRY) writeFileSync(path, final);
return { changed };
}
const files = walk(ROOT);
let totalFiles = 0;
let totalReplacements = 0;
for (const f of files) {
const { changed } = processFile(f);
if (changed > 0) {
totalFiles++;
totalReplacements += changed;
console.log(`${changed.toString().padStart(3)} ${f}`);
}
}
console.log(
`\n${DRY ? '[dry] ' : ''}${totalReplacements} replacements across ${totalFiles} files`
);
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import Symbol from "./Symbol.svelte" import Symbol from "./Symbol.svelte"
import ThemeToggle from "./ThemeToggle.svelte" import ThemeToggle from "./ThemeToggle.svelte"
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
@@ -329,7 +330,7 @@ nav {
<div> <div>
<nav class:no-links={!links}> <nav class:no-links={!links}>
<a href="/" aria-label="Home" class="home-link" class:full={fullSymbol}><Symbol /></a> <a href={resolve('/')} aria-label="Home" class="home-link" class:full={fullSymbol}><Symbol /></a>
{#if links} {#if links}
<div class="links-wrapper"> <div class="links-wrapper">
{@render links()} {@render links()}
+4 -3
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import { onMount } from "svelte"; import { onMount } from "svelte";
import { page } from '$app/stores'; import { page } from '$app/stores';
import LogIn from '@lucide/svelte/icons/log-in'; import LogIn from '@lucide/svelte/icons/log-in';
@@ -153,10 +154,10 @@
<p>({user.nickname})</p> <p>({user.nickname})</p>
<ul> <ul>
{#if user.groups?.includes('rezepte_users')} {#if user.groups?.includes('rezepte_users')}
<li><a href="/{recipeLang}/administration">Administration</a></li> <li><a href={resolve('/[recipeLang=recipeLang]/administration', { recipeLang })}>Administration</a></li>
{/if} {/if}
<li><a href="https://sso.bocken.org/if/user/#/settings" >Einstellungen</a></li> <li><a href="https://sso.bocken.org/if/user/#/settings" >Einstellungen</a></li>
<li><a href="/logout?callbackUrl={encodeURIComponent(getLogoutCallbackUrl($page.url.pathname))}">Log Out</a></li> <li><a href={`${resolve('/logout')}?callbackUrl=${encodeURIComponent(getLogoutCallbackUrl($page.url.pathname))}`}>Log Out</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -164,7 +165,7 @@
{:else} {:else}
<a <a
class="entry login-link" class="entry login-link"
href="/login?callbackUrl={encodeURIComponent($page.url.pathname + $page.url.search)}" href={`${resolve('/login')}?callbackUrl=${encodeURIComponent($page.url.pathname + $page.url.search)}`}
aria-label={lang === 'de' ? 'Anmelden' : 'Login'} aria-label={lang === 'de' ? 'Anmelden' : 'Login'}
title={lang === 'de' ? 'Anmelden' : 'Login'} title={lang === 'de' ? 'Anmelden' : 'Login'}
> >
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
@@ -243,7 +244,7 @@
</div> </div>
{#if payment} {#if payment}
<EditButton href="/{root}/payments/edit/{paymentId}" /> <EditButton href={resolve('/[cospendRoot=cospendRoot]/payments/edit/[id]', { cospendRoot: root, id: paymentId })} />
{/if} {/if}
<style> <style>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import Shield from '@lucide/svelte/icons/shield'; import Shield from '@lucide/svelte/icons/shield';
import Flame from '@lucide/svelte/icons/flame'; import Flame from '@lucide/svelte/icons/flame';
@@ -18,7 +19,7 @@
<a <a
class="case-tab" class="case-tab"
class:active={active === 'contra'} class:active={active === 'contra'}
href="/{faithLang}/{slug}/contra" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/contra', { faithLang, apologetikSlug: slug })}
> >
<Shield class="ct-glyph" size={14} strokeWidth={2} aria-hidden="true" /> <Shield class="ct-glyph" size={14} strokeWidth={2} aria-hidden="true" />
<span>{l.contra}</span> <span>{l.contra}</span>
@@ -26,7 +27,7 @@
<a <a
class="case-tab" class="case-tab"
class:active={active === 'pro'} class:active={active === 'pro'}
href="/{faithLang}/{slug}/pro" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/pro', { faithLang, apologetikSlug: slug })}
> >
<Flame class="ct-glyph" size={14} strokeWidth={2} aria-hidden="true" /> <Flame class="ct-glyph" size={14} strokeWidth={2} aria-hidden="true" />
<span>{l.pro}</span> <span>{l.pro}</span>
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { getEnrichedExerciseById } from '$lib/data/exercisedb'; import { getEnrichedExerciseById } from '$lib/data/exercisedb';
import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n';
@@ -14,7 +15,7 @@
{#if plain} {#if plain}
<span class="exercise-plain">{exercise.localName}</span> <span class="exercise-plain">{exercise.localName}</span>
{:else} {:else}
<a href="/fitness/{sl.exercises}/{exerciseId}" class="exercise-link">{exercise.localName}</a> <a href={resolve('/fitness/[exercises=fitnessExercises]/[id]', { exercises: sl.exercises, id: exerciseId })} class="exercise-link">{exercise.localName}</a>
{/if} {/if}
{:else} {:else}
<span class="exercise-unknown">Unknown Exercise</span> <span class="exercise-unknown">Unknown Exercise</span>
+2 -1
View File
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
@@ -477,7 +478,7 @@
<span class="fs-result-cal">{item.calories}<small> kcal</small></span> <span class="fs-result-cal">{item.calories}<small> kcal</small></span>
</button> </button>
{#if showDetailLinks && (item.source === 'bls' || item.source === 'usda' || item.source === 'off')} {#if showDetailLinks && (item.source === 'bls' || item.source === 'usda' || item.source === 'off')}
<a class="fs-detail-link" href="/fitness/{s.nutrition}/food/{item.source}/{item.id}" aria-label="View details"> <a class="fs-detail-link" href={resolve('/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]', { nutrition: s.nutrition, source: item.source, id: item.id })} aria-label="View details">
<ExternalLink size={13} /> <ExternalLink size={13} />
</a> </a>
{/if} {/if}
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { getExerciseById, getExerciseMetrics } from '$lib/data/exercises'; import { getExerciseById, getExerciseMetrics } from '$lib/data/exercises';
import Clock from '@lucide/svelte/icons/clock'; import Clock from '@lucide/svelte/icons/clock';
@@ -152,7 +153,7 @@
}); });
</script> </script>
<a href="/fitness/{sl.history}/{session._id}" class="session-card"> <a href={resolve('/fitness/[history=fitnessHistory]/[id]', { history: sl.history, id: session._id })} class="session-card">
<div class="card-top"> <div class="card-top">
<h3 class="session-name">{session.name}</h3> <h3 class="session-name">{session.name}</h3>
<span class="session-date">{formatDate(session.startTime)} &middot; {formatTime(session.startTime)}</span> <span class="session-date">{formatDate(session.startTime)} &middot; {formatTime(session.startTime)}</span>
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import "$lib/css/shake.css" import "$lib/css/shake.css"
let { icon, ...restProps } = $props<{ icon: string, [key: string]: any }>(); let { icon, ...restProps } = $props<{ icon: string, [key: string]: any }>();
</script> </script>
@@ -26,4 +27,4 @@
} }
</style> </style>
<a href="/rezepte/icon/{icon}" {...restProps} >{icon}</a> <a href={resolve('/[recipeLang=recipeLang]/icon/[icon]', { recipeLang: 'rezepte', icon })} {...restProps} >{icon}</a>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { TranslatedRecipeType } from '$types/types'; import type { TranslatedRecipeType } from '$types/types';
import TranslationFieldComparison from './TranslationFieldComparison.svelte'; import TranslationFieldComparison from './TranslationFieldComparison.svelte';
import CreateIngredientList from '$lib/components/recipes/CreateIngredientList.svelte'; import CreateIngredientList from '$lib/components/recipes/CreateIngredientList.svelte';
@@ -758,7 +759,7 @@ button:disabled {
{#each untranslatedBaseRecipes as baseRecipe} {#each untranslatedBaseRecipes as baseRecipe}
<li> <li>
<strong>{baseRecipe.name}</strong> <strong>{baseRecipe.name}</strong>
<a href="/de/edit/{baseRecipe.shortName}" target="_blank" rel="noopener noreferrer"> <a href={resolve('/[recipeLang=recipeLang]/edit/[name]', { recipeLang: 'de', name: baseRecipe.shortName })} target="_blank" rel="noopener noreferrer">
Open in new tab → Open in new tab →
</a> </a>
</li> </li>
+13 -12
View File
@@ -1,30 +1,31 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import LinksGrid from "$lib/components/LinksGrid.svelte"; import LinksGrid from "$lib/components/LinksGrid.svelte";
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let { data } = $props(); let { data } = $props();
let lang = $state<'de' | 'en'>('de'); let lang = $state<'de' | 'en'>('de');
let recipesUrl = $state('/rezepte'); let recipesUrl = $state(resolve('/[recipeLang=recipeLang]', { recipeLang: 'rezepte' }));
let faithUrl = $state('/glaube'); let faithUrl = $state(resolve('/[faithLang=faithLang]', { faithLang: 'glaube' }));
onMount(() => { onMount(() => {
// Check localStorage for preferred language // Check localStorage for preferred language
const preferredLanguage = localStorage.getItem('preferredLanguage'); const preferredLanguage = localStorage.getItem('preferredLanguage');
if (preferredLanguage === 'en') { if (preferredLanguage === 'en') {
lang = 'en'; lang = 'en';
recipesUrl = '/recipes'; recipesUrl = resolve('/[recipeLang=recipeLang]', { recipeLang: 'recipes' });
faithUrl = '/faith'; faithUrl = resolve('/[faithLang=faithLang]', { faithLang: 'faith' });
} else { } else {
lang = 'de'; lang = 'de';
recipesUrl = '/rezepte'; recipesUrl = resolve('/[recipeLang=recipeLang]', { recipeLang: 'rezepte' });
faithUrl = '/glaube'; faithUrl = resolve('/[faithLang=faithLang]', { faithLang: 'glaube' });
} }
// Listen for language changes from UserHeader // Listen for language changes from UserHeader
const handleLanguageChange = (e: CustomEvent) => { const handleLanguageChange = (e: CustomEvent) => {
lang = e.detail.lang; lang = e.detail.lang;
recipesUrl = lang === 'en' ? '/recipes' : '/rezepte'; recipesUrl = resolve('/[recipeLang=recipeLang]', { recipeLang: lang === 'en' ? 'recipes' : 'rezepte' });
faithUrl = lang === 'en' ? '/faith' : '/glaube'; faithUrl = resolve('/[faithLang=faithLang]', { faithLang: lang === 'en' ? 'faith' : 'glaube' });
}; };
window.addEventListener('languagechange', handleLanguageChange as EventListener); window.addEventListener('languagechange', handleLanguageChange as EventListener);
@@ -151,13 +152,13 @@ section h2{
<h3>{labels.shopping}</h3> <h3>{labels.shopping}</h3>
</a> </a>
<a href="/fitness"> <a href={resolve('/fitness')}>
<svg class="lock-icon"><use href="#lock-icon"/></svg> <svg class="lock-icon"><use href="#lock-icon"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M96 64c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 160 0 64 0 160c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-64-32 0c-17.7 0-32-14.3-32-32l0-64c-17.7 0-32-14.3-32-32s14.3-32 32-32l0-64c0-17.7 14.3-32 32-32l32 0 0-64zm448 0l0 64 32 0c17.7 0 32 14.3 32 32l0 64c17.7 0 32 14.3 32 32s-14.3 32-32 32l0 64c0 17.7-14.3 32-32 32l-32 0 0 64c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-160 0-64 0-160c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32zM416 224l0 64-192 0 0-64 192 0z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M96 64c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 160 0 64 0 160c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-64-32 0c-17.7 0-32-14.3-32-32l0-64c-17.7 0-32-14.3-32-32s14.3-32 32-32l0-64c0-17.7 14.3-32 32-32l32 0 0-64zm448 0l0 64 32 0c17.7 0 32 14.3 32 32l0 64c17.7 0 32 14.3 32 32s-14.3 32-32 32l0 64c0 17.7-14.3 32-32 32l-32 0 0 64c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-160 0-64 0-160c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32zM416 224l0 64-192 0 0-64 192 0z"/></svg>
<h3>{labels.fitness}</h3> <h3>{labels.fitness}</h3>
</a> </a>
<a href="/tasks"> <a href={resolve('/tasks')}>
<svg class="lock-icon"><use href="#lock-icon"/></svg> <svg class="lock-icon"><use href="#lock-icon"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M152.1 38.2c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 113c-9.3-9.4-9.3-24.6 0-34C16.3 69.5 31.5 69.5 40.7 79l21.9 22.3 53.5-59.4c8.9-9.9 24-10.7 33.9-1.8zm0 160c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 273c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l22 22.3 53.5-59.4c8.9-9.9 24-10.7 33.9-1.8zM224 96c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32zm0 160c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32zM160 416c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H192c-17.7 0-32-14.3-32-32zM48 368a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M152.1 38.2c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 113c-9.3-9.4-9.3-24.6 0-34C16.3 69.5 31.5 69.5 40.7 79l21.9 22.3 53.5-59.4c8.9-9.9 24-10.7 33.9-1.8zm0 160c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 273c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l22 22.3 53.5-59.4c8.9-9.9 24-10.7 33.9-1.8zM224 96c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32zm0 160c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32zM160 416c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H192c-17.7 0-32-14.3-32-32zM48 368a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/></svg>
<h3>{labels.tasks}</h3> <h3>{labels.tasks}</h3>
@@ -169,7 +170,7 @@ section h2{
</a> </a>
<a href="/fitness/nutrition"> <a href={resolve('/fitness/nutrition')}>
<svg class="lock-icon"><use href="#lock-icon"/></svg> <svg class="lock-icon"><use href="#lock-icon"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -226 532 468"><path d="m256-140-15-21c-25-34-65-55-108-55C60-216 0-156 0-83v3C0-57 6-32 17-8h106c3 0 6-2 7-5l32-76c4-9 12-15 22-15 9 0 18 5 22 14l51 114 42-83c4-8 12-13 21-13s17 5 22 13l23 47c1 2 4 4 7 4h124c10-24 16-49 16-72v-3c0-73-60-133-133-133-43 0-83 21-108 55l-15 21zM470 40h-98c-21 0-41-12-50-31l-2-3-42 85c-5 8-13 13-22 13-10 0-18-6-22-14L185-20 174 6c-8 21-29 34-51 34H42c48 74 123 142 171 178 12 9 27 14 43 14 15 0 31-4 43-14 48-36 123-104 171-178z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -226 532 468"><path d="m256-140-15-21c-25-34-65-55-108-55C60-216 0-156 0-83v3C0-57 6-32 17-8h106c3 0 6-2 7-5l32-76c4-9 12-15 22-15 9 0 18 5 22 14l51 114 42-83c4-8 12-13 21-13s17 5 22 13l23 47c1 2 4 4 7 4h124c10-24 16-49 16-72v-3c0-73-60-133-133-133-43 0-83 21-108 55l-15 21zM470 40h-98c-21 0-41-12-50-31l-2-3-42 85c-5 8-13 13-22 13-10 0-18-6-22-14L185-20 174 6c-8 21-29 34-51 34H42c48 74 123 142 171 178 12 9 27 14 43 14 15 0 31-4 43-14 48-36 123-104 171-178z"/></svg>
<h3>{labels.nutrition}</h3> <h3>{labels.nutrition}</h3>
@@ -222,7 +223,7 @@ section h2{
<!-- instead of redirect_to_docs(), use a normal link with internal checks for data.session --> <!-- instead of redirect_to_docs(), use a normal link with internal checks for data.session -->
{#if !data.session} {#if !data.session}
<a href="/auth/signin"> <a href={resolve('/auth/signin')}>
<svg class="lock-icon"><use href="#lock-icon"/></svg> <svg class="lock-icon"><use href="#lock-icon"/></svg>
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m106 512h300c24.814 0 45-20.186 45-45v-317h-105c-24.814 0-45-20.186-45-45v-105h-195c-24.814 0-45 20.186-45 45v422c0 24.814 20.186 45 45 45zm60-301h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h120c8.291 0 15 6.709 15 15s-6.709 15-15 15h-120c-8.291 0-15-6.709-15-15s6.709-15 15-15z"/><path d="m346 120h96.211l-111.211-111.211v96.211c0 8.276 6.724 15 15 15z"/></svg> <svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m106 512h300c24.814 0 45-20.186 45-45v-317h-105c-24.814 0-45-20.186-45-45v-105h-195c-24.814 0-45 20.186-45 45v422c0 24.814 20.186 45 45 45zm60-301h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h120c8.291 0 15 6.709 15 15s-6.709 15-15 15h-120c-8.291 0-15-6.709-15-15s6.709-15 15-15z"/><path d="m346 120h96.211l-111.211-111.211v96.211c0 8.276 6.724 15 15 15z"/></svg>
<h3>{labels.documents}</h3> <h3>{labels.documents}</h3>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import SectionError from '$lib/components/SectionError.svelte'; import SectionError from '$lib/components/SectionError.svelte';
import { detectCospendLang, cospendRoot } from '$lib/js/cospendI18n'; import { detectCospendLang, cospendRoot } from '$lib/js/cospendI18n';
@@ -8,7 +9,7 @@
</script> </script>
<SectionError <SectionError
sectionHref={cospendRoot(lang)} sectionHref={resolve('/[cospendRoot=cospendRoot]', { cospendRoot: cospendRoot(lang) })}
sectionLabel={{ en: 'Expenses', de: 'Kosten' }} sectionLabel={{ en: 'Expenses', de: 'Kosten' }}
{isEnglish} {isEnglish}
/> />
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@@ -69,12 +70,12 @@
{#snippet links()} {#snippet links()}
<ul class="site_header"> <ul class="site_header">
{#if !isGuest} {#if !isGuest}
<li style="--active-fill: var(--nord9)"><a href="/{root}/dash" class:active={isActive(`/${root}/dash`)}><LayoutDashboard size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.dash}</span></a></li> <li style="--active-fill: var(--nord9)"><a href={resolve('/[cospendRoot=cospendRoot]/dash', { cospendRoot: root })} class:active={isActive(`/${root}/dash`)}><LayoutDashboard size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.dash}</span></a></li>
{/if} {/if}
<li style="--active-fill: var(--nord13)"><a href="/{root}/list" class:active={isActive(`/${root}/list`)}><ShoppingCart size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.list}</span></a></li> <li style="--active-fill: var(--nord13)"><a href={resolve('/[cospendRoot=cospendRoot]/list', { cospendRoot: root })} class:active={isActive(`/${root}/list`)}><ShoppingCart size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.list}</span></a></li>
{#if !isGuest} {#if !isGuest}
<li style="--active-fill: var(--nord14)"><a href="/{root}/payments" class:active={isActive(`/${root}/payments`)}><span class="nav-icon-wrap nav-icon-wallet"><Wallet size={16} strokeWidth={1.5} class="nav-icon" /></span><span class="nav-label">{labels.payments}</span></a></li> <li style="--active-fill: var(--nord14)"><a href={resolve('/[cospendRoot=cospendRoot]/payments', { cospendRoot: root })} class:active={isActive(`/${root}/payments`)}><span class="nav-icon-wrap nav-icon-wallet"><Wallet size={16} strokeWidth={1.5} class="nav-icon" /></span><span class="nav-label">{labels.payments}</span></a></li>
<li style="--active-fill: var(--nord12); --active-shape: circle(50%)"><a href="/{root}/recurring" class:active={isActive(`/${root}/recurring`)}><span class="nav-icon-wrap"><RefreshCw size={16} strokeWidth={1.5} class="nav-icon" /></span><span class="nav-label">{labels.recurring}</span></a></li> <li style="--active-fill: var(--nord12); --active-shape: circle(50%)"><a href={resolve('/[cospendRoot=cospendRoot]/recurring', { cospendRoot: root })} class:active={isActive(`/${root}/recurring`)}><span class="nav-icon-wrap"><RefreshCw size={16} strokeWidth={1.5} class="nav-icon" /></span><span class="nav-label">{labels.recurring}</span></a></li>
{/if} {/if}
</ul> </ul>
{/snippet} {/snippet}
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
@@ -137,7 +138,7 @@
<div class="actions"> <div class="actions">
{#if balance.netBalance !== 0} {#if balance.netBalance !== 0}
<a href="/{root}/settle" class="btn btn-settlement">{t('settle_debts', lang)}</a> <a href={resolve('/[cospendRoot=cospendRoot]/settle', { cospendRoot: root })} class="btn btn-settlement">{t('settle_debts', lang)}</a>
{/if} {/if}
</div> </div>
@@ -186,7 +187,7 @@
{#if isSettlementPayment(split.paymentId)} {#if isSettlementPayment(split.paymentId)}
<!-- Settlement Payment Display - User -> User Flow --> <!-- Settlement Payment Display - User -> User Flow -->
<a <a
href="/{root}/payments/view/{split.paymentId?._id}" href={resolve('/[cospendRoot=cospendRoot]/payments/view/[id]', { cospendRoot: root, id: split.paymentId?._id })}
class="settlement-flow-activity" class="settlement-flow-activity"
onclick={(e) => handlePaymentClick(split.paymentId?._id, e)} onclick={(e) => handlePaymentClick(split.paymentId?._id, e)}
> >
@@ -217,7 +218,7 @@
<div class="message-content"> <div class="message-content">
<ProfilePicture username={split.paymentId?.paidBy || 'Unknown'} size={36} /> <ProfilePicture username={split.paymentId?.paidBy || 'Unknown'} size={36} />
<a <a
href="/{root}/payments/view/{split.paymentId?._id}" href={resolve('/[cospendRoot=cospendRoot]/payments/view/[id]', { cospendRoot: root, id: split.paymentId?._id })}
class="activity-bubble" class="activity-bubble"
onclick={(e) => handlePaymentClick(split.paymentId?._id, e)} onclick={(e) => handlePaymentClick(split.paymentId?._id, e)}
> >
@@ -262,7 +263,7 @@
{/if} {/if}
</main> </main>
<AddButton href="/{root}/payments/add" /> <AddButton href={resolve('/[cospendRoot=cospendRoot]/payments/add', { cospendRoot: root })} />
<style> <style>
.cospend-main { .cospend-main {
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
@@ -148,7 +149,7 @@
</svg> </svg>
<h2>{t('no_payments_yet', lang)}</h2> <h2>{t('no_payments_yet', lang)}</h2>
<p>{t('start_first_expense', lang)}</p> <p>{t('start_first_expense', lang)}</p>
<a href="/{root}/payments/add" class="btn btn-primary">{t('add_first_payment', lang)}</a> <a href={resolve('/[cospendRoot=cospendRoot]/payments/add', { cospendRoot: root })} class="btn btn-primary">{t('add_first_payment', lang)}</a>
</div> </div>
</div> </div>
{:else} {:else}
@@ -156,7 +157,7 @@
{#each payments as payment} {#each payments as payment}
{#if isSettlementPayment(payment)} {#if isSettlementPayment(payment)}
<!-- Settlement Card - Distinct Layout --> <!-- Settlement Card - Distinct Layout -->
<a href="/{root}/payments/view/{payment._id}" class="payment-card settlement-card"> <a href={resolve('/[cospendRoot=cospendRoot]/payments/view/[id]', { cospendRoot: root, id: payment._id })} class="payment-card settlement-card">
<div class="settlement-header"> <div class="settlement-header">
<div class="settlement-badge"> <div class="settlement-badge">
<span class="settlement-icon">💸</span> <span class="settlement-icon">💸</span>
@@ -190,7 +191,7 @@
</a> </a>
{:else} {:else}
<!-- Regular Payment Card --> <!-- Regular Payment Card -->
<a href="/{root}/payments/view/{payment._id}" class="payment-card"> <a href={resolve('/[cospendRoot=cospendRoot]/payments/view/[id]', { cospendRoot: root, id: payment._id })} class="payment-card">
<div class="payment-header"> <div class="payment-header">
<div class="payment-title-section"> <div class="payment-title-section">
<ProfilePicture username={payment.paidBy} size={40} /> <ProfilePicture username={payment.paidBy} size={40} />
@@ -279,7 +280,7 @@
{/if} {/if}
</main> </main>
<AddButton href="/{root}/payments/add" /> <AddButton href={resolve('/[cospendRoot=cospendRoot]/payments/add', { cospendRoot: root })} />
<style> <style>
.payments-list { .payments-list {
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto, invalidateAll } from '$app/navigation'; import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
@@ -152,7 +153,7 @@
</main> </main>
{#if payment} {#if payment}
<EditButton href="/{root}/payments/edit/{data.paymentId}" /> <EditButton href={resolve('/[cospendRoot=cospendRoot]/payments/edit/[id]', { cospendRoot: root, id: data.paymentId ?? '' })} />
{/if} {/if}
<style> <style>
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getCategoryEmoji } from '$lib/utils/categories'; import { getCategoryEmoji } from '$lib/utils/categories';
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte'; import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
@@ -122,7 +123,7 @@
<div class="empty-state"> <div class="empty-state">
<h2>{t('no_recurring', lang)}</h2> <h2>{t('no_recurring', lang)}</h2>
<p>{t('no_recurring_desc', lang)}</p> <p>{t('no_recurring_desc', lang)}</p>
<a href="/{root}/payments/add" class="btn btn-primary">{t('add_first_payment', lang)}</a> <a href={resolve('/[cospendRoot=cospendRoot]/payments/add', { cospendRoot: root })} class="btn btn-primary">{t('add_first_payment', lang)}</a>
</div> </div>
{:else} {:else}
<div class="payments-grid"> <div class="payments-grid">
@@ -208,7 +209,7 @@
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a href="/{root}/recurring/edit/{payment._id}" class="btn btn-secondary btn-small"> <a href={resolve('/[cospendRoot=cospendRoot]/recurring/edit/[id]', { cospendRoot: root, id: payment._id })} class="btn btn-secondary btn-small">
{t('edit', lang)} {t('edit', lang)}
</a> </a>
<button <button
@@ -232,7 +233,7 @@
{/if} {/if}
</main> </main>
<AddButton href="/{root}/payments/add" /> <AddButton href={resolve('/[cospendRoot=cospendRoot]/payments/add', { cospendRoot: root })} />
<style> <style>
.recurring-payments { .recurring-payments {
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { page } from '$app/stores'; import { page } from '$app/stores';
@@ -165,7 +166,7 @@
<h2>🎉 {t('all_settled', lang)}</h2> <h2>🎉 {t('all_settled', lang)}</h2>
<p>{t('no_debts_msg', lang)}</p> <p>{t('no_debts_msg', lang)}</p>
<div class="actions"> <div class="actions">
<a href="/{root}/dash" class="btn btn-primary">{t('back_to_dashboard', lang)}</a> <a href={resolve('/[cospendRoot=cospendRoot]/dash', { cospendRoot: root })} class="btn btn-primary">{t('back_to_dashboard', lang)}</a>
</div> </div>
</div> </div>
{:else} {:else}
@@ -349,7 +350,7 @@
<button type="submit" class="btn btn-settlement"> <button type="submit" class="btn btn-settlement">
{t('record_settlement', lang)} {t('record_settlement', lang)}
</button> </button>
<a href="/{root}/dash" class="btn btn-secondary"> <a href={resolve('/[cospendRoot=cospendRoot]/dash', { cospendRoot: root })} class="btn btn-secondary">
{t('cancel', lang)} {t('cancel', lang)}
</a> </a>
</div> </div>
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import SectionError from '$lib/components/SectionError.svelte'; import SectionError from '$lib/components/SectionError.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
let faithLang = $derived($page.params.faithLang); let faithLang = $derived($page.params.faithLang!);
let isEnglish = $derived(faithLang === 'faith'); let isEnglish = $derived(faithLang === 'faith');
let sectionLabel = $derived( let sectionLabel = $derived(
faithLang === 'fides' faithLang === 'fides'
@@ -12,7 +13,7 @@
</script> </script>
<SectionError <SectionError
sectionHref="/{faithLang}" sectionHref={resolve('/[faithLang=faithLang]', { faithLang })}
{sectionLabel} {sectionLabel}
{isEnglish} {isEnglish}
/> />
@@ -1,4 +1,5 @@
<script> <script>
import { asset, resolve } from '$app/paths';
import '$lib/css/christ.css'; import '$lib/css/christ.css';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Header from '$lib/components/Header.svelte' import Header from '$lib/components/Header.svelte'
@@ -10,13 +11,20 @@ let { data, children } = $props();
const isEnglish = $derived(data.lang === 'en'); const isEnglish = $derived(data.lang === 'en');
const isLatin = $derived(data.lang === 'la'); const isLatin = $derived(data.lang === 'la');
const eastertide = isEastertide(); const eastertide = isEastertide();
const prayersHref = $derived(isLatin ? '/fides/orationes' : `/${data.faithLang}/${isEnglish ? 'prayers' : 'gebete'}`); const prayersSlug = $derived(isLatin ? 'orationes' : isEnglish ? 'prayers' : 'gebete');
const rosaryHref = $derived(`/${data.faithLang}/${isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz'}`); const prayersHref = $derived(resolve('/[faithLang=faithLang]/[prayers=prayersLang]', { faithLang: data.faithLang, prayers: prayersSlug }));
const calendarHref = $derived(`/${data.faithLang}/${isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender'}`); const rosaryHref = $derived(resolve('/[faithLang=faithLang]/[rosary=rosaryLang]', { faithLang: data.faithLang, rosary: isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz' }));
const apologetikHref = $derived(isLatin ? '/faith/apologetics' : `/${data.faithLang}/${isEnglish ? 'apologetics' : 'apologetik'}`); const calendarHref = $derived(resolve('/[faithLang=faithLang]/[calendar=calendarLang]', { faithLang: data.faithLang, calendar: isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender' }));
const angelusHref = $derived(eastertide const apologetikHref = $derived(
? `${prayersHref}/regina-caeli` isLatin
: `${prayersHref}/angelus`); ? resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: 'faith', apologetikSlug: 'apologetics' })
: resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: data.faithLang, apologetikSlug: isEnglish ? 'apologetics' : 'apologetik' })
);
const angelusHref = $derived(resolve('/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]', {
faithLang: data.faithLang,
prayers: prayersSlug,
prayer: eastertide ? 'regina-caeli' : 'angelus'
}));
const angelusLabel = $derived(eastertide ? 'Regína Cæli' : 'Angelus'); const angelusLabel = $derived(eastertide ? 'Regína Cæli' : 'Angelus');
const labels = $derived({ const labels = $derived({
@@ -38,7 +46,7 @@ function isActive(path) {
const prayersActive = $derived(isActive(prayersHref) && !isActive(angelusHref)); const prayersActive = $derived(isActive(prayersHref) && !isActive(angelusHref));
</script> </script>
<svelte:head> <svelte:head>
<link rel="preload" href="/fonts/crosses.woff2" as="font" type="font/woff2" crossorigin="anonymous"> <link rel="preload" href={asset('/fonts/crosses.woff2')} as="font" type="font/woff2" crossorigin="anonymous">
</svelte:head> </svelte:head>
<Header> <Header>
{#snippet links()} {#snippet links()}
@@ -50,7 +58,7 @@ const prayersActive = $derived(isActive(prayersHref) && !isActive(angelusHref));
{:else} {:else}
<li style="--active-fill: var(--nord14)"><a href={angelusHref} class:active={isActive(angelusHref)} title={angelusLabel}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="6 -274 564 548" fill="currentColor"><path d="M392-162c-4-10-9-18-15-26 5-4 7-8 7-12 0-18-43-32-96-32s-96 14-96 32c0 4 3 8 7 12-6 8-11 16-15 26-15-11-24-24-24-38 0-35 57-64 128-64s128 29 128 64c0 14-9 27-24 38zm-104-22c35 0 64 29 64 64s-29 64-64 64-64-29-64-64 29-64 64-64zM82 159c3-22-3-48-20-64C34 68 16 30 16-12v-64c0-42 34-76 76-76 23 0 44 10 59 27l65 78c-21 16-37 40-43 67l-43 195c-4 17-2 34 5 49h-21c-26 0-46-24-42-50l10-55zm364 56L403 20c-6-27-21-51-42-67l64-77c15-18 36-28 59-28 42 0 76 34 76 76v64c0 42-18 80-46 107-17 16-23 42-20 64l10 56c4 26-16 49-42 49h-20c6-15 8-32 4-49zM220 31c7-32 35-55 68-55s61 23 68 55l43 194c5 20-11 39-31 39H208c-21 0-36-19-31-39l43-194z"/></svg><span class="nav-label">{angelusLabel}</span></a></li> <li style="--active-fill: var(--nord14)"><a href={angelusHref} class:active={isActive(angelusHref)} title={angelusLabel}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="6 -274 564 548" fill="currentColor"><path d="M392-162c-4-10-9-18-15-26 5-4 7-8 7-12 0-18-43-32-96-32s-96 14-96 32c0 4 3 8 7 12-6 8-11 16-15 26-15-11-24-24-24-38 0-35 57-64 128-64s128 29 128 64c0 14-9 27-24 38zm-104-22c35 0 64 29 64 64s-29 64-64 64-64-29-64-64 29-64 64-64zM82 159c3-22-3-48-20-64C34 68 16 30 16-12v-64c0-42 34-76 76-76 23 0 44 10 59 27l65 78c-21 16-37 40-43 67l-43 195c-4 17-2 34 5 49h-21c-26 0-46-24-42-50l10-55zm364 56L403 20c-6-27-21-51-42-67l64-77c15-18 36-28 59-28 42 0 76 34 76 76v64c0 42-18 80-46 107-17 16-23 42-20 64l10 56c4 26-16 49-42 49h-20c6-15 8-32 4-49zM220 31c7-32 35-55 68-55s61 23 68 55l43 194c5 20-11 39-31 39H208c-21 0-36-19-31-39l43-194z"/></svg><span class="nav-label">{angelusLabel}</span></a></li>
{/if} {/if}
<li style="--active-fill: var(--nord13)"><a href="/{data.faithLang}/katechese" class:active={isActive(`/${data.faithLang}/katechese`)} title={labels.catechesis}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M16 12h2"/><path d="M16 8h2"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/><path d="M6 12h2"/><path d="M6 8h2"/></svg><span class="nav-label">{labels.catechesis}</span></a></li> <li style="--active-fill: var(--nord13)"><a href={resolve('/[faithLang=faithLang]/katechese', { faithLang: data.faithLang })} class:active={isActive(`/${data.faithLang}/katechese`)} title={labels.catechesis}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M16 12h2"/><path d="M16 8h2"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/><path d="M6 12h2"/><path d="M6 8h2"/></svg><span class="nav-label">{labels.catechesis}</span></a></li>
<li style="--active-fill: var(--nord10)"><a href={apologetikHref} class:active={isActive(apologetikHref)} title={labels.apologetics}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="M7 21h10"/><path d="M12 3v18"/><path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/></svg><span class="nav-label">{labels.apologetics}</span></a></li> <li style="--active-fill: var(--nord10)"><a href={apologetikHref} class:active={isActive(apologetikHref)} title={labels.apologetics}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="M7 21h10"/><path d="M12 3v18"/><path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/></svg><span class="nav-label">{labels.apologetics}</span></a></li>
<li style="--active-fill: var(--nord15)"><a href={calendarHref} class:active={isActive(calendarHref)} title={labels.calendar}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg><span class="nav-label">{labels.calendar}</span></a></li> <li style="--active-fill: var(--nord15)"><a href={calendarHref} class:active={isActive(calendarHref)} title={labels.calendar}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg><span class="nav-label">{labels.calendar}</span></a></li>
</ul> </ul>
+12 -7
View File
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import LinksGrid from '$lib/components/LinksGrid.svelte'; import LinksGrid from '$lib/components/LinksGrid.svelte';
import { isEastertide } from '$lib/js/easter.svelte'; import { isEastertide } from '$lib/js/easter.svelte';
let { data } = $props(); let { data } = $props();
@@ -8,7 +9,11 @@
const prayersPath = $derived(isLatin ? 'orationes' : isEnglish ? 'prayers' : 'gebete'); const prayersPath = $derived(isLatin ? 'orationes' : isEnglish ? 'prayers' : 'gebete');
const rosaryPath = $derived(isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz'); const rosaryPath = $derived(isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz');
const calendarPath = $derived(isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender'); const calendarPath = $derived(isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender');
const apologetikHref = $derived(isLatin ? '/faith/apologetics' : `/${data.faithLang}/${isEnglish ? 'apologetics' : 'apologetik'}`); const apologetikHref = $derived(
isLatin
? resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: 'faith', apologetikSlug: 'apologetics' })
: resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]', { faithLang: data.faithLang, apologetikSlug: isEnglish ? 'apologetics' : 'apologetik' })
);
const eastertide = isEastertide(); const eastertide = isEastertide();
const labels = $derived({ const labels = $derived({
@@ -81,11 +86,11 @@
</p> </p>
<LinksGrid> <LinksGrid>
<a href="/{data.faithLang}/{prayersPath}"> <a href={resolve('/[faithLang=faithLang]/[prayers=prayersLang]', { faithLang: data.faithLang, prayers: prayersPath })}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M351.2 4.8c3.2-2 6.6-3.3 10-4.1c4.7-1 9.6-.9 14.1 .1c7.7 1.8 14.8 6.5 19.4 13.6L514.6 194.2c8.8 13.1 13.4 28.6 13.4 44.4v73.5c0 6.9 4.4 13 10.9 15.2l79.2 26.4C631.2 358 640 370.2 640 384v96c0 9.9-4.6 19.3-12.5 25.4s-18.1 8.1-27.7 5.5L431 465.9c-56-14.9-95-65.7-95-123.7V224c0-17.7 14.3-32 32-32s32 14.3 32 32v80c0 8.8 7.2 16 16 16s16-7.2 16-16V219.1c0-7-1.8-13.8-5.3-19.8L340.3 48.1c-1.7-3-2.9-6.1-3.6-9.3c-1-4.7-1-9.6 .1-14.1c1.9-8 6.8-15.2 14.3-19.9zm-62.4 0c7.5 4.6 12.4 11.9 14.3 19.9c1.1 4.6 1.2 9.4 .1 14.1c-.7 3.2-1.9 6.3-3.6 9.3L213.3 199.3c-3.5 6-5.3 12.9-5.3 19.8V304c0 8.8 7.2 16 16 16s16-7.2 16-16V224c0-17.7 14.3-32 32-32s32 14.3 32 32V342.3c0 58-39 108.7-95 123.7l-168.7 45c-9.6 2.6-19.9 .5-27.7-5.5S0 490 0 480V384c0-13.8 8.8-26 21.9-30.4l79.2-26.4c6.5-2.2 10.9-8.3 10.9-15.2V238.5c0-15.8 4.7-31.2 13.4-44.4L245.2 14.5c4.6-7.1 11.7-11.8 19.4-13.6c4.6-1.1 9.4-1.2 14.1-.1c3.5 .8 6.9 2.1 10 4.1z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M351.2 4.8c3.2-2 6.6-3.3 10-4.1c4.7-1 9.6-.9 14.1 .1c7.7 1.8 14.8 6.5 19.4 13.6L514.6 194.2c8.8 13.1 13.4 28.6 13.4 44.4v73.5c0 6.9 4.4 13 10.9 15.2l79.2 26.4C631.2 358 640 370.2 640 384v96c0 9.9-4.6 19.3-12.5 25.4s-18.1 8.1-27.7 5.5L431 465.9c-56-14.9-95-65.7-95-123.7V224c0-17.7 14.3-32 32-32s32 14.3 32 32v80c0 8.8 7.2 16 16 16s16-7.2 16-16V219.1c0-7-1.8-13.8-5.3-19.8L340.3 48.1c-1.7-3-2.9-6.1-3.6-9.3c-1-4.7-1-9.6 .1-14.1c1.9-8 6.8-15.2 14.3-19.9zm-62.4 0c7.5 4.6 12.4 11.9 14.3 19.9c1.1 4.6 1.2 9.4 .1 14.1c-.7 3.2-1.9 6.3-3.6 9.3L213.3 199.3c-3.5 6-5.3 12.9-5.3 19.8V304c0 8.8 7.2 16 16 16s16-7.2 16-16V224c0-17.7 14.3-32 32-32s32 14.3 32 32V342.3c0 58-39 108.7-95 123.7l-168.7 45c-9.6 2.6-19.9 .5-27.7-5.5S0 490 0 480V384c0-13.8 8.8-26 21.9-30.4l79.2-26.4c6.5-2.2 10.9-8.3 10.9-15.2V238.5c0-15.8 4.7-31.2 13.4-44.4L245.2 14.5c4.6-7.1 11.7-11.8 19.4-13.6c4.6-1.1 9.4-1.2 14.1-.1c3.5 .8 6.9 2.1 10 4.1z"/></svg>
<h3>{labels.prayers}</h3> <h3>{labels.prayers}</h3>
</a> </a>
<a href="/{data.faithLang}/{rosaryPath}"> <a href={resolve('/[faithLang=faithLang]/[rosary=rosaryLang]', { faithLang: data.faithLang, rosary: rosaryPath })}>
<svg viewBox="0 0 512 512"> <svg viewBox="0 0 512 512">
<g> <g>
<path class="st0" d="M241.251,145.056c-39.203-17.423-91.472,17.423-104.54,60.982c-13.068,43.558,8.712,117.608,65.337,143.742 <path class="st0" d="M241.251,145.056c-39.203-17.423-91.472,17.423-104.54,60.982c-13.068,43.558,8.712,117.608,65.337,143.742
@@ -114,18 +119,18 @@
<h3>{labels.rosary}</h3> <h3>{labels.rosary}</h3>
</a> </a>
{#if eastertide} {#if eastertide}
<a href="/{data.faithLang}/{prayersPath}/regina-caeli" class="regina-link"> <a href={resolve('/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]', { faithLang: data.faithLang, prayers: prayersPath, prayer: 'regina-caeli' })} class="regina-link">
<span class="easter-badge">{isLatin ? 'Tempore' : isEnglish ? 'In season' : 'Zur Zeit'}</span> <span class="easter-badge">{isLatin ? 'Tempore' : isEnglish ? 'In season' : 'Zur Zeit'}</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -274 532 548"><path d="M256-168c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zM6-63l122 199-56 70c-5 7-8 14-8 23 0 19 16 35 36 35h312c20 0 36-16 36-35 0-9-3-16-8-23l-56-70L507-63c3-6 5-13 5-20 0-20-16-37-37-37-7 0-14 2-20 6l-17 12c-13 8-30 6-40-4l-35-35c-7-7-17-11-27-11s-20 4-27 11l-30 30c-13 13-33 13-46 0l-30-30c-7-7-17-11-27-11s-20 4-27 11l-34 34c-11 11-28 13-41 4l-17-11c-6-4-13-6-20-6-20 0-37 17-37 37 0 7 2 14 6 20z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -274 532 548"><path d="M256-168c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zM6-63l122 199-56 70c-5 7-8 14-8 23 0 19 16 35 36 35h312c20 0 36-16 36-35 0-9-3-16-8-23l-56-70L507-63c3-6 5-13 5-20 0-20-16-37-37-37-7 0-14 2-20 6l-17 12c-13 8-30 6-40-4l-35-35c-7-7-17-11-27-11s-20 4-27 11l-30 30c-13 13-33 13-46 0l-30-30c-7-7-17-11-27-11s-20 4-27 11l-34 34c-11 11-28 13-41 4l-17-11c-6-4-13-6-20-6-20 0-37 17-37 37 0 7 2 14 6 20z"/></svg>
<h3>Regína Cæli</h3> <h3>Regína Cæli</h3>
</a> </a>
{:else} {:else}
<a href="/{data.faithLang}/{prayersPath}/angelus"> <a href={resolve('/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]', { faithLang: data.faithLang, prayers: prayersPath, prayer: 'angelus' })}>
<svg xmlns="http://www.w3.org/2000/svg"viewBox="6 -274 564 548"><path d="M392-162c-4-10-9-18-15-26 5-4 7-8 7-12 0-18-43-32-96-32s-96 14-96 32c0 4 3 8 7 12-6 8-11 16-15 26-15-11-24-24-24-38 0-35 57-64 128-64s128 29 128 64c0 14-9 27-24 38zm-104-22c35 0 64 29 64 64s-29 64-64 64-64-29-64-64 29-64 64-64zM82 159c3-22-3-48-20-64C34 68 16 30 16-12v-64c0-42 34-76 76-76 23 0 44 10 59 27l65 78c-21 16-37 40-43 67l-43 195c-4 17-2 34 5 49h-21c-26 0-46-24-42-50l10-55zm364 56L403 20c-6-27-21-51-42-67l64-77c15-18 36-28 59-28 42 0 76 34 76 76v64c0 42-18 80-46 107-17 16-23 42-20 64l10 56c4 26-16 49-42 49h-20c6-15 8-32 4-49zM220 31c7-32 35-55 68-55s61 23 68 55l43 194c5 20-11 39-31 39H208c-21 0-36-19-31-39l43-194z"/></svg> <svg xmlns="http://www.w3.org/2000/svg"viewBox="6 -274 564 548"><path d="M392-162c-4-10-9-18-15-26 5-4 7-8 7-12 0-18-43-32-96-32s-96 14-96 32c0 4 3 8 7 12-6 8-11 16-15 26-15-11-24-24-24-38 0-35 57-64 128-64s128 29 128 64c0 14-9 27-24 38zm-104-22c35 0 64 29 64 64s-29 64-64 64-64-29-64-64 29-64 64-64zM82 159c3-22-3-48-20-64C34 68 16 30 16-12v-64c0-42 34-76 76-76 23 0 44 10 59 27l65 78c-21 16-37 40-43 67l-43 195c-4 17-2 34 5 49h-21c-26 0-46-24-42-50l10-55zm364 56L403 20c-6-27-21-51-42-67l64-77c15-18 36-28 59-28 42 0 76 34 76 76v64c0 42-18 80-46 107-17 16-23 42-20 64l10 56c4 26-16 49-42 49h-20c6-15 8-32 4-49zM220 31c7-32 35-55 68-55s61 23 68 55l43 194c5 20-11 39-31 39H208c-21 0-36-19-31-39l43-194z"/></svg>
<h3>Angelus</h3> <h3>Angelus</h3>
</a> </a>
{/if} {/if}
<a href="/{data.faithLang}/katechese" class="katechese-link"> <a href={resolve('/[faithLang=faithLang]/katechese', { faithLang: data.faithLang })} class="katechese-link">
{#if isEnglish || isLatin}<span class="lang-badge">DE</span>{/if} {#if isEnglish || isLatin}<span class="lang-badge">DE</span>{/if}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -226 532 506"><path d="M256-107v310l1-1c54-22 113-34 172-34h19v-320h-19c-42 0-84 8-123 25-17 7-34 14-50 20zm-25-79 25 10 25-10c47-20 97-30 148-30h35c27 0 48 22 48 48v352c0 27-21 48-48 48h-35c-51 0-101 10-148 30l-13 5c-8 3-16 3-24 0l-13-5c-47-20-97-30-148-30H48c-26 0-48-21-48-48v-352c0-26 22-48 48-48h35c51 0 101 10 148 30z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -226 532 506"><path d="M256-107v310l1-1c54-22 113-34 172-34h19v-320h-19c-42 0-84 8-123 25-17 7-34 14-50 20zm-25-79 25 10 25-10c47-20 97-30 148-30h35c27 0 48 22 48 48v352c0 27-21 48-48 48h-35c-51 0-101 10-148 30l-13 5c-8 3-16 3-24 0l-13-5c-47-20-97-30-148-30H48c-26 0-48-21-48-48v-352c0-26 22-48 48-48h35c51 0 101 10 148 30z"/></svg>
<h3>Katechese</h3> <h3>Katechese</h3>
@@ -134,7 +139,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M384 32H512c17.7 0 32 14.3 32 32s-14.3 32-32 32H398.4c-5.2 25.8-22.9 47.1-46.4 57.3V448H544c17.7 0 32 14.3 32 32s-14.3 32-32 32H320 96c-17.7 0-32-14.3-32-32s14.3-32 32-32H288V153.3c-23.5-10.3-41.2-31.6-46.4-57.3H128c-17.7 0-32-14.3-32-32s14.3-32 32-32H256c14.6-19.4 37.8-32 64-32s49.4 12.6 64 32zm55.6 288H584.4L512 195.8 439.6 320zM512 416c-62.9 0-115.2-34-126-78.9c-2.6-11 1-22.3 6.7-32.1l95.2-163.2c5-8.6 14.2-13.8 24.1-13.8s19.1 5.3 24.1 13.8l95.2 163.2c5.7 9.8 9.3 21.1 6.7 32.1C627.2 382 574.9 416 512 416zM126.8 195.8L54.4 320H199.3L126.8 195.8zM.9 337.1c-2.6-11 1-22.3 6.7-32.1l95.2-163.2c5-8.6 14.2-13.8 24.1-13.8s19.1 5.3 24.1 13.8l95.2 163.2c5.7 9.8 9.3 21.1 6.7 32.1C242 382 189.7 416 126.8 416S11.7 382 .9 337.1z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M384 32H512c17.7 0 32 14.3 32 32s-14.3 32-32 32H398.4c-5.2 25.8-22.9 47.1-46.4 57.3V448H544c17.7 0 32 14.3 32 32s-14.3 32-32 32H320 96c-17.7 0-32-14.3-32-32s14.3-32 32-32H288V153.3c-23.5-10.3-41.2-31.6-46.4-57.3H128c-17.7 0-32-14.3-32-32s14.3-32 32-32H256c14.6-19.4 37.8-32 64-32s49.4 12.6 64 32zm55.6 288H584.4L512 195.8 439.6 320zM512 416c-62.9 0-115.2-34-126-78.9c-2.6-11 1-22.3 6.7-32.1l95.2-163.2c5-8.6 14.2-13.8 24.1-13.8s19.1 5.3 24.1 13.8l95.2 163.2c5.7 9.8 9.3 21.1 6.7 32.1C627.2 382 574.9 416 512 416zM126.8 195.8L54.4 320H199.3L126.8 195.8zM.9 337.1c-2.6-11 1-22.3 6.7-32.1l95.2-163.2c5-8.6 14.2-13.8 24.1-13.8s19.1 5.3 24.1 13.8l95.2 163.2c5.7 9.8 9.3 21.1 6.7 32.1C242 382 189.7 416 126.8 416S11.7 382 .9 337.1z"/></svg>
<h3>{labels.apologetics}</h3> <h3>{labels.apologetics}</h3>
</a> </a>
<a href="/{data.faithLang}/{calendarPath}"> <a href={resolve('/[faithLang=faithLang]/[calendar=calendarLang]', { faithLang: data.faithLang, calendar: calendarPath })}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M128 0c17.7 0 32 14.3 32 32V64H288V32c0-17.7 14.3-32 32-32s32 14.3 32 32V64h48c26.5 0 48 21.5 48 48v48H0V112C0 85.5 21.5 64 48 64H96V32c0-17.7 14.3-32 32-32zM0 192H448V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V192zm64 80v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm128 0v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H208c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H336zM64 400v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H208zm112 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H336c-8.8 0-16 7.2-16 16z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M128 0c17.7 0 32 14.3 32 32V64H288V32c0-17.7 14.3-32 32-32s32 14.3 32 32V64h48c26.5 0 48 21.5 48 48v48H0V112C0 85.5 21.5 64 48 64H96V32c0-17.7 14.3-32 32-32zM0 192H448V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V192zm64 80v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm128 0v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H208c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H336zM64 400v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H208zm112 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H336c-8.8 0-16 7.2-16 16z"/></svg>
<h3>{labels.calendar}</h3> <h3>{labels.calendar}</h3>
</a> </a>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import Shield from '@lucide/svelte/icons/shield'; import Shield from '@lucide/svelte/icons/shield';
import Flame from '@lucide/svelte/icons/flame'; import Flame from '@lucide/svelte/icons/flame';
@@ -55,7 +56,7 @@
</section> </section>
<section class="cards" aria-label={t.title}> <section class="cards" aria-label={t.title}>
<a class="case-card contra" href="/{faithLang}/{slug}/contra"> <a class="case-card contra" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/contra', { faithLang, apologetikSlug: slug })}>
<div class="card-glyph" aria-hidden="true"><Shield size={28} strokeWidth={2} /></div> <div class="card-glyph" aria-hidden="true"><Shield size={28} strokeWidth={2} /></div>
<div class="card-body"> <div class="card-body">
<div class="card-sub">{t.contraSub}</div> <div class="card-sub">{t.contraSub}</div>
@@ -65,7 +66,7 @@
</div> </div>
</a> </a>
<a class="case-card pro" href="/{faithLang}/{slug}/pro"> <a class="case-card pro" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/pro', { faithLang, apologetikSlug: slug })}>
<div class="card-glyph" aria-hidden="true"><Flame size={28} strokeWidth={2} /></div> <div class="card-glyph" aria-hidden="true"><Flame size={28} strokeWidth={2} /></div>
<div class="card-body"> <div class="card-body">
<div class="card-sub">{t.proSub}</div> <div class="card-sub">{t.proSub}</div>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import CaseTabs from '$lib/components/faith/CaseTabs.svelte'; import CaseTabs from '$lib/components/faith/CaseTabs.svelte';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte'; import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
@@ -181,7 +182,7 @@
<article class="arg-row" id="arg-{arg.id}"> <article class="arg-row" id="arg-{arg.id}">
<a <a
class="card-link" class="card-link"
href="/{faithLang}/{slug}/contra/{arg.id}" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/contra/[argId]', { faithLang, apologetikSlug: slug, argId: arg.id })}
aria-label={arg.title} aria-label={arg.title}
></a> ></a>
<div class="arg-num"> <div class="arg-num">
@@ -201,7 +202,7 @@
{@const a = ARCHETYPES[archId]} {@const a = ARCHETYPES[archId]}
<a <a
class="archetype-badge" class="archetype-badge"
href="/{faithLang}/{slug}/contra/{arg.id}/{archId}" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/contra/[argId]/[[archId]]', { faithLang, apologetikSlug: slug, argId: arg.id, archId })}
title="{a.name}{a.sub}" title="{a.name}{a.sub}"
> >
<span class="glyph" aria-hidden="true" style="background:{a.color};"> <span class="glyph" aria-hidden="true" style="background:{a.color};">
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte'; import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
let { data } = $props(); let { data } = $props();
@@ -100,7 +101,7 @@
<ApologetikToc title={tocLabel} items={tocItems} activeId={arg.id} /> <ApologetikToc title={tocLabel} items={tocItems} activeId={arg.id} />
<main class="detail"> <main class="detail">
<a class="back-link" href="/{faithLang}/{slug}/contra">{labels.back}</a> <a class="back-link" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/contra', { faithLang, apologetikSlug: slug })}>{labels.back}</a>
<div class="detail-eyebrow"> <div class="detail-eyebrow">
{labels.eyebrowPrefix} {labels.eyebrowPrefix}
@@ -122,7 +123,7 @@
{@const isActive = id === activeId} {@const isActive = id === activeId}
{@const isPick = alexPicks.includes(id)} {@const isPick = alexPicks.includes(id)}
<a <a
href="/{faithLang}/{slug}/contra/{arg.id}/{id}" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/contra/[argId]/[[archId]]', { faithLang, apologetikSlug: slug, argId: arg.id, archId: id })}
role="tab" role="tab"
aria-selected={isActive} aria-selected={isActive}
class="tab" class="tab"
@@ -198,7 +199,7 @@
{#each arg.related as rid (rid)} {#each arg.related as rid (rid)}
{@const r = data.args.find((x) => x.id === rid)} {@const r = data.args.find((x) => x.id === rid)}
{#if r} {#if r}
<a class="related-item" href="/{faithLang}/{slug}/contra/{r.id}"> <a class="related-item" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/contra/[argId]', { faithLang, apologetikSlug: slug, argId: r.id })}>
<span class="num">{String(r.n).padStart(2, '0')}</span> <span class="num">{String(r.n).padStart(2, '0')}</span>
{r.title} {r.title}
</a> </a>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { POS_LAYER_COLORS, type PosArgument } from '$lib/data/apologetik'; import { POS_LAYER_COLORS, type PosArgument } from '$lib/data/apologetik';
import CaseTabs from '$lib/components/faith/CaseTabs.svelte'; import CaseTabs from '$lib/components/faith/CaseTabs.svelte';
@@ -174,7 +175,7 @@
{@const stroke = POS_LAYER_COLORS[it.layer]} {@const stroke = POS_LAYER_COLORS[it.layer]}
{@const opacity = 0.25 + (it.strength / 5) * 0.55} {@const opacity = 0.25 + (it.strength / 5) * 0.55}
{@const sw = 1.6 + it.strength * 1.0} {@const sw = 1.6 + it.strength * 1.0}
<a href="/{faithLang}/{slug}/pro/{it.id}" aria-label={it.title}> <a href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/pro/[posArgId]', { faithLang, apologetikSlug: slug, posArgId: it.id })} aria-label={it.title}>
<path <path
d="M 38 {it.y} C {W * 0.45} {it.y}, {W * 0.55} {targetY}, {targetX} {targetY}" d="M 38 {it.y} C {W * 0.45} {it.y}, {W * 0.55} {targetY}, {targetX} {targetY}"
fill="none" fill="none"
@@ -253,7 +254,7 @@
<article class="pos-row" id="pos-{arg.id}"> <article class="pos-row" id="pos-{arg.id}">
<a <a
class="card-link" class="card-link"
href="/{faithLang}/{slug}/pro/{arg.id}" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/pro/[posArgId]', { faithLang, apologetikSlug: slug, posArgId: arg.id })}
aria-label={arg.title} aria-label={arg.title}
></a> ></a>
<div class="pos-num"> <div class="pos-num">
@@ -286,7 +287,7 @@
{@const v = POS_VOICES[vid]} {@const v = POS_VOICES[vid]}
<a <a
class="archetype-badge" class="archetype-badge"
href="/{faithLang}/{slug}/pro/{arg.id}/{vid}" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/pro/[posArgId]/[[voiceId]]', { faithLang, apologetikSlug: slug, posArgId: arg.id, voiceId: vid })}
title="{v.name}{v.sub}" title="{v.name}{v.sub}"
> >
<span class="glyph" aria-hidden="true" style="background:{v.color};" <span class="glyph" aria-hidden="true" style="background:{v.color};"
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte'; import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
let { data } = $props(); let { data } = $props();
@@ -106,7 +107,7 @@
<ApologetikToc title={tocLabel} items={tocItems} activeId={arg.id} /> <ApologetikToc title={tocLabel} items={tocItems} activeId={arg.id} />
<main class="detail"> <main class="detail">
<a class="back-link" href="/{faithLang}/{slug}/pro">{labels.back}</a> <a class="back-link" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/pro', { faithLang, apologetikSlug: slug })}>{labels.back}</a>
{#if layer} {#if layer}
<div class="layer-tag">{layer.sub}</div> <div class="layer-tag">{layer.sub}</div>
@@ -142,7 +143,7 @@
{@const v = POS_VOICES[id]} {@const v = POS_VOICES[id]}
{@const isActive = id === activeId} {@const isActive = id === activeId}
<a <a
href="/{faithLang}/{slug}/pro/{arg.id}/{id}" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/pro/[posArgId]/[[voiceId]]', { faithLang, apologetikSlug: slug, posArgId: arg.id, voiceId: id })}
role="tab" role="tab"
aria-selected={isActive} aria-selected={isActive}
class="tab" class="tab"
@@ -203,7 +204,7 @@
{#each arg.related as rid (rid)} {#each arg.related as rid (rid)}
{@const r = POS_ARGUMENTS.find((x) => x.id === rid)} {@const r = POS_ARGUMENTS.find((x) => x.id === rid)}
{#if r} {#if r}
<a class="related-item" href="/{faithLang}/{slug}/pro/{r.id}"> <a class="related-item" href={resolve('/[faithLang=faithLang]/[apologetikSlug=apologetikSlug]/pro/[posArgId]', { faithLang, apologetikSlug: slug, posArgId: r.id })}>
<span class="num">{String(r.n).padStart(2, '0')}</span> <span class="num">{String(r.n).padStart(2, '0')}</span>
{r.title} {r.title}
</a> </a>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { page } from '$app/state'; import { page } from '$app/state';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@@ -103,17 +104,24 @@
// URL: /{faithLang}/{calendar}/{rite}/{yyyy}/{mm}/{dd} — rite is a required // URL: /{faithLang}/{calendar}/{rite}/{yyyy}/{mm}/{dd} — rite is a required
// path segment so day/month nav stays inside the active rite. // path segment so day/month nav stays inside the active rite.
const riteBase = $derived(`/${page.params.faithLang}/${page.params.calendar}/${rite}`); const riteParams = $derived({
const calendarBase = $derived(`/${page.params.faithLang}/${page.params.calendar}`); faithLang: page.params.faithLang!,
calendar: page.params.calendar!,
rite
});
function dayHref(iso: string) { function dayHref(iso: string) {
const [yy, mm, dd] = iso.split('-'); const [yy, mm, dd] = iso.split('-');
return `${riteBase}/${yy}/${mm}/${dd}${dioceseQuery}`; return resolve('/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]', {
...riteParams, yyyy: yy, mm, dd
}) + dioceseQuery;
} }
function detailHref(iso: string) { function detailHref(iso: string) {
const [yy, mm, dd] = iso.split('-'); const [yy, mm, dd] = iso.split('-');
return `${riteBase}/detail/${yy}/${mm}/${dd}${dioceseQuery}`; return resolve('/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/detail/[yyyy=calendarYear]/[mm=calendarMonth]/[dd=calendarDay]', {
...riteParams, yyyy: yy, mm, dd
}) + dioceseQuery;
} }
// Hero card: prefer the currently-selected day; fall back to today when // Hero card: prefer the currently-selected day; fall back to today when
@@ -121,12 +129,19 @@
const hero = $derived(selected ?? today); const hero = $derived(selected ?? today);
function monthHref(y: number, m: number) { function monthHref(y: number, m: number) {
return `${riteBase}/${y}/${pad(m + 1)}${dioceseQuery}`; return resolve('/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]', {
...riteParams, yyyy: String(y), mm: pad(m + 1)
}) + dioceseQuery;
} }
const todayHref = $derived.by(() => { const todayHref = $derived.by(() => {
const now = new Date(); const now = new Date();
return `${riteBase}/${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())}${dioceseQuery}`; return resolve('/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]', {
...riteParams,
yyyy: String(now.getFullYear()),
mm: pad(now.getMonth() + 1),
dd: pad(now.getDate())
}) + dioceseQuery;
}); });
const pageTitle = $derived(t('calendar', lang)); const pageTitle = $derived(t('calendar', lang));
@@ -136,14 +151,23 @@
// re-applies each rite's default if none is given. // re-applies each rite's default if none is given.
function riteHref(r: 'novus' | 'vetus') { function riteHref(r: 'novus' | 'vetus') {
const dd = selectedIso.slice(8, 10); const dd = selectedIso.slice(8, 10);
return `${calendarBase}/${r}/${year}/${pad(month + 1)}/${dd}`; return resolve('/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]', {
faithLang: page.params.faithLang!,
calendar: page.params.calendar!,
rite: r,
yyyy: String(year),
mm: pad(month + 1),
dd
});
} }
function onDioceseChange(e: Event) { function onDioceseChange(e: Event) {
const next = (e.currentTarget as HTMLSelectElement).value; const next = (e.currentTarget as HTMLSelectElement).value;
const def = rite === 'vetus' ? DEFAULT_DIOCESE_1962 : DEFAULT_DIOCESE_1969; const def = rite === 'vetus' ? DEFAULT_DIOCESE_1962 : DEFAULT_DIOCESE_1969;
const dd = selectedIso.slice(8, 10); const dd = selectedIso.slice(8, 10);
const path = `${riteBase}/${year}/${pad(month + 1)}/${dd}`; const path = resolve('/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]', {
...riteParams, yyyy: String(year), mm: pad(month + 1), dd
});
goto(next === def ? path : `${path}?diocese=${next}`, { noScroll: true }); goto(next === def ? path : `${path}?diocese=${next}`, { noScroll: true });
} }
</script> </script>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { page } from '$app/state'; import { page } from '$app/state';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
@@ -37,23 +38,46 @@
return String(n).padStart(2, '0'); return String(n).padStart(2, '0');
} }
const riteBase = $derived(`/${page.params.faithLang}/${page.params.calendar}/${rite}`);
const dioceseQuery = $derived.by(() => { const dioceseQuery = $derived.by(() => {
const q = page.url.searchParams.get('diocese'); const q = page.url.searchParams.get('diocese');
return q ? `?diocese=${q}` : ''; return q ? `?diocese=${q}` : '';
}); });
const riteParams = $derived({
faithLang: page.params.faithLang!,
calendar: page.params.calendar!,
rite
});
// Back link: return to the month view for the day's month // Back link: return to the month view for the day's month
const backHref = $derived(`${riteBase}/${year}/${pad(month + 1)}${dioceseQuery}`); const backHref = $derived(
resolve('/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]', {
...riteParams,
yyyy: String(year),
mm: pad(month + 1)
}) + dioceseQuery
);
// Day cell in the month grid is the same URL, kept the selection by including dd // Day cell in the month grid is the same URL, kept the selection by including dd
const dayInMonthHref = $derived(`${riteBase}/${year}/${pad(month + 1)}/${pad(data.day)}${dioceseQuery}`); const dayInMonthHref = $derived(
resolve('/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]', {
...riteParams,
yyyy: String(year),
mm: pad(month + 1),
dd: pad(data.day)
}) + dioceseQuery
);
// Next/prev day navigation inside the detail view // Next/prev day navigation inside the detail view
function shiftDay(days: number): string { function shiftDay(days: number): string {
const [y, m, d] = iso.split('-').map(Number); const [y, m, d] = iso.split('-').map(Number);
const next = new Date(y, m - 1, d); const next = new Date(y, m - 1, d);
next.setDate(next.getDate() + days); next.setDate(next.getDate() + days);
return `${riteBase}/detail/${next.getFullYear()}/${pad(next.getMonth() + 1)}/${pad(next.getDate())}${dioceseQuery}`; return resolve('/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/detail/[yyyy=calendarYear]/[mm=calendarMonth]/[dd=calendarDay]', {
...riteParams,
yyyy: String(next.getFullYear()),
mm: pad(next.getMonth() + 1),
dd: pad(next.getDate())
}) + dioceseQuery;
} }
const prevHref = $derived(shiftDay(-1)); const prevHref = $derived(shiftDay(-1));
const nextHref = $derived(shiftDay(1)); const nextHref = $derived(shiftDay(1));
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { createLanguageContext } from "$lib/contexts/languageContext.js"; import { createLanguageContext } from "$lib/contexts/languageContext.js";
@@ -171,7 +172,10 @@
]); ]);
// Base URL for prayer links // Base URL for prayer links
const baseUrl = $derived(isLatin ? '/fides/orationes' : isEnglish ? '/faith/prayers' : '/glaube/gebete'); const baseUrl = $derived(resolve('/[faithLang=faithLang]/[prayers=prayersLang]', {
faithLang: isLatin ? 'fides' : isEnglish ? 'faith' : 'glaube',
prayers: isLatin ? 'orationes' : isEnglish ? 'prayers' : 'gebete'
}));
// Get prayer name by ID (reactive based on language) // Get prayer name by ID (reactive based on language)
/** @param {string} id */ /** @param {string} id */
@@ -1,4 +1,5 @@
<script> <script>
import { asset } from '$app/paths';
let { pos, BEAD_SPACING, DECADE_OFFSET, activeSection, decadeCounters } = $props(); let { pos, BEAD_SPACING, DECADE_OFFSET, activeSection, decadeCounters } = $props();
</script> </script>
<style> <style>
@@ -106,7 +107,7 @@
<circle cx="25" cy={pos.lbead2} r="15" class="large-bead" class:active-large-bead={activeSection === 'lbead2'} data-section="lbead2" /> <circle cx="25" cy={pos.lbead2} r="15" class="large-bead" class:active-large-bead={activeSection === 'lbead2'} data-section="lbead2" />
<!-- Benedictus Medal --> <!-- Benedictus Medal -->
<image class="medal" href="/glaube/benedictus.svg" x="5" y={pos.lbead2 + 25} width="40" height="40" /> <image class="medal" href={asset('/glaube/benedictus.svg')} x="5" y={pos.lbead2 + 25} width="40" height="40" />
<!-- 5 Decades --> <!-- 5 Decades -->
{#each [1, 2, 3, 4, 5] as d (d)} {#each [1, 2, 3, 4, 5] as d (d)}
@@ -125,7 +126,7 @@
{/if} {/if}
{/each} {/each}
<image class="medal" href="/glaube/benedictus.svg" x="5" y={pos.secret5 + DECADE_OFFSET + 9 * BEAD_SPACING + 15} width="40" height="40" /> <image class="medal" href={asset('/glaube/benedictus.svg')} x="5" y={pos.secret5 + DECADE_OFFSET + 9 * BEAD_SPACING + 15} width="40" height="40" />
<!-- Final transition: Gloria + Fatima --> <!-- Final transition: Gloria + Fatima -->
<circle cx="25" cy={pos.final_transition} r="15" class="large-bead" class:active-large-bead={activeSection === 'final_transition'} data-section="final_transition" /> <circle cx="25" cy={pos.final_transition} r="15" class="large-bead" class:active-large-bead={activeSection === 'final_transition'} data-section="final_transition" />
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import LinksGrid from '$lib/components/LinksGrid.svelte'; import LinksGrid from '$lib/components/LinksGrid.svelte';
let { data } = $props(); let { data } = $props();
const isGerman = $derived(data.lang === 'de'); const isGerman = $derived(data.lang === 'de');
@@ -48,7 +49,7 @@
<h1>Katechese</h1> <h1>Katechese</h1>
{#if !isGerman} {#if !isGerman}
<p class="lang-notice">{isLatin ? 'Haec catechesis tantum in ' : 'This catechesis is only available in '}<a href="/glaube/katechese">{isLatin ? 'lingua Germanica' : 'German'}</a>{isLatin ? ' praesto est.' : '.'}</p> <p class="lang-notice">{isLatin ? 'Haec catechesis tantum in ' : 'This catechesis is only available in '}<a href={resolve('/glaube/katechese')}>{isLatin ? 'lingua Germanica' : 'German'}</a>{isLatin ? ' praesto est.' : '.'}</p>
{/if} {/if}
<p> <p>
Aufgearbeitete Lehrinhalte aus dem Glaubenskurs von P. Martin Ramm FSSP. Aufgearbeitete Lehrinhalte aus dem Glaubenskurs von P. Martin Ramm FSSP.
@@ -57,7 +58,7 @@
<p class="disclaimer">Diese Seiten stellen eine freie Aufbereitung der erhaltenen Unterlagen dar und sind kein offizielles Angebot von P. Martin Ramm oder der FSSP. Etwaige Fehler oder Missverständnisse sind dem Verfasser dieser Seiten anzulasten.</p> <p class="disclaimer">Diese Seiten stellen eine freie Aufbereitung der erhaltenen Unterlagen dar und sind kein offizielles Angebot von P. Martin Ramm oder der FSSP. Etwaige Fehler oder Missverständnisse sind dem Verfasser dieser Seiten anzulasten.</p>
<LinksGrid> <LinksGrid>
<a href="/{data.faithLang}/katechese/zehn-gebote"> <a href={resolve('/[faithLang=faithLang]/katechese/zehn-gebote', { faithLang: data.faithLang })}>
<svg viewBox="2 14 96 68" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="2 14 96 68" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="20" width="38" height="55" rx="12" ry="12" stroke="currentColor" stroke-width="3" fill="none"/> <rect x="8" y="20" width="38" height="55" rx="12" ry="12" stroke="currentColor" stroke-width="3" fill="none"/>
<rect x="54" y="20" width="38" height="55" rx="12" ry="12" stroke="currentColor" stroke-width="3" fill="none"/> <rect x="54" y="20" width="38" height="55" rx="12" ry="12" stroke="currentColor" stroke-width="3" fill="none"/>
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import ArrowDown from '@lucide/svelte/icons/arrow-down'; import ArrowDown from '@lucide/svelte/icons/arrow-down';
import ArrowLeft from '@lucide/svelte/icons/arrow-left'; import ArrowLeft from '@lucide/svelte/icons/arrow-left';
@@ -92,7 +93,7 @@
</header> </header>
{#if !isGerman} {#if !isGerman}
<p class="lang-notice">{isLatin ? 'Haec catechesis tantum in ' : 'This catechesis is only available in '}<a href="/glaube/katechese/zehn-gebote">{isLatin ? 'lingua Germanica' : 'German'}</a>{isLatin ? ' praesto est.' : '.'}</p> <p class="lang-notice">{isLatin ? 'Haec catechesis tantum in ' : 'This catechesis is only available in '}<a href={resolve('/glaube/katechese/zehn-gebote')}>{isLatin ? 'lingua Germanica' : 'German'}</a>{isLatin ? ' praesto est.' : '.'}</p>
{/if} {/if}
<section id="ursprung"> <section id="ursprung">
@@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import SectionError from '$lib/components/SectionError.svelte'; import SectionError from '$lib/components/SectionError.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
let recipeLang = $derived($page.params.recipeLang); let recipeLang = $derived($page.params.recipeLang!);
let isEnglish = $derived(recipeLang === 'recipes'); let isEnglish = $derived(recipeLang === 'recipes');
</script> </script>
<SectionError <SectionError
sectionHref="/{recipeLang}" sectionHref={resolve('/[recipeLang=recipeLang]', { recipeLang })}
sectionLabel={{ en: 'Recipes', de: 'Rezepte' }} sectionLabel={{ en: 'Recipes', de: 'Rezepte' }}
{isEnglish} {isEnglish}
/> />
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import '$lib/css/recipe-links.css'; import '$lib/css/recipe-links.css';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onNavigate } from '$app/navigation'; import { onNavigate } from '$app/navigation';
@@ -80,14 +81,14 @@ function isActive(path) {
<Header> <Header>
{#snippet links()} {#snippet links()}
<ul class=site_header> <ul class=site_header>
<li style="--active-fill: var(--nord9)"><a href="/{data.recipeLang}" class:active={isActive(`/${data.recipeLang}`)} title={labels.allRecipes}><BookOpen size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.allRecipes}</span></a></li> <li style="--active-fill: var(--nord9)"><a href={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} class:active={isActive(`/${data.recipeLang}`)} title={labels.allRecipes}><BookOpen size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.allRecipes}</span></a></li>
{#if user} {#if user}
<li style="--active-fill: var(--nord11)"><a href="/{data.recipeLang}/favorites" class:active={isActive(`/${data.recipeLang}/favorites`)} title={labels.favorites}><Heart size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.favorites}</span></a></li> <li style="--active-fill: var(--nord11)"><a href={resolve('/[recipeLang=recipeLang]/favorites', { recipeLang: data.recipeLang })} class:active={isActive(`/${data.recipeLang}/favorites`)} title={labels.favorites}><Heart size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.favorites}</span></a></li>
{/if} {/if}
<li style="--active-fill: var(--nord14)"><a href="/{data.recipeLang}/season" class:active={isActive(`/${data.recipeLang}/season`)} title={labels.inSeason}><Leaf size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.inSeason}</span></a></li> <li style="--active-fill: var(--nord14)"><a href={resolve('/[recipeLang=recipeLang]/season', { recipeLang: data.recipeLang })} class:active={isActive(`/${data.recipeLang}/season`)} title={labels.inSeason}><Leaf size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.inSeason}</span></a></li>
<li style="--active-fill: var(--nord9)"><a href="/{data.recipeLang}/category" class:active={isActive(`/${data.recipeLang}/category`)} title={labels.category}><LayoutGrid size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.category}</span></a></li> <li style="--active-fill: var(--nord9)"><a href={resolve('/[recipeLang=recipeLang]/category', { recipeLang: data.recipeLang })} class:active={isActive(`/${data.recipeLang}/category`)} title={labels.category}><LayoutGrid size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.category}</span></a></li>
<li style="--active-fill: var(--nord15)"><a href="/{data.recipeLang}/icon" class:active={isActive(`/${data.recipeLang}/icon`)} title={labels.icon}><Palette size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.icon}</span></a></li> <li style="--active-fill: var(--nord15)"><a href={resolve('/[recipeLang=recipeLang]/icon', { recipeLang: data.recipeLang })} class:active={isActive(`/${data.recipeLang}/icon`)} title={labels.icon}><Palette size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.icon}</span></a></li>
<li style="--active-fill: var(--nord13)"><a href="/{data.recipeLang}/tag" class:active={isActive(`/${data.recipeLang}/tag`)} title={labels.keywords}><Tag size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.keywords}</span></a></li> <li style="--active-fill: var(--nord13)"><a href={resolve('/[recipeLang=recipeLang]/tag', { recipeLang: data.recipeLang })} class:active={isActive(`/${data.recipeLang}/tag`)} title={labels.keywords}><Tag size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.keywords}</span></a></li>
</ul> </ul>
{/snippet} {/snippet}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import AddButton from '$lib/components/AddButton.svelte'; import AddButton from '$lib/components/AddButton.svelte';
import CompactCard from '$lib/components/recipes/CompactCard.svelte'; import CompactCard from '$lib/components/recipes/CompactCard.svelte';
@@ -367,7 +368,7 @@
<div class="hero-text"> <div class="hero-text">
<h1>{labels.title}</h1> <h1>{labels.title}</h1>
<p class="subheading">{labels.subheading}</p> <p class="subheading">{labels.subheading}</p>
<a href="/{data.recipeLang}/{heroRecipe.short_name}" class="hero-featured" <a href={resolve('/[recipeLang=recipeLang]/[name]', { recipeLang: data.recipeLang, name: heroRecipe.short_name })} class="hero-featured"
onclick={() => { onclick={() => {
const img = document.querySelector('.hero-img') as HTMLElement | null; const img = document.querySelector('.hero-img') as HTMLElement | null;
if (img) (img.style as any).viewTransitionName = `recipe-${heroRecipe.short_name}-img`; if (img) (img.style as any).viewTransitionName = `recipe-${heroRecipe.short_name}-img`;
@@ -435,7 +436,7 @@
isFavorite={recipe.isFavorite} isFavorite={recipe.isFavorite}
showFavoriteIndicator={!!data.session?.user} showFavoriteIndicator={!!data.session?.user}
loading_strat={i < 12 ? "eager" : "lazy"} loading_strat={i < 12 ? "eager" : "lazy"}
routePrefix="/{data.recipeLang}" routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })}
/> />
{/each} {/each}
</div> </div>
@@ -448,7 +449,7 @@
</div> </div>
</section> </section>
{#if !isEnglish} {#if !isEnglish}
<AddButton href="/rezepte/add"></AddButton> <AddButton href={resolve('/rezepte/add')}></AddButton>
{/if} {/if}
{:else} {:else}
<div class="hero-fallback"> <div class="hero-fallback">
@@ -465,7 +466,7 @@
isFavorite={recipe.isFavorite} isFavorite={recipe.isFavorite}
showFavoriteIndicator={!!data.session?.user} showFavoriteIndicator={!!data.session?.user}
loading_strat={i < 12 ? "eager" : "lazy"} loading_strat={i < 12 ? "eager" : "lazy"}
routePrefix="/{data.recipeLang}" routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })}
/> />
{/each} {/each}
</div> </div>
@@ -475,6 +476,6 @@
{/if} {/if}
{#if !isEnglish} {#if !isEnglish}
<AddButton href="/rezepte/add"></AddButton> <AddButton href={resolve('/rezepte/add')}></AddButton>
{/if} {/if}
{/if} {/if}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
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';
@@ -55,7 +56,7 @@
: error?.details : error?.details
); );
let recipesHref = $derived(isEnglishRoute ? '/recipes' : '/rezepte'); let recipesHref = $derived(resolve('/[recipeLang=recipeLang]', { recipeLang: isEnglishRoute ? 'recipes' : 'rezepte' }));
function viewGermanRecipe() { goto(`/rezepte/${recipeName}`); } function viewGermanRecipe() { goto(`/rezepte/${recipeName}`); }
function editToTranslate() { goto(`/rezepte/edit/${recipeName}`); } function editToTranslate() { goto(`/rezepte/edit/${recipeName}`); }
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
export const multiplier = writable(0); export const multiplier = writable(0);
@@ -318,7 +319,7 @@ h2{
<div class=tags> <div class=tags>
<h2>{labels.season}</h2> <h2>{labels.season}</h2>
{#each season_iv as season} {#each season_iv as season}
<a class="g-tag" href="/{data.recipeLang}/season/{season[0]}"> <a class="g-tag" href={resolve('/[recipeLang=recipeLang]/season/[month]', { recipeLang: data.recipeLang, month: season[0] })}>
{#if season[0]} {#if season[0]}
{months[season[0] - 1]} {months[season[0] - 1]}
{/if} {/if}
@@ -333,7 +334,7 @@ h2{
<h2 class="section-label">{labels.keywords}</h2> <h2 class="section-label">{labels.keywords}</h2>
<div class="tags center"> <div class="tags center">
{#each data.tags as tag} {#each data.tags as tag}
<a class="g-tag" href="/{data.recipeLang}/tag/{tag}">{tag}</a> <a class="g-tag" href={resolve('/[recipeLang=recipeLang]/tag/[tag]', { recipeLang: data.recipeLang, tag })}>{tag}</a>
{/each} {/each}
</div> </div>
{/if} {/if}
@@ -361,4 +362,4 @@ h2{
</div> </div>
</TitleImgParallax> </TitleImgParallax>
<EditButton href="/rezepte/edit/{data.germanShortName}"></EditButton> <EditButton href={resolve('/[recipeLang=recipeLang]/edit/[name]', { recipeLang: 'rezepte', name: data.germanShortName })}></EditButton>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import CompactCard from '$lib/components/recipes/CompactCard.svelte'; import CompactCard from '$lib/components/recipes/CompactCard.svelte';
@@ -161,7 +162,7 @@ h1 {
<CompactCard <CompactCard
{recipe} {recipe}
{current_month} {current_month}
routePrefix="/{data.recipeLang}" routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })}
/> />
<div class="translation-badge {recipe.translationStatus || 'none'}"> <div class="translation-badge {recipe.translationStatus || 'none'}">
{#if recipe.translationStatus === 'pending'} {#if recipe.translationStatus === 'pending'}
@@ -179,7 +180,7 @@ h1 {
<div class="empty-state"> <div class="empty-state">
<p>Alle Rezepte sind übersetzt!</p> <p>Alle Rezepte sind übersetzt!</p>
<p style="font-size: 1rem; margin-top: 1rem;"> <p style="font-size: 1rem; margin-top: 1rem;">
<a href="/{data.recipeLang}">Zurück zu den Rezepten</a> <a href={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })}>Zurück zu den Rezepten</a>
</p> </p>
</div> </div>
{/if} {/if}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
let { data } = $props<{ data: PageData }>(); let { data } = $props<{ data: PageData }>();
import TagCloud from '$lib/components/TagCloud.svelte'; import TagCloud from '$lib/components/TagCloud.svelte';
@@ -18,7 +19,7 @@
<section> <section>
<TagCloud> <TagCloud>
{#each data.categories as tag} {#each data.categories as tag}
<TagBall {tag} ref="/{data.recipeLang}/category"> <TagBall {tag} ref={resolve('/[recipeLang=recipeLang]/category', { recipeLang: data.recipeLang })}>
</TagBall> </TagBall>
{/each} {/each}
</TagCloud> </TagCloud>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import type { BriefRecipeType } from '$types/types'; import type { BriefRecipeType } from '$types/types';
import Search from '$lib/components/recipes/Search.svelte'; import Search from '$lib/components/recipes/Search.svelte';
@@ -44,6 +45,6 @@
<Search category={data.category} lang={data.lang} recipes={data.allRecipes} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search> <Search category={data.category} lang={data.lang} recipes={data.allRecipes} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
<div class="recipe-grid"> <div class="recipe-grid">
{#each rand_array(displayRecipes) as recipe (recipe._id)} {#each rand_array(displayRecipes) as recipe (recipe._id)}
<CompactCard {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}" /> <CompactCard {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} />
{/each} {/each}
</div> </div>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import CompactCard from '$lib/components/recipes/CompactCard.svelte'; import CompactCard from '$lib/components/recipes/CompactCard.svelte';
import Search from '$lib/components/recipes/Search.svelte'; import Search from '$lib/components/recipes/Search.svelte';
@@ -91,7 +92,7 @@
{/if} {/if}
</p> </p>
<p class="to-try-link"><a href="/{data.recipeLang}/to-try">{labels.toTry} &rarr;</a></p> <p class="to-try-link"><a href={resolve('/[recipeLang=recipeLang]/to-try', { recipeLang: data.recipeLang })}>{labels.toTry} &rarr;</a></p>
<Search favoritesOnly={true} lang={data.lang} recipes={data.favorites} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search> <Search favoritesOnly={true} lang={data.lang} recipes={data.favorites} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
@@ -100,7 +101,7 @@
{:else if filteredFavorites.length > 0} {:else if filteredFavorites.length > 0}
<div class="recipe-grid"> <div class="recipe-grid">
{#each filteredFavorites as recipe (recipe._id)} {#each filteredFavorites as recipe (recipe._id)}
<CompactCard {recipe} {current_month} isFavorite={true} showFavoriteIndicator={true} routePrefix="/{data.recipeLang}" /> <CompactCard {recipe} {current_month} isFavorite={true} showFavoriteIndicator={true} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} />
{/each} {/each}
</div> </div>
{:else if data.favorites.length > 0} {:else if data.favorites.length > 0}
@@ -110,6 +111,6 @@
{:else} {:else}
<div class="empty-state"> <div class="empty-state">
<p>{labels.emptyState1}</p> <p>{labels.emptyState1}</p>
<p><a href="/{data.recipeLang}">{labels.emptyState2}</a></p> <p><a href={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })}>{labels.emptyState2}</a></p>
</div> </div>
{/if} {/if}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
let { data } = $props<{ data: PageData }>(); let { data } = $props<{ data: PageData }>();
@@ -79,6 +80,6 @@
</style> </style>
<div class=flex> <div class=flex>
{#each data.icons as icon} {#each data.icons as icon}
<a href="/{data.recipeLang}/icon/{icon}">{icon}</a> <a href={resolve('/[recipeLang=recipeLang]/icon/[icon]', { recipeLang: data.recipeLang, icon })}>{icon}</a>
{/each} {/each}
</div> </div>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import IconLayout from '$lib/components/recipes/IconLayout.svelte'; import IconLayout from '$lib/components/recipes/IconLayout.svelte';
import CompactCard from '$lib/components/recipes/CompactCard.svelte'; import CompactCard from '$lib/components/recipes/CompactCard.svelte';
@@ -31,11 +32,11 @@
<title>{data.icon} - {siteTitle}</title> <title>{data.icon} - {siteTitle}</title>
</svelte:head> </svelte:head>
<IconLayout icons={data.icons} active_icon={data.icon} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}> <IconLayout icons={data.icons} active_icon={data.icon} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} lang={data.lang} recipes={data.season} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}>
{#snippet recipesSlot()} {#snippet recipesSlot()}
<div class="recipe-grid"> <div class="recipe-grid">
{#each rand_array(filteredRecipes) as recipe (recipe._id)} {#each rand_array(filteredRecipes) as recipe (recipe._id)}
<CompactCard {recipe} icon_override={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}" /> <CompactCard {recipe} icon_override={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} />
{/each} {/each}
</div> </div>
{/snippet} {/snippet}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import Search from '$lib/components/recipes/Search.svelte'; import Search from '$lib/components/recipes/Search.svelte';
import CompactCard from '$lib/components/recipes/CompactCard.svelte'; import CompactCard from '$lib/components/recipes/CompactCard.svelte';
@@ -112,7 +113,7 @@
{#if displayedRecipes.length > 0} {#if displayedRecipes.length > 0}
<div class="recipe-grid"> <div class="recipe-grid">
{#each displayedRecipes as recipe (recipe._id)} {#each displayedRecipes as recipe (recipe._id)}
<CompactCard {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={true} routePrefix="/{data.recipeLang}" /> <CompactCard {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={true} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} />
{/each} {/each}
</div> </div>
{:else if (data.query || hasActiveSearch) && !data.error} {:else if (data.query || hasActiveSearch) && !data.error}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import SeasonLayout from '$lib/components/recipes/SeasonLayout.svelte' import SeasonLayout from '$lib/components/recipes/SeasonLayout.svelte'
import CompactCard from '$lib/components/recipes/CompactCard.svelte'; import CompactCard from '$lib/components/recipes/CompactCard.svelte';
@@ -38,11 +39,11 @@
<title>{labels.title} - {labels.siteTitle}</title> <title>{labels.title} - {labels.siteTitle}</title>
</svelte:head> </svelte:head>
<SeasonLayout active_index={current_month-1} {months} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}> <SeasonLayout active_index={current_month-1} {months} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} lang={data.lang} recipes={data.season} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}>
{#snippet recipesSlot()} {#snippet recipesSlot()}
<div class="recipe-grid"> <div class="recipe-grid">
{#each rand_array(filteredRecipes) as recipe (recipe._id)} {#each rand_array(filteredRecipes) as recipe (recipe._id)}
<CompactCard {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}" /> <CompactCard {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} />
{/each} {/each}
</div> </div>
{/snippet} {/snippet}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import SeasonLayout from '$lib/components/recipes/SeasonLayout.svelte'; import SeasonLayout from '$lib/components/recipes/SeasonLayout.svelte';
import CompactCard from '$lib/components/recipes/CompactCard.svelte'; import CompactCard from '$lib/components/recipes/CompactCard.svelte';
@@ -36,11 +37,11 @@
<title>{currentMonth} - {siteTitle}</title> <title>{currentMonth} - {siteTitle}</title>
</svelte:head> </svelte:head>
<SeasonLayout active_index={data.month -1} {months} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} onSearchResults={handleSearchResults}> <SeasonLayout active_index={data.month -1} {months} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} lang={data.lang} recipes={data.season} onSearchResults={handleSearchResults}>
{#snippet recipesSlot()} {#snippet recipesSlot()}
<div class="recipe-grid"> <div class="recipe-grid">
{#each rand_array(filteredRecipes) as recipe (recipe._id)} {#each rand_array(filteredRecipes) as recipe (recipe._id)}
<CompactCard {recipe} icon_override={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}" /> <CompactCard {recipe} icon_override={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} />
{/each} {/each}
</div> </div>
{/snippet} {/snippet}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
let { data } = $props<{ data: PageData }>(); let { data } = $props<{ data: PageData }>();
import TagCloud from '$lib/components/TagCloud.svelte'; import TagCloud from '$lib/components/TagCloud.svelte';
@@ -45,7 +46,7 @@
<section> <section>
<TagCloud> <TagCloud>
{#each filteredTags as tag} {#each filteredTags as tag}
<TagBall {tag} ref="/{data.recipeLang}/tag"> <TagBall {tag} ref={resolve('/[recipeLang=recipeLang]/tag', { recipeLang: data.recipeLang })}>
</TagBall> </TagBall>
{/each} {/each}
</TagCloud> </TagCloud>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import type { BriefRecipeType } from '$types/types'; import type { BriefRecipeType } from '$types/types';
import CompactCard from '$lib/components/recipes/CompactCard.svelte'; import CompactCard from '$lib/components/recipes/CompactCard.svelte';
@@ -44,6 +45,6 @@
<Search tag={data.tag} lang={data.lang} recipes={data.allRecipes} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search> <Search tag={data.tag} lang={data.lang} recipes={data.allRecipes} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
<div class="recipe-grid"> <div class="recipe-grid">
{#each rand_array(displayRecipes) as recipe (recipe._id)} {#each rand_array(displayRecipes) as recipe (recipe._id)}
<CompactCard {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}" /> <CompactCard {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} />
{/each} {/each}
</div> </div>
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import type { PageData } from './$types'; import type { PageData } from './$types';
import AddButton from '$lib/components/AddButton.svelte'; import AddButton from '$lib/components/AddButton.svelte';
import Converter from './Converter.svelte'; import Converter from './Converter.svelte';
@@ -64,4 +65,4 @@ h1{
</p> </p>
</div> </div>
<AddButton href="/rezepte/add"></AddButton> <AddButton href={resolve('/rezepte/add')}></AddButton>
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import SectionError from '$lib/components/SectionError.svelte'; import SectionError from '$lib/components/SectionError.svelte';
import { detectFitnessLang } from '$lib/js/fitnessI18n'; import { detectFitnessLang } from '$lib/js/fitnessI18n';
@@ -8,7 +9,7 @@
</script> </script>
<SectionError <SectionError
sectionHref={isEnglish ? '/fitness/workout' : '/fitness/training'} sectionHref={resolve('/fitness/[workout=fitnessWorkout]', { workout: isEnglish ? 'workout' : 'training' })}
sectionLabel={{ en: 'Fitness', de: 'Fitness' }} sectionLabel={{ en: 'Fitness', de: 'Fitness' }}
{isEnglish} {isEnglish}
/> />
+7 -6
View File
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
@@ -90,12 +91,12 @@
<Header> <Header>
{#snippet links()} {#snippet links()}
<ul class="site_header"> <ul class="site_header">
<li><a href="/fitness/{s.stats}" class:active={isActive(`/fitness/${s.stats}`)}><BarChart3 size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.stats}</span></a></li> <li><a href={resolve('/fitness/[stats=fitnessStats]', { stats: s.stats })} class:active={isActive(`/fitness/${s.stats}`)}><BarChart3 size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.stats}</span></a></li>
<li style="--active-fill: var(--nord13)"><a href="/fitness/{s.history}" class:active={isActive(`/fitness/${s.history}`)}><Clock size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.history}</span></a></li> <li style="--active-fill: var(--nord13)"><a href={resolve('/fitness/[history=fitnessHistory]', { history: s.history })} class:active={isActive(`/fitness/${s.history}`)}><Clock size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.history}</span></a></li>
<li style="--active-fill: var(--nord8)"><a href="/fitness/{s.workout}" class:active={isActive(`/fitness/${s.workout}`)}><Dumbbell size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.workout}</span></a></li> <li style="--active-fill: var(--nord8)"><a href={resolve('/fitness/[workout=fitnessWorkout]', { workout: s.workout })} class:active={isActive(`/fitness/${s.workout}`)}><Dumbbell size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.workout}</span></a></li>
<li style="--active-fill: var(--nord14)"><a href="/fitness/{s.exercises}" class:active={isActive(`/fitness/${s.exercises}`)}><ListChecks size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.exercises}</span></a></li> <li style="--active-fill: var(--nord14)"><a href={resolve('/fitness/[exercises=fitnessExercises]', { exercises: s.exercises })} class:active={isActive(`/fitness/${s.exercises}`)}><ListChecks size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.exercises}</span></a></li>
<li style="--active-fill: var(--nord12)"><a href="/fitness/{s.measure}" class:active={isActive(`/fitness/${s.measure}`)}><NotebookPen size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.measure}</span></a></li> <li style="--active-fill: var(--nord12)"><a href={resolve('/fitness/[checkin=fitnessCheckIn]', { checkin: s.measure })} class:active={isActive(`/fitness/${s.measure}`)}><NotebookPen size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.measure}</span></a></li>
<li style="--active-fill: var(--nord15)"><a href="/fitness/{s.nutrition}" class:active={isActive(`/fitness/${s.nutrition}`)}><UtensilsCrossed size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.nutrition}</span></a></li> <li style="--active-fill: var(--nord15)"><a href={resolve('/fitness/[nutrition=fitnessNutrition]', { nutrition: s.nutrition })} class:active={isActive(`/fitness/${s.nutrition}`)}><UtensilsCrossed size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">{labels.nutrition}</span></a></li>
</ul> </ul>
{/snippet} {/snippet}
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Pencil from '@lucide/svelte/icons/pencil'; import Pencil from '@lucide/svelte/icons/pencil';
import Trash2 from '@lucide/svelte/icons/trash-2'; import Trash2 from '@lucide/svelte/icons/trash-2';
@@ -541,7 +542,7 @@
</div> </div>
</div> </div>
<a class="bp-card" href="/fitness/{checkinSlug}/body-parts"> <a class="bp-card" href={resolve('/fitness/[checkin=fitnessCheckIn]/body-parts', { checkin: checkinSlug })}>
<div class="bp-figure" aria-hidden="true"> <div class="bp-figure" aria-hidden="true">
<div class="muscle-base">{@html bpFrontSvg}</div> <div class="muscle-base">{@html bpFrontSvg}</div>
<svg class="dot-overlay" viewBox="0 {BP_VIEW_TOP} 660.46 {BP_VIEW_H}" preserveAspectRatio="xMidYMid meet"> <svg class="dot-overlay" viewBox="0 {BP_VIEW_TOP} 660.46 {BP_VIEW_H}" preserveAspectRatio="xMidYMid meet">
@@ -633,7 +634,7 @@
<span class="edit-unit">%</span> <span class="edit-unit">%</span>
</div> </div>
<div class="edit-actions"> <div class="edit-actions">
<a class="edit-more" href="/fitness/{checkinSlug}/edit/{m._id}" aria-label={t('edit_measurement', lang)}> <a class="edit-more" href={resolve('/fitness/[checkin=fitnessCheckIn]/edit/[id]', { checkin: checkinSlug, id: m._id })} aria-label={t('edit_measurement', lang)}>
<Pencil size={11} /> <Pencil size={11} />
<span class="edit-more-label">{lang === 'en' ? 'Edit all fields' : 'Alle Felder bearbeiten'}</span> <span class="edit-more-label">{lang === 'en' ? 'Edit all fields' : 'Alle Felder bearbeiten'}</span>
<ChevronRight size={11} /> <ChevronRight size={11} />
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Search from '@lucide/svelte/icons/search'; import Search from '@lucide/svelte/icons/search';
import Cable from '@lucide/svelte/icons/cable'; import Cable from '@lucide/svelte/icons/cable';
@@ -212,7 +213,7 @@
<ul class="exercise-list"> <ul class="exercise-list">
{#each filtered as exercise (exercise.id)} {#each filtered as exercise (exercise.id)}
<li> <li>
<a href="/fitness/{sl.exercises}/{exercise.id}" class="exercise-row"> <a href={resolve('/fitness/[exercises=fitnessExercises]/[id]', { exercises: sl.exercises, id: exercise.id })} class="exercise-row">
<div class="exercise-info"> <div class="exercise-info">
<span class="exercise-name"> <span class="exercise-name">
{exercise.localName} {exercise.localName}
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
/** @param {string | undefined | null} type @param {'en'|'de'} lang */ /** @param {string | undefined | null} type @param {'en'|'de'} lang */
@@ -198,7 +199,7 @@
<h3>{lang === 'en' ? 'Similar Exercises' : 'Ähnliche Übungen'}</h3> <h3>{lang === 'en' ? 'Similar Exercises' : 'Ähnliche Übungen'}</h3>
<div class="similar-scroll"> <div class="similar-scroll">
{#each similar as sim} {#each similar as sim}
<a class="similar-card" href="/fitness/{s.exercises}/{sim.id}"> <a class="similar-card" href={resolve('/fitness/[exercises=fitnessExercises]/[id]', { exercises: s.exercises, id: sim.id })}>
<div class="similar-info"> <div class="similar-info">
<span class="similar-name">{sim.localName}</span> <span class="similar-name">{sim.localName}</span>
<span class="similar-meta">{sim.localBodyPart} · {sim.localEquipment}</span> <span class="similar-meta">{sim.localBodyPart} · {sim.localEquipment}</span>
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page as appPage } from '$app/stores'; import { page as appPage } from '$app/stores';
import ChevronLeft from '@lucide/svelte/icons/chevron-left'; import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right'; import ChevronRight from '@lucide/svelte/icons/chevron-right';
@@ -48,9 +49,15 @@
return d.toLocaleString(lang === 'de' ? 'de-DE' : 'en-US', { month: 'long', year: 'numeric' }); return d.toLocaleString(lang === 'de' ? 'de-DE' : 'en-US', { month: 'long', year: 'numeric' });
} }
const prevHref = $derived(`/fitness/${s.history}/${prevMonth}`); const prevHref = $derived(resolve('/fitness/[history=fitnessHistory]/[[month=fitnessMonth]]', { history: s.history, month: prevMonth }));
const nextHref = $derived(nextMonth && nextMonth === currentYM ? `/fitness/${s.history}` : nextMonth ? `/fitness/${s.history}/${nextMonth}` : null); const nextHref = $derived(
const recentHref = $derived(`/fitness/${s.history}`); nextMonth && nextMonth === currentYM
? resolve('/fitness/[history=fitnessHistory]', { history: s.history })
: nextMonth
? resolve('/fitness/[history=fitnessHistory]/[[month=fitnessMonth]]', { history: s.history, month: nextMonth })
: null
);
const recentHref = $derived(resolve('/fitness/[history=fitnessHistory]', { history: s.history }));
</script> </script>
<svelte:head><title>{t('history_title', lang)} - Bocken</title></svelte:head> <svelte:head><title>{t('history_title', lang)} - Bocken</title></svelte:head>
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto, invalidateAll } from '$app/navigation'; import { goto, invalidateAll } from '$app/navigation';
import ChevronLeft from '@lucide/svelte/icons/chevron-left'; import ChevronLeft from '@lucide/svelte/icons/chevron-left';
@@ -95,9 +96,17 @@
const prevDate = $derived(dateOffset(-1)); const prevDate = $derived(dateOffset(-1));
const nextDate = $derived(dateOffset(1)); const nextDate = $derived(dateOffset(1));
const prevHref = $derived(prevDate === todayStr ? `/fitness/${s.nutrition}` : `/fitness/${s.nutrition}/${prevDate}`); const prevHref = $derived(
const nextHref = $derived(nextDate === todayStr ? `/fitness/${s.nutrition}` : `/fitness/${s.nutrition}/${nextDate}`); prevDate === todayStr
const todayHref = $derived(`/fitness/${s.nutrition}`); ? resolve('/fitness/[nutrition=fitnessNutrition]', { nutrition: s.nutrition })
: resolve('/fitness/[nutrition=fitnessNutrition]/[[date=fitnessDate]]', { nutrition: s.nutrition, date: prevDate })
);
const nextHref = $derived(
nextDate === todayStr
? resolve('/fitness/[nutrition=fitnessNutrition]', { nutrition: s.nutrition })
: resolve('/fitness/[nutrition=fitnessNutrition]/[[date=fitnessDate]]', { nutrition: s.nutrition, date: nextDate })
);
const todayHref = $derived(resolve('/fitness/[nutrition=fitnessNutrition]', { nutrition: s.nutrition }));
// --- Entries --- // --- Entries ---
// svelte-ignore state_referenced_locally // svelte-ignore state_referenced_locally
@@ -635,7 +644,7 @@
const showFabModal = $derived($page.url.searchParams.has('add')); const showFabModal = $derived($page.url.searchParams.has('add'));
let fabMealType = $state('lunch'); let fabMealType = $state('lunch');
const fabHref = $derived(`/fitness/${s.nutrition}?add`); const fabHref = $derived(`${resolve('/fitness/[nutrition=fitnessNutrition]', { nutrition: s.nutrition })}?add`);
function defaultMealType() { function defaultMealType() {
const h = new Date().getHours(); const h = new Date().getHours();
@@ -1343,7 +1352,7 @@
</div> </div>
</div> </div>
{/each} {/each}
<a class="manage-meals-link" href="/fitness/{s.nutrition}/meals"> <a class="manage-meals-link" href={resolve('/fitness/[nutrition=fitnessNutrition]/meals', { nutrition: s.nutrition })}>
<Settings size={13} /> <Settings size={13} />
{isEn ? 'Manage meals' : 'Mahlzeiten verwalten'} {isEn ? 'Manage meals' : 'Mahlzeiten verwalten'}
</a> </a>
@@ -1473,7 +1482,7 @@
{/if} {/if}
</span> </span>
{#if !hasBmrData} {#if !hasBmrData}
<div class="bmr-hint">{isEn ? 'Set profile in' : 'Profil unter'} <a href="/fitness/{s.measure}">{t('measure_title', lang)}</a></div> <div class="bmr-hint">{isEn ? 'Set profile in' : 'Profil unter'} <a href={resolve('/fitness/[checkin=fitnessCheckIn]', { checkin: s.measure })}>{t('measure_title', lang)}</a></div>
{/if} {/if}
</div> </div>
</div> </div>
@@ -1601,7 +1610,7 @@
<span>{isEn <span>{isEn
? 'Your TDEE (Total Daily Energy Expenditure) is the calories you burn per day. Set weight, height, and birth year under' ? 'Your TDEE (Total Daily Energy Expenditure) is the calories you burn per day. Set weight, height, and birth year under'
: 'Dein TDEE (Gesamtenergieumsatz) sind die Kalorien, die du pro Tag verbrauchst. Gewicht, Größe und Geburtsjahr einstellen unter'} : 'Dein TDEE (Gesamtenergieumsatz) sind die Kalorien, die du pro Tag verbrauchst. Gewicht, Größe und Geburtsjahr einstellen unter'}
<a href="/fitness/{s.measure}">{t('measure_title', lang)}</a> <a href={resolve('/fitness/[checkin=fitnessCheckIn]', { checkin: s.measure })}>{t('measure_title', lang)}</a>
</span> </span>
</div> </div>
</div> </div>
@@ -1907,9 +1916,9 @@
{/if} {/if}
<div class="food-card-body"> <div class="food-card-body">
{#if entry.source === 'bls' || entry.source === 'usda' || entry.source === 'off'} {#if entry.source === 'bls' || entry.source === 'usda' || entry.source === 'off'}
<a class="food-card-name food-card-link" draggable="false" href="/fitness/{s.nutrition}/food/{entry.source}/{entry.sourceId}">{entry.name}</a> <a class="food-card-name food-card-link" draggable="false" href={resolve('/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]', { nutrition: s.nutrition, source: entry.source, id: entry.sourceId })}>{entry.name}</a>
{:else if (entry.source === 'recipe' || entry.source === 'custom') && entry.sourceId} {:else if (entry.source === 'recipe' || entry.source === 'custom') && entry.sourceId}
<a class="food-card-name food-card-link" draggable="false" href="/fitness/{s.nutrition}/food/{entry.source}/{entry.sourceId}?logEntry={entry._id}">{entry.name}</a> <a class="food-card-name food-card-link" draggable="false" href={`${resolve('/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]', { nutrition: s.nutrition, source: entry.source, id: entry.sourceId })}?logEntry=${entry._id}`}>{entry.name}</a>
{:else} {:else}
<span class="food-card-name">{entry.name}</span> <span class="food-card-name">{entry.name}</span>
{/if} {/if}
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import ChevronDown from '@lucide/svelte/icons/chevron-down'; import ChevronDown from '@lucide/svelte/icons/chevron-down';
import ExternalLink from '@lucide/svelte/icons/external-link'; import ExternalLink from '@lucide/svelte/icons/external-link';
@@ -206,7 +207,7 @@
<span class="badge badge-nutriscore" data-score={food.nutriscore.toLowerCase()}>Nutri-Score {food.nutriscore.toUpperCase()}</span> <span class="badge badge-nutriscore" data-score={food.nutriscore.toLowerCase()}>Nutri-Score {food.nutriscore.toUpperCase()}</span>
{/if} {/if}
{#if food.recipeSlug} {#if food.recipeSlug}
<a class="badge badge-recipe-link" href="/{isEn ? 'recipes' : 'rezepte'}/{isEn && food.recipeSlugEn ? food.recipeSlugEn : food.recipeSlug}"> <a class="badge badge-recipe-link" href={resolve('/[recipeLang=recipeLang]/[name]', { recipeLang: isEn ? 'recipes' : 'rezepte', name: isEn && food.recipeSlugEn ? food.recipeSlugEn : food.recipeSlug })}>
{isEn ? 'View recipe' : 'Zum Rezept'} <ExternalLink size={12} /> {isEn ? 'View recipe' : 'Zum Rezept'} <ExternalLink size={12} />
</a> </a>
{/if} {/if}
@@ -289,7 +290,7 @@
<div class="ingredient-row"> <div class="ingredient-row">
<div class="ingredient-info"> <div class="ingredient-info">
{#if ing.sourceId && (ing.source === 'bls' || ing.source === 'usda' || ing.source === 'off')} {#if ing.sourceId && (ing.source === 'bls' || ing.source === 'usda' || ing.source === 'off')}
<a class="ingredient-name" href="/fitness/{s.nutrition}/food/{ing.source}/{ing.sourceId}">{ing.name}</a> <a class="ingredient-name" href={resolve('/fitness/[nutrition=fitnessNutrition]/food/[source]/[id]', { nutrition: s.nutrition, source: ing.source, id: ing.sourceId })}>{ing.name}</a>
{:else} {:else}
<span class="ingredient-name">{ing.name}</span> <span class="ingredient-name">{ing.name}</span>
{/if} {/if}
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte'; import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
@@ -293,7 +294,7 @@
<div class="card-value">~{stats.kcalEstimate.kcal.toLocaleString()}<span class="card-unit">kcal</span></div> <div class="card-value">~{stats.kcalEstimate.kcal.toLocaleString()}<span class="card-unit">kcal</span></div>
<div class="card-label">{t('burned', lang)}</div> <div class="card-label">{t('burned', lang)}</div>
{#if !hasDemographics} {#if !hasDemographics}
<div class="card-hint">{t('kcal_set_profile', lang)} <a href="/fitness/{fitnessSlugs(lang).measure}">{t('measure_title', lang)}</a></div> <div class="card-hint">{t('kcal_set_profile', lang)} <a href={resolve('/fitness/[checkin=fitnessCheckIn]', { checkin: fitnessSlugs(lang).measure })}>{t('measure_title', lang)}</a></div>
{/if} {/if}
</div> </div>
{/if} {/if}
@@ -512,7 +513,7 @@
<a <a
class="bp-card" class="bp-card"
style="--accent: {bodyPartAccent(card.key)}" style="--accent: {bodyPartAccent(card.key)}"
href="/fitness/{statsSlug}/{historySlug}/{bodyPartSlug(card, lang)}" href={resolve('/fitness/[stats=fitnessStats]/[history=fitnessHistory]/[part]', { stats: statsSlug, history: historySlug, part: bodyPartSlug(card, lang) })}
> >
<div class="bp-img-wrap" aria-hidden="true"> <div class="bp-img-wrap" aria-hidden="true">
{#if card.img} {#if card.img}
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import ArrowLeft from '@lucide/svelte/icons/arrow-left'; import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import Ruler from '@lucide/svelte/icons/ruler'; import Ruler from '@lucide/svelte/icons/ruler';
@@ -151,7 +152,7 @@
<div class="detail-page"> <div class="detail-page">
<header class="detail-header" style="--accent: {bodyPartAccent(card.key)}"> <header class="detail-header" style="--accent: {bodyPartAccent(card.key)}">
<a class="back-link" href="/fitness/{statsSlug}" aria-label={t('back', lang)}> <a class="back-link" href={resolve('/fitness/[stats=fitnessStats]', { stats: statsSlug })} aria-label={t('back', lang)}>
<ArrowLeft size={18} /> <ArrowLeft size={18} />
</a> </a>
<div class="head-text"> <div class="head-text">
@@ -173,7 +174,7 @@
{#if !hasData} {#if !hasData}
<div class="empty"> <div class="empty">
<p>{t('no_measurements_yet', lang)}</p> <p>{t('no_measurements_yet', lang)}</p>
<a class="cta" href="/fitness/{checkinSlug}/body-parts"> <a class="cta" href={resolve('/fitness/[checkin=fitnessCheckIn]/body-parts', { checkin: checkinSlug })}>
<Ruler size={16} /> {t('measure_body_parts', lang)} <Ruler size={16} /> {t('measure_body_parts', lang)}
</a> </a>
</div> </div>
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Trash2 from '@lucide/svelte/icons/trash-2'; import Trash2 from '@lucide/svelte/icons/trash-2';
@@ -1667,7 +1668,7 @@
exerciseId={activeExercise.exerciseId} exerciseId={activeExercise.exerciseId}
bodyPart={activeExerciseMeta?.localBodyPart ?? null} bodyPart={activeExerciseMeta?.localBodyPart ?? null}
equipment={activeExerciseMeta?.localEquipment ?? null} equipment={activeExerciseMeta?.localEquipment ?? null}
detailsHref={`/fitness/${sl.exercises}/${activeExercise.exerciseId}`} detailsHref={resolve('/fitness/[exercises=fitnessExercises]/[id]', { exercises: sl.exercises, id: activeExercise.exerciseId })}
detailsLabel={isEn ? 'Exercise details' : 'Übungsdetails'} detailsLabel={isEn ? 'Exercise details' : 'Übungsdetails'}
exerciseIndex={activeIdx} exerciseIndex={activeIdx}
totalExercises={workout.exercises.length} totalExercises={workout.exercises.length}
+2 -1
View File
@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import SectionError from '$lib/components/SectionError.svelte'; import SectionError from '$lib/components/SectionError.svelte';
</script> </script>
<SectionError <SectionError
sectionHref="/tasks" sectionHref={resolve('/tasks')}
sectionLabel={{ en: 'Tasks', de: 'Aufgaben' }} sectionLabel={{ en: 'Tasks', de: 'Aufgaben' }}
isEnglish={false} isEnglish={false}
/> />
+3 -2
View File
@@ -1,4 +1,5 @@
<script> <script>
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
import UserHeader from '$lib/components/UserHeader.svelte'; import UserHeader from '$lib/components/UserHeader.svelte';
@@ -20,8 +21,8 @@
<Header> <Header>
{#snippet links()} {#snippet links()}
<ul class="site_header"> <ul class="site_header">
<li style="--active-fill: var(--nord10)"><a href="/tasks" class:active={isActive('/tasks')}><ClipboardList size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">Aufgaben</span></a></li> <li style="--active-fill: var(--nord10)"><a href={resolve('/tasks')} class:active={isActive('/tasks')}><ClipboardList size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">Aufgaben</span></a></li>
<li style="--active-fill: var(--nord13)"><a href="/tasks/rewards" class:active={isActive('/tasks/rewards')}><Trophy size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">Sticker</span></a></li> <li style="--active-fill: var(--nord13)"><a href={resolve('/tasks/rewards')} class:active={isActive('/tasks/rewards')}><Trophy size={16} strokeWidth={1.5} class="nav-icon" /><span class="nav-label">Sticker</span></a></li>
</ul> </ul>
{/snippet} {/snippet}