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:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.50.0",
|
||||
"version": "1.51.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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}]`);
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
);
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import Symbol from "./Symbol.svelte"
|
||||
import ThemeToggle from "./ThemeToggle.svelte"
|
||||
import type { Snippet } from 'svelte';
|
||||
@@ -329,7 +330,7 @@ nav {
|
||||
<div>
|
||||
|
||||
<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}
|
||||
<div class="links-wrapper">
|
||||
{@render links()}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount } from "svelte";
|
||||
import { page } from '$app/stores';
|
||||
import LogIn from '@lucide/svelte/icons/log-in';
|
||||
@@ -153,10 +154,10 @@
|
||||
<p>({user.nickname})</p>
|
||||
<ul>
|
||||
{#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}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,7 +165,7 @@
|
||||
{:else}
|
||||
<a
|
||||
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'}
|
||||
title={lang === 'de' ? 'Anmelden' : 'Login'}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
@@ -243,7 +244,7 @@
|
||||
</div>
|
||||
|
||||
{#if payment}
|
||||
<EditButton href="/{root}/payments/edit/{paymentId}" />
|
||||
<EditButton href={resolve('/[cospendRoot=cospendRoot]/payments/edit/[id]', { cospendRoot: root, id: paymentId })} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import Shield from '@lucide/svelte/icons/shield';
|
||||
import Flame from '@lucide/svelte/icons/flame';
|
||||
|
||||
@@ -18,7 +19,7 @@
|
||||
<a
|
||||
class="case-tab"
|
||||
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" />
|
||||
<span>{l.contra}</span>
|
||||
@@ -26,7 +27,7 @@
|
||||
<a
|
||||
class="case-tab"
|
||||
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" />
|
||||
<span>{l.pro}</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { getEnrichedExerciseById } from '$lib/data/exercisedb';
|
||||
import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n';
|
||||
@@ -14,7 +15,7 @@
|
||||
{#if plain}
|
||||
<span class="exercise-plain">{exercise.localName}</span>
|
||||
{: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}
|
||||
{:else}
|
||||
<span class="exercise-unknown">Unknown Exercise</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { untrack } from 'svelte';
|
||||
@@ -477,7 +478,7 @@
|
||||
<span class="fs-result-cal">{item.calories}<small> kcal</small></span>
|
||||
</button>
|
||||
{#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} />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { getExerciseById, getExerciseMetrics } from '$lib/data/exercises';
|
||||
import Clock from '@lucide/svelte/icons/clock';
|
||||
@@ -152,7 +153,7 @@
|
||||
});
|
||||
</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">
|
||||
<h3 class="session-name">{session.name}</h3>
|
||||
<span class="session-date">{formatDate(session.startTime)} · {formatTime(session.startTime)}</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import "$lib/css/shake.css"
|
||||
let { icon, ...restProps } = $props<{ icon: string, [key: string]: any }>();
|
||||
</script>
|
||||
@@ -26,4 +27,4 @@
|
||||
}
|
||||
|
||||
</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">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { TranslatedRecipeType } from '$types/types';
|
||||
import TranslationFieldComparison from './TranslationFieldComparison.svelte';
|
||||
import CreateIngredientList from '$lib/components/recipes/CreateIngredientList.svelte';
|
||||
@@ -758,7 +759,7 @@ button:disabled {
|
||||
{#each untranslatedBaseRecipes as baseRecipe}
|
||||
<li>
|
||||
<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 →
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import LinksGrid from "$lib/components/LinksGrid.svelte";
|
||||
import { onMount } from 'svelte';
|
||||
let { data } = $props();
|
||||
|
||||
let lang = $state<'de' | 'en'>('de');
|
||||
let recipesUrl = $state('/rezepte');
|
||||
let faithUrl = $state('/glaube');
|
||||
let recipesUrl = $state(resolve('/[recipeLang=recipeLang]', { recipeLang: 'rezepte' }));
|
||||
let faithUrl = $state(resolve('/[faithLang=faithLang]', { faithLang: 'glaube' }));
|
||||
|
||||
onMount(() => {
|
||||
// Check localStorage for preferred language
|
||||
const preferredLanguage = localStorage.getItem('preferredLanguage');
|
||||
if (preferredLanguage === 'en') {
|
||||
lang = 'en';
|
||||
recipesUrl = '/recipes';
|
||||
faithUrl = '/faith';
|
||||
recipesUrl = resolve('/[recipeLang=recipeLang]', { recipeLang: 'recipes' });
|
||||
faithUrl = resolve('/[faithLang=faithLang]', { faithLang: 'faith' });
|
||||
} else {
|
||||
lang = 'de';
|
||||
recipesUrl = '/rezepte';
|
||||
faithUrl = '/glaube';
|
||||
recipesUrl = resolve('/[recipeLang=recipeLang]', { recipeLang: 'rezepte' });
|
||||
faithUrl = resolve('/[faithLang=faithLang]', { faithLang: 'glaube' });
|
||||
}
|
||||
|
||||
// Listen for language changes from UserHeader
|
||||
const handleLanguageChange = (e: CustomEvent) => {
|
||||
lang = e.detail.lang;
|
||||
recipesUrl = lang === 'en' ? '/recipes' : '/rezepte';
|
||||
faithUrl = lang === 'en' ? '/faith' : '/glaube';
|
||||
recipesUrl = resolve('/[recipeLang=recipeLang]', { recipeLang: lang === 'en' ? 'recipes' : 'rezepte' });
|
||||
faithUrl = resolve('/[faithLang=faithLang]', { faithLang: lang === 'en' ? 'faith' : 'glaube' });
|
||||
};
|
||||
window.addEventListener('languagechange', handleLanguageChange as EventListener);
|
||||
|
||||
@@ -151,13 +152,13 @@ section h2{
|
||||
<h3>{labels.shopping}</h3>
|
||||
</a>
|
||||
|
||||
<a href="/fitness">
|
||||
<a href={resolve('/fitness')}>
|
||||
<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>
|
||||
<h3>{labels.fitness}</h3>
|
||||
</a>
|
||||
|
||||
<a href="/tasks">
|
||||
<a href={resolve('/tasks')}>
|
||||
<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>
|
||||
<h3>{labels.tasks}</h3>
|
||||
@@ -169,7 +170,7 @@ section h2{
|
||||
</a>
|
||||
|
||||
|
||||
<a href="/fitness/nutrition">
|
||||
<a href={resolve('/fitness/nutrition')}>
|
||||
<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>
|
||||
<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 -->
|
||||
{#if !data.session}
|
||||
<a href="/auth/signin">
|
||||
<a href={resolve('/auth/signin')}>
|
||||
<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>
|
||||
<h3>{labels.documents}</h3>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import SectionError from '$lib/components/SectionError.svelte';
|
||||
import { detectCospendLang, cospendRoot } from '$lib/js/cospendI18n';
|
||||
@@ -8,7 +9,7 @@
|
||||
</script>
|
||||
|
||||
<SectionError
|
||||
sectionHref={cospendRoot(lang)}
|
||||
sectionHref={resolve('/[cospendRoot=cospendRoot]', { cospendRoot: cospendRoot(lang) })}
|
||||
sectionLabel={{ en: 'Expenses', de: 'Kosten' }}
|
||||
{isEnglish}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -69,12 +70,12 @@
|
||||
{#snippet links()}
|
||||
<ul class="site_header">
|
||||
{#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}
|
||||
<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}
|
||||
<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(--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(--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={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}
|
||||
</ul>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
@@ -137,7 +138,7 @@
|
||||
|
||||
<div class="actions">
|
||||
{#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}
|
||||
</div>
|
||||
|
||||
@@ -186,7 +187,7 @@
|
||||
{#if isSettlementPayment(split.paymentId)}
|
||||
<!-- Settlement Payment Display - User -> User Flow -->
|
||||
<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"
|
||||
onclick={(e) => handlePaymentClick(split.paymentId?._id, e)}
|
||||
>
|
||||
@@ -217,7 +218,7 @@
|
||||
<div class="message-content">
|
||||
<ProfilePicture username={split.paymentId?.paidBy || 'Unknown'} size={36} />
|
||||
<a
|
||||
href="/{root}/payments/view/{split.paymentId?._id}"
|
||||
href={resolve('/[cospendRoot=cospendRoot]/payments/view/[id]', { cospendRoot: root, id: split.paymentId?._id })}
|
||||
class="activity-bubble"
|
||||
onclick={(e) => handlePaymentClick(split.paymentId?._id, e)}
|
||||
>
|
||||
@@ -262,7 +263,7 @@
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<AddButton href="/{root}/payments/add" />
|
||||
<AddButton href={resolve('/[cospendRoot=cospendRoot]/payments/add', { cospendRoot: root })} />
|
||||
|
||||
<style>
|
||||
.cospend-main {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
@@ -148,7 +149,7 @@
|
||||
</svg>
|
||||
<h2>{t('no_payments_yet', lang)}</h2>
|
||||
<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>
|
||||
{:else}
|
||||
@@ -156,7 +157,7 @@
|
||||
{#each payments as payment}
|
||||
{#if isSettlementPayment(payment)}
|
||||
<!-- 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-badge">
|
||||
<span class="settlement-icon">💸</span>
|
||||
@@ -190,7 +191,7 @@
|
||||
</a>
|
||||
{:else}
|
||||
<!-- 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-title-section">
|
||||
<ProfilePicture username={payment.paidBy} size={40} />
|
||||
@@ -279,7 +280,7 @@
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<AddButton href="/{root}/payments/add" />
|
||||
<AddButton href={resolve('/[cospendRoot=cospendRoot]/payments/add', { cospendRoot: root })} />
|
||||
|
||||
<style>
|
||||
.payments-list {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
@@ -152,7 +153,7 @@
|
||||
</main>
|
||||
|
||||
{#if payment}
|
||||
<EditButton href="/{root}/payments/edit/{data.paymentId}" />
|
||||
<EditButton href={resolve('/[cospendRoot=cospendRoot]/payments/edit/[id]', { cospendRoot: root, id: data.paymentId ?? '' })} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount } from 'svelte';
|
||||
import { getCategoryEmoji } from '$lib/utils/categories';
|
||||
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
|
||||
@@ -122,7 +123,7 @@
|
||||
<div class="empty-state">
|
||||
<h2>{t('no_recurring', lang)}</h2>
|
||||
<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>
|
||||
{:else}
|
||||
<div class="payments-grid">
|
||||
@@ -208,7 +209,7 @@
|
||||
</div>
|
||||
|
||||
<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)}
|
||||
</a>
|
||||
<button
|
||||
@@ -232,7 +233,7 @@
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<AddButton href="/{root}/payments/add" />
|
||||
<AddButton href={resolve('/[cospendRoot=cospendRoot]/payments/add', { cospendRoot: root })} />
|
||||
|
||||
<style>
|
||||
.recurring-payments {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import { page } from '$app/stores';
|
||||
@@ -165,7 +166,7 @@
|
||||
<h2>🎉 {t('all_settled', lang)}</h2>
|
||||
<p>{t('no_debts_msg', lang)}</p>
|
||||
<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>
|
||||
{:else}
|
||||
@@ -349,7 +350,7 @@
|
||||
<button type="submit" class="btn btn-settlement">
|
||||
{t('record_settlement', lang)}
|
||||
</button>
|
||||
<a href="/{root}/dash" class="btn btn-secondary">
|
||||
<a href={resolve('/[cospendRoot=cospendRoot]/dash', { cospendRoot: root })} class="btn btn-secondary">
|
||||
{t('cancel', lang)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import SectionError from '$lib/components/SectionError.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let faithLang = $derived($page.params.faithLang);
|
||||
let faithLang = $derived($page.params.faithLang!);
|
||||
let isEnglish = $derived(faithLang === 'faith');
|
||||
let sectionLabel = $derived(
|
||||
faithLang === 'fides'
|
||||
@@ -12,7 +13,7 @@
|
||||
</script>
|
||||
|
||||
<SectionError
|
||||
sectionHref="/{faithLang}"
|
||||
sectionHref={resolve('/[faithLang=faithLang]', { faithLang })}
|
||||
{sectionLabel}
|
||||
{isEnglish}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { asset, resolve } from '$app/paths';
|
||||
import '$lib/css/christ.css';
|
||||
import { page } from '$app/stores';
|
||||
import Header from '$lib/components/Header.svelte'
|
||||
@@ -10,13 +11,20 @@ let { data, children } = $props();
|
||||
const isEnglish = $derived(data.lang === 'en');
|
||||
const isLatin = $derived(data.lang === 'la');
|
||||
const eastertide = isEastertide();
|
||||
const prayersHref = $derived(isLatin ? '/fides/orationes' : `/${data.faithLang}/${isEnglish ? 'prayers' : 'gebete'}`);
|
||||
const rosaryHref = $derived(`/${data.faithLang}/${isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz'}`);
|
||||
const calendarHref = $derived(`/${data.faithLang}/${isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender'}`);
|
||||
const apologetikHref = $derived(isLatin ? '/faith/apologetics' : `/${data.faithLang}/${isEnglish ? 'apologetics' : 'apologetik'}`);
|
||||
const angelusHref = $derived(eastertide
|
||||
? `${prayersHref}/regina-caeli`
|
||||
: `${prayersHref}/angelus`);
|
||||
const prayersSlug = $derived(isLatin ? 'orationes' : isEnglish ? 'prayers' : 'gebete');
|
||||
const prayersHref = $derived(resolve('/[faithLang=faithLang]/[prayers=prayersLang]', { faithLang: data.faithLang, prayers: prayersSlug }));
|
||||
const rosaryHref = $derived(resolve('/[faithLang=faithLang]/[rosary=rosaryLang]', { faithLang: data.faithLang, rosary: isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz' }));
|
||||
const calendarHref = $derived(resolve('/[faithLang=faithLang]/[calendar=calendarLang]', { faithLang: data.faithLang, calendar: isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender' }));
|
||||
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 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 labels = $derived({
|
||||
@@ -38,7 +46,7 @@ function isActive(path) {
|
||||
const prayersActive = $derived(isActive(prayersHref) && !isActive(angelusHref));
|
||||
</script>
|
||||
<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>
|
||||
<Header>
|
||||
{#snippet links()}
|
||||
@@ -50,7 +58,7 @@ const prayersActive = $derived(isActive(prayersHref) && !isActive(angelusHref));
|
||||
{: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>
|
||||
{/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(--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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import LinksGrid from '$lib/components/LinksGrid.svelte';
|
||||
import { isEastertide } from '$lib/js/easter.svelte';
|
||||
let { data } = $props();
|
||||
@@ -8,7 +9,11 @@
|
||||
const prayersPath = $derived(isLatin ? 'orationes' : isEnglish ? 'prayers' : 'gebete');
|
||||
const rosaryPath = $derived(isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz');
|
||||
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 labels = $derived({
|
||||
@@ -81,11 +86,11 @@
|
||||
</p>
|
||||
|
||||
<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>
|
||||
<h3>{labels.prayers}</h3>
|
||||
</a>
|
||||
<a href="/{data.faithLang}/{rosaryPath}">
|
||||
<a href={resolve('/[faithLang=faithLang]/[rosary=rosaryLang]', { faithLang: data.faithLang, rosary: rosaryPath })}>
|
||||
<svg viewBox="0 0 512 512">
|
||||
<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
|
||||
@@ -114,18 +119,18 @@
|
||||
<h3>{labels.rosary}</h3>
|
||||
</a>
|
||||
{#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>
|
||||
<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>
|
||||
</a>
|
||||
{: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>
|
||||
<h3>Angelus</h3>
|
||||
</a>
|
||||
{/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}
|
||||
<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>
|
||||
@@ -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>
|
||||
<h3>{labels.apologetics}</h3>
|
||||
</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>
|
||||
<h3>{labels.calendar}</h3>
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import Shield from '@lucide/svelte/icons/shield';
|
||||
import Flame from '@lucide/svelte/icons/flame';
|
||||
|
||||
@@ -55,7 +56,7 @@
|
||||
</section>
|
||||
|
||||
<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-body">
|
||||
<div class="card-sub">{t.contraSub}</div>
|
||||
@@ -65,7 +66,7 @@
|
||||
</div>
|
||||
</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-body">
|
||||
<div class="card-sub">{t.proSub}</div>
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import CaseTabs from '$lib/components/faith/CaseTabs.svelte';
|
||||
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
|
||||
@@ -181,7 +182,7 @@
|
||||
<article class="arg-row" id="arg-{arg.id}">
|
||||
<a
|
||||
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}
|
||||
></a>
|
||||
<div class="arg-num">
|
||||
@@ -201,7 +202,7 @@
|
||||
{@const a = ARCHETYPES[archId]}
|
||||
<a
|
||||
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}"
|
||||
>
|
||||
<span class="glyph" aria-hidden="true" style="background:{a.color};">
|
||||
|
||||
+4
-3
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
@@ -100,7 +101,7 @@
|
||||
<ApologetikToc title={tocLabel} items={tocItems} activeId={arg.id} />
|
||||
|
||||
<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">
|
||||
{labels.eyebrowPrefix}
|
||||
@@ -122,7 +123,7 @@
|
||||
{@const isActive = id === activeId}
|
||||
{@const isPick = alexPicks.includes(id)}
|
||||
<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"
|
||||
aria-selected={isActive}
|
||||
class="tab"
|
||||
@@ -198,7 +199,7 @@
|
||||
{#each arg.related as rid (rid)}
|
||||
{@const r = data.args.find((x) => x.id === rid)}
|
||||
{#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>
|
||||
{r.title}
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount } from 'svelte';
|
||||
import { POS_LAYER_COLORS, type PosArgument } from '$lib/data/apologetik';
|
||||
import CaseTabs from '$lib/components/faith/CaseTabs.svelte';
|
||||
@@ -174,7 +175,7 @@
|
||||
{@const stroke = POS_LAYER_COLORS[it.layer]}
|
||||
{@const opacity = 0.25 + (it.strength / 5) * 0.55}
|
||||
{@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
|
||||
d="M 38 {it.y} C {W * 0.45} {it.y}, {W * 0.55} {targetY}, {targetX} {targetY}"
|
||||
fill="none"
|
||||
@@ -253,7 +254,7 @@
|
||||
<article class="pos-row" id="pos-{arg.id}">
|
||||
<a
|
||||
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}
|
||||
></a>
|
||||
<div class="pos-num">
|
||||
@@ -286,7 +287,7 @@
|
||||
{@const v = POS_VOICES[vid]}
|
||||
<a
|
||||
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}"
|
||||
>
|
||||
<span class="glyph" aria-hidden="true" style="background:{v.color};"
|
||||
|
||||
+4
-3
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
@@ -106,7 +107,7 @@
|
||||
<ApologetikToc title={tocLabel} items={tocItems} activeId={arg.id} />
|
||||
|
||||
<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}
|
||||
<div class="layer-tag">{layer.sub}</div>
|
||||
@@ -142,7 +143,7 @@
|
||||
{@const v = POS_VOICES[id]}
|
||||
{@const isActive = id === activeId}
|
||||
<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"
|
||||
aria-selected={isActive}
|
||||
class="tab"
|
||||
@@ -203,7 +204,7 @@
|
||||
{#each arg.related as rid (rid)}
|
||||
{@const r = POS_ARGUMENTS.find((x) => x.id === rid)}
|
||||
{#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>
|
||||
{r.title}
|
||||
</a>
|
||||
|
||||
+32
-8
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -103,17 +104,24 @@
|
||||
|
||||
// URL: /{faithLang}/{calendar}/{rite}/{yyyy}/{mm}/{dd} — rite is a required
|
||||
// path segment so day/month nav stays inside the active rite.
|
||||
const riteBase = $derived(`/${page.params.faithLang}/${page.params.calendar}/${rite}`);
|
||||
const calendarBase = $derived(`/${page.params.faithLang}/${page.params.calendar}`);
|
||||
const riteParams = $derived({
|
||||
faithLang: page.params.faithLang!,
|
||||
calendar: page.params.calendar!,
|
||||
rite
|
||||
});
|
||||
|
||||
function dayHref(iso: string) {
|
||||
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) {
|
||||
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
|
||||
@@ -121,12 +129,19 @@
|
||||
const hero = $derived(selected ?? today);
|
||||
|
||||
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 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));
|
||||
@@ -136,14 +151,23 @@
|
||||
// re-applies each rite's default if none is given.
|
||||
function riteHref(r: 'novus' | 'vetus') {
|
||||
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) {
|
||||
const next = (e.currentTarget as HTMLSelectElement).value;
|
||||
const def = rite === 'vetus' ? DEFAULT_DIOCESE_1962 : DEFAULT_DIOCESE_1969;
|
||||
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 });
|
||||
}
|
||||
</script>
|
||||
|
||||
+28
-4
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import { page } from '$app/state';
|
||||
import { browser } from '$app/environment';
|
||||
@@ -37,23 +38,46 @@
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
const riteBase = $derived(`/${page.params.faithLang}/${page.params.calendar}/${rite}`);
|
||||
const dioceseQuery = $derived.by(() => {
|
||||
const q = page.url.searchParams.get('diocese');
|
||||
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
|
||||
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
|
||||
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
|
||||
function shiftDay(days: number): string {
|
||||
const [y, m, d] = iso.split('-').map(Number);
|
||||
const next = new Date(y, m - 1, d);
|
||||
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 nextHref = $derived(shiftDay(1));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { createLanguageContext } from "$lib/contexts/languageContext.js";
|
||||
@@ -171,7 +172,10 @@
|
||||
]);
|
||||
|
||||
// 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)
|
||||
/** @param {string} id */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { asset } from '$app/paths';
|
||||
let { pos, BEAD_SPACING, DECADE_OFFSET, activeSection, decadeCounters } = $props();
|
||||
</script>
|
||||
<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" />
|
||||
|
||||
<!-- 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 -->
|
||||
{#each [1, 2, 3, 4, 5] as d (d)}
|
||||
@@ -125,7 +126,7 @@
|
||||
{/if}
|
||||
{/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 -->
|
||||
<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>
|
||||
import { resolve } from '$app/paths';
|
||||
import LinksGrid from '$lib/components/LinksGrid.svelte';
|
||||
let { data } = $props();
|
||||
const isGerman = $derived(data.lang === 'de');
|
||||
@@ -48,7 +49,7 @@
|
||||
|
||||
<h1>Katechese</h1>
|
||||
{#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}
|
||||
<p>
|
||||
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>
|
||||
|
||||
<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">
|
||||
<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"/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { onMount } from 'svelte';
|
||||
import ArrowDown from '@lucide/svelte/icons/arrow-down';
|
||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
||||
@@ -92,7 +93,7 @@
|
||||
</header>
|
||||
|
||||
{#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}
|
||||
|
||||
<section id="ursprung">
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import SectionError from '$lib/components/SectionError.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let recipeLang = $derived($page.params.recipeLang);
|
||||
let recipeLang = $derived($page.params.recipeLang!);
|
||||
let isEnglish = $derived(recipeLang === 'recipes');
|
||||
</script>
|
||||
|
||||
<SectionError
|
||||
sectionHref="/{recipeLang}"
|
||||
sectionHref={resolve('/[recipeLang=recipeLang]', { recipeLang })}
|
||||
sectionLabel={{ en: 'Recipes', de: 'Rezepte' }}
|
||||
{isEnglish}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import '$lib/css/recipe-links.css';
|
||||
import { page } from '$app/stores';
|
||||
import { onNavigate } from '$app/navigation';
|
||||
@@ -80,14 +81,14 @@ function isActive(path) {
|
||||
<Header>
|
||||
{#snippet links()}
|
||||
<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}
|
||||
<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}
|
||||
<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(--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(--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(--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(--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={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={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={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>
|
||||
{/snippet}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import AddButton from '$lib/components/AddButton.svelte';
|
||||
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
|
||||
@@ -367,7 +368,7 @@
|
||||
<div class="hero-text">
|
||||
<h1>{labels.title}</h1>
|
||||
<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={() => {
|
||||
const img = document.querySelector('.hero-img') as HTMLElement | null;
|
||||
if (img) (img.style as any).viewTransitionName = `recipe-${heroRecipe.short_name}-img`;
|
||||
@@ -435,7 +436,7 @@
|
||||
isFavorite={recipe.isFavorite}
|
||||
showFavoriteIndicator={!!data.session?.user}
|
||||
loading_strat={i < 12 ? "eager" : "lazy"}
|
||||
routePrefix="/{data.recipeLang}"
|
||||
routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -448,7 +449,7 @@
|
||||
</div>
|
||||
</section>
|
||||
{#if !isEnglish}
|
||||
<AddButton href="/rezepte/add"></AddButton>
|
||||
<AddButton href={resolve('/rezepte/add')}></AddButton>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="hero-fallback">
|
||||
@@ -465,7 +466,7 @@
|
||||
isFavorite={recipe.isFavorite}
|
||||
showFavoriteIndicator={!!data.session?.user}
|
||||
loading_strat={i < 12 ? "eager" : "lazy"}
|
||||
routePrefix="/{data.recipeLang}"
|
||||
routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -475,6 +476,6 @@
|
||||
{/if}
|
||||
|
||||
{#if !isEnglish}
|
||||
<AddButton href="/rezepte/add"></AddButton>
|
||||
<AddButton href={resolve('/rezepte/add')}></AddButton>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -55,7 +56,7 @@
|
||||
: error?.details
|
||||
);
|
||||
|
||||
let recipesHref = $derived(isEnglishRoute ? '/recipes' : '/rezepte');
|
||||
let recipesHref = $derived(resolve('/[recipeLang=recipeLang]', { recipeLang: isEnglishRoute ? 'recipes' : 'rezepte' }));
|
||||
|
||||
function viewGermanRecipe() { goto(`/rezepte/${recipeName}`); }
|
||||
function editToTranslate() { goto(`/rezepte/edit/${recipeName}`); }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { writable } from 'svelte/store';
|
||||
export const multiplier = writable(0);
|
||||
|
||||
@@ -318,7 +319,7 @@ h2{
|
||||
<div class=tags>
|
||||
<h2>{labels.season}</h2>
|
||||
{#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]}
|
||||
{months[season[0] - 1]}
|
||||
{/if}
|
||||
@@ -333,7 +334,7 @@ h2{
|
||||
<h2 class="section-label">{labels.keywords}</h2>
|
||||
<div class="tags center">
|
||||
{#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}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -361,4 +362,4 @@ h2{
|
||||
</div>
|
||||
</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">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
|
||||
|
||||
@@ -161,7 +162,7 @@ h1 {
|
||||
<CompactCard
|
||||
{recipe}
|
||||
{current_month}
|
||||
routePrefix="/{data.recipeLang}"
|
||||
routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })}
|
||||
/>
|
||||
<div class="translation-badge {recipe.translationStatus || 'none'}">
|
||||
{#if recipe.translationStatus === 'pending'}
|
||||
@@ -179,7 +180,7 @@ h1 {
|
||||
<div class="empty-state">
|
||||
<p>Alle Rezepte sind übersetzt!</p>
|
||||
<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>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
let { data } = $props<{ data: PageData }>();
|
||||
import TagCloud from '$lib/components/TagCloud.svelte';
|
||||
@@ -18,7 +19,7 @@
|
||||
<section>
|
||||
<TagCloud>
|
||||
{#each data.categories as tag}
|
||||
<TagBall {tag} ref="/{data.recipeLang}/category">
|
||||
<TagBall {tag} ref={resolve('/[recipeLang=recipeLang]/category', { recipeLang: data.recipeLang })}>
|
||||
</TagBall>
|
||||
{/each}
|
||||
</TagCloud>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import type { BriefRecipeType } from '$types/types';
|
||||
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>
|
||||
<div class="recipe-grid">
|
||||
{#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}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
|
||||
import Search from '$lib/components/recipes/Search.svelte';
|
||||
@@ -91,7 +92,7 @@
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<p class="to-try-link"><a href="/{data.recipeLang}/to-try">{labels.toTry} →</a></p>
|
||||
<p class="to-try-link"><a href={resolve('/[recipeLang=recipeLang]/to-try', { recipeLang: data.recipeLang })}>{labels.toTry} →</a></p>
|
||||
|
||||
<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}
|
||||
<div class="recipe-grid">
|
||||
{#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}
|
||||
</div>
|
||||
{:else if data.favorites.length > 0}
|
||||
@@ -110,6 +111,6 @@
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<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>
|
||||
{/if}
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
let { data } = $props<{ data: PageData }>();
|
||||
|
||||
@@ -79,6 +80,6 @@
|
||||
</style>
|
||||
<div class=flex>
|
||||
{#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}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import IconLayout from '$lib/components/recipes/IconLayout.svelte';
|
||||
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
|
||||
@@ -31,11 +32,11 @@
|
||||
<title>{data.icon} - {siteTitle}</title>
|
||||
</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()}
|
||||
<div class="recipe-grid">
|
||||
{#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}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import Search from '$lib/components/recipes/Search.svelte';
|
||||
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
|
||||
@@ -112,7 +113,7 @@
|
||||
{#if displayedRecipes.length > 0}
|
||||
<div class="recipe-grid">
|
||||
{#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}
|
||||
</div>
|
||||
{:else if (data.query || hasActiveSearch) && !data.error}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import SeasonLayout from '$lib/components/recipes/SeasonLayout.svelte'
|
||||
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
|
||||
@@ -38,11 +39,11 @@
|
||||
<title>{labels.title} - {labels.siteTitle}</title>
|
||||
</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()}
|
||||
<div class="recipe-grid">
|
||||
{#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}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import SeasonLayout from '$lib/components/recipes/SeasonLayout.svelte';
|
||||
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
|
||||
@@ -36,11 +37,11 @@
|
||||
<title>{currentMonth} - {siteTitle}</title>
|
||||
</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()}
|
||||
<div class="recipe-grid">
|
||||
{#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}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
let { data } = $props<{ data: PageData }>();
|
||||
import TagCloud from '$lib/components/TagCloud.svelte';
|
||||
@@ -45,7 +46,7 @@
|
||||
<section>
|
||||
<TagCloud>
|
||||
{#each filteredTags as tag}
|
||||
<TagBall {tag} ref="/{data.recipeLang}/tag">
|
||||
<TagBall {tag} ref={resolve('/[recipeLang=recipeLang]/tag', { recipeLang: data.recipeLang })}>
|
||||
</TagBall>
|
||||
{/each}
|
||||
</TagCloud>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import type { BriefRecipeType } from '$types/types';
|
||||
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>
|
||||
<div class="recipe-grid">
|
||||
{#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}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import AddButton from '$lib/components/AddButton.svelte';
|
||||
import Converter from './Converter.svelte';
|
||||
@@ -64,4 +65,4 @@ h1{
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AddButton href="/rezepte/add"></AddButton>
|
||||
<AddButton href={resolve('/rezepte/add')}></AddButton>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import SectionError from '$lib/components/SectionError.svelte';
|
||||
import { detectFitnessLang } from '$lib/js/fitnessI18n';
|
||||
@@ -8,7 +9,7 @@
|
||||
</script>
|
||||
|
||||
<SectionError
|
||||
sectionHref={isEnglish ? '/fitness/workout' : '/fitness/training'}
|
||||
sectionHref={resolve('/fitness/[workout=fitnessWorkout]', { workout: isEnglish ? 'workout' : 'training' })}
|
||||
sectionLabel={{ en: 'Fitness', de: 'Fitness' }}
|
||||
{isEnglish}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
@@ -90,12 +91,12 @@
|
||||
<Header>
|
||||
{#snippet links()}
|
||||
<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 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(--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(--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(--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(--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><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={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={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={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={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={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>
|
||||
{/snippet}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import Pencil from '@lucide/svelte/icons/pencil';
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
@@ -541,7 +542,7 @@
|
||||
</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="muscle-base">{@html bpFrontSvg}</div>
|
||||
<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>
|
||||
</div>
|
||||
<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} />
|
||||
<span class="edit-more-label">{lang === 'en' ? 'Edit all fields' : 'Alle Felder bearbeiten'}</span>
|
||||
<ChevronRight size={11} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import Cable from '@lucide/svelte/icons/cable';
|
||||
@@ -212,7 +213,7 @@
|
||||
<ul class="exercise-list">
|
||||
{#each filtered as exercise (exercise.id)}
|
||||
<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">
|
||||
<span class="exercise-name">
|
||||
{exercise.localName}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
/** @param {string | undefined | null} type @param {'en'|'de'} lang */
|
||||
@@ -198,7 +199,7 @@
|
||||
<h3>{lang === 'en' ? 'Similar Exercises' : 'Ähnliche Übungen'}</h3>
|
||||
<div class="similar-scroll">
|
||||
{#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">
|
||||
<span class="similar-name">{sim.localName}</span>
|
||||
<span class="similar-meta">{sim.localBodyPart} · {sim.localEquipment}</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page as appPage } from '$app/stores';
|
||||
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
||||
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' });
|
||||
}
|
||||
|
||||
const prevHref = $derived(`/fitness/${s.history}/${prevMonth}`);
|
||||
const nextHref = $derived(nextMonth && nextMonth === currentYM ? `/fitness/${s.history}` : nextMonth ? `/fitness/${s.history}/${nextMonth}` : null);
|
||||
const recentHref = $derived(`/fitness/${s.history}`);
|
||||
const prevHref = $derived(resolve('/fitness/[history=fitnessHistory]/[[month=fitnessMonth]]', { history: s.history, month: prevMonth }));
|
||||
const nextHref = $derived(
|
||||
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>
|
||||
|
||||
<svelte:head><title>{t('history_title', lang)} - Bocken</title></svelte:head>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
||||
@@ -95,9 +96,17 @@
|
||||
|
||||
const prevDate = $derived(dateOffset(-1));
|
||||
const nextDate = $derived(dateOffset(1));
|
||||
const prevHref = $derived(prevDate === todayStr ? `/fitness/${s.nutrition}` : `/fitness/${s.nutrition}/${prevDate}`);
|
||||
const nextHref = $derived(nextDate === todayStr ? `/fitness/${s.nutrition}` : `/fitness/${s.nutrition}/${nextDate}`);
|
||||
const todayHref = $derived(`/fitness/${s.nutrition}`);
|
||||
const prevHref = $derived(
|
||||
prevDate === todayStr
|
||||
? 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 ---
|
||||
// svelte-ignore state_referenced_locally
|
||||
@@ -635,7 +644,7 @@
|
||||
const showFabModal = $derived($page.url.searchParams.has('add'));
|
||||
let fabMealType = $state('lunch');
|
||||
|
||||
const fabHref = $derived(`/fitness/${s.nutrition}?add`);
|
||||
const fabHref = $derived(`${resolve('/fitness/[nutrition=fitnessNutrition]', { nutrition: s.nutrition })}?add`);
|
||||
|
||||
function defaultMealType() {
|
||||
const h = new Date().getHours();
|
||||
@@ -1343,7 +1352,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{/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} />
|
||||
{isEn ? 'Manage meals' : 'Mahlzeiten verwalten'}
|
||||
</a>
|
||||
@@ -1473,7 +1482,7 @@
|
||||
{/if}
|
||||
</span>
|
||||
{#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}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1601,7 +1610,7 @@
|
||||
<span>{isEn
|
||||
? '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'}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1907,9 +1916,9 @@
|
||||
{/if}
|
||||
<div class="food-card-body">
|
||||
{#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}
|
||||
<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}
|
||||
<span class="food-card-name">{entry.name}</span>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
||||
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>
|
||||
{/if}
|
||||
{#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} />
|
||||
</a>
|
||||
{/if}
|
||||
@@ -289,7 +290,7 @@
|
||||
<div class="ingredient-row">
|
||||
<div class="ingredient-info">
|
||||
{#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}
|
||||
<span class="ingredient-name">{ing.name}</span>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
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-label">{t('burned', lang)}</div>
|
||||
{#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}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -512,7 +513,7 @@
|
||||
<a
|
||||
class="bp-card"
|
||||
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">
|
||||
{#if card.img}
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
||||
import Ruler from '@lucide/svelte/icons/ruler';
|
||||
@@ -151,7 +152,7 @@
|
||||
|
||||
<div class="detail-page">
|
||||
<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} />
|
||||
</a>
|
||||
<div class="head-text">
|
||||
@@ -173,7 +174,7 @@
|
||||
{#if !hasData}
|
||||
<div class="empty">
|
||||
<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)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
@@ -1667,7 +1668,7 @@
|
||||
exerciseId={activeExercise.exerciseId}
|
||||
bodyPart={activeExerciseMeta?.localBodyPart ?? 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'}
|
||||
exerciseIndex={activeIdx}
|
||||
totalExercises={workout.exercises.length}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import SectionError from '$lib/components/SectionError.svelte';
|
||||
</script>
|
||||
|
||||
<SectionError
|
||||
sectionHref="/tasks"
|
||||
sectionHref={resolve('/tasks')}
|
||||
sectionLabel={{ en: 'Tasks', de: 'Aufgaben' }}
|
||||
isEnglish={false}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import UserHeader from '$lib/components/UserHeader.svelte';
|
||||
@@ -20,8 +21,8 @@
|
||||
<Header>
|
||||
{#snippet links()}
|
||||
<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(--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(--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={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>
|
||||
{/snippet}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user