4 Commits

Author SHA1 Message Date
Alexander 4ad218cc39 i18n(apologetik): rename 'Alex's choice' chip to 'Alex's pick'
CI / update (push) Successful in 4m12s
English label and variable name now match the existing ALEX_PICKS
data convention. German keeps 'Alex' Wahl' (the natural translation).
Latin updated to 'Alexandri delectus' to mirror the pick semantics.
2026-04-30 19:00:14 +02:00
Alexander 3cd2a678a6 refactor: $app/stores → $app/state, legacy stores → runes
Codemod-driven migration of 55 .svelte files from the deprecated
$app/stores module to the rune-based $app/state ($page.x → page.x,
no auto-subscription wrapper). Two custom writable() stores converted
to .svelte.ts factory functions matching the existing theme store
pattern, with consumers updated to use .value getters and the explicit
.set() method.

UserHeader.svelte's login link now guards page.url.search behind
the browser flag — search-param access throws during prerender, and
this defensive change unblocks future prerender adoption on any page
that includes the header.
2026-04-29 22:31:16 +02:00
Alexander e5d218820b 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.
2026-04-29 22:14:29 +02:00
Alexander 70506e169a feat(faith/apologetik): voice routing + Alex's choice chip
- contra/pro detail pages move from #voice-X hash to /[argId]/[archId]
  (and /[posArgId]/[voiceId]) optional path segments. SSR renders the
  selected voice directly — no hydration flash on deep links.
- Tab onclick uses replaceState to update path without a load roundtrip.
- Add Alex's choice chip on contra detail tabs: small circular pfp on
  picks, expanded label on the active tab. ALEX_PICKS map per argument.
- Answer-rail pills on contra index extend past 760px column into the
  right viewport gutter when space allows; wrap otherwise.
2026-04-29 21:32:02 +02:00
98 changed files with 1079 additions and 384 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.49.3",
"version": "1.52.1",
"private": true,
"type": "module",
"scripts": {
+84
View File
@@ -0,0 +1,84 @@
/**
* Migrate `$app/stores` (deprecated) to `$app/state` (rune-based).
*
* For each .svelte file:
* - Rewrite `from '$app/stores'` → `from '$app/state'`
* - For each named import, drop the `$` prefix from auto-subscriptions:
* `$page.url.pathname` → `page.url.pathname`
* `$navigating` → `navigating`
* `$updated` → `updated`
* Aliased imports (`page as appPage`) are tracked, so `$appPage` becomes `appPage`.
*
* Skips:
* - Non-.svelte files (server-only code uses getRequestEvent instead).
* - Files importing other things from $app/stores that don't have a state equivalent
* (none observed in this repo).
*
* Run: pnpm exec vite-node scripts/codemod-app-stores-to-state.ts [--dry]
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import { join, extname } from 'node:path';
const SRC = 'src';
const DRY = process.argv.includes('--dry');
const STORES_IMPORT_RE =
/import\s*\{([^}]+)\}\s*from\s*['"]\$app\/stores['"]\s*;?/;
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;
}
function parseImports(inner: string): Array<{ orig: string; local: string }> {
return inner
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((spec) => {
const m = spec.match(/^(\w+)(?:\s+as\s+(\w+))?$/);
if (!m) return null;
return { orig: m[1], local: m[2] ?? m[1] };
})
.filter((x): x is { orig: string; local: string } => x !== null);
}
function rewriteFile(src: string): { code: string; changed: boolean } {
const m = STORES_IMPORT_RE.exec(src);
if (!m) return { code: src, changed: false };
const imports = parseImports(m[1]);
if (imports.length === 0) return { code: src, changed: false };
// Replace the import path; preserve the same import shape.
let out = src.replace(STORES_IMPORT_RE, (full) =>
full.replace(/['"]\$app\/stores['"]/, "'$app/state'")
);
// Drop `$` prefix from each local name where it appears as a store
// auto-subscription (i.e. $name followed by a non-word boundary).
for (const { local } of imports) {
const re = new RegExp(`\\$${local}\\b`, 'g');
out = out.replace(re, local);
}
return { code: out, changed: out !== src };
}
const files = walk(SRC);
let changed = 0;
for (const f of files) {
const orig = readFileSync(f, 'utf8');
const { code, changed: didChange } = rewriteFile(orig);
if (!didChange) continue;
if (!DRY) writeFileSync(f, code);
changed++;
console.log(` ${f}`);
}
console.log(`\n${DRY ? '[dry] ' : ''}${changed} files migrated`);
+268
View File
@@ -0,0 +1,268 @@
/**
* Bucket 2 codemod: replace template-literal hrefs that start with `/` and
* contain `{expr}` interpolations with `resolve(routeId, { ... })`.
*
* Skips:
* - tags: <link>, <image> (svg), <use>, <textPath>
* - hrefs not starting with `/`
* - hrefs containing `?` or `#` (query/fragment) — handle manually
* - mixed segments like `view-{id}`
* - paths matching 0 or >1 routes
*
* Run: pnpm exec vite-node scripts/codemod-href-resolve-bucket2.ts [--dry] [--verbose]
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import { join, extname } from 'node:path';
const SRC = 'src';
const ROUTES = 'src/routes';
const DRY = process.argv.includes('--dry');
const SKIP_TAGS = new Set(['link', 'image', 'use', 'textpath']);
// --- Route tree ---------------------------------------------------------
type Dir = { name: string; subdirs: Dir[] };
function loadTree(dir: string, name = ''): Dir {
const subdirs: Dir[] = [];
for (const e of readdirSync(dir, { withFileTypes: true })) {
if (!e.isDirectory()) continue;
if (e.name === 'api' || e.name.startsWith('.')) continue;
subdirs.push(loadTree(join(dir, e.name), e.name));
}
return { name, subdirs };
}
const ROUTE_TREE = loadTree(ROUTES);
// --- Path parsing -------------------------------------------------------
type HrefSeg = { kind: 'literal'; text: string } | { kind: 'param'; expr: string };
function hasUnbracedChar(path: string, chars: string): boolean {
let depth = 0;
for (const c of path) {
if (c === '{') depth++;
else if (c === '}') depth--;
else if (depth === 0 && chars.includes(c)) return true;
}
return false;
}
function parsePath(path: string): HrefSeg[] | null {
if (!path.startsWith('/')) return null;
if (hasUnbracedChar(path, '?#')) return null;
if (path.includes('//')) return null;
// Split on `/`, but only outside of {...}
const parts: string[] = [];
let buf = '';
let depth = 0;
for (const c of path.slice(1)) {
if (c === '{') { depth++; buf += c; }
else if (c === '}') { depth--; buf += c; }
else if (c === '/' && depth === 0) { parts.push(buf); buf = ''; }
else buf += c;
}
parts.push(buf);
if (parts.length === 1 && parts[0] === '') return [];
const segs: HrefSeg[] = [];
for (const p of parts) {
if (p === '') return null;
const m = p.match(/^\{([^}]+)\}$/);
if (m) {
segs.push({ kind: 'param', expr: m[1] });
} else if (!p.includes('{') && !p.includes('}')) {
segs.push({ kind: 'literal', text: p });
} else {
return null; // mixed segment
}
}
return segs;
}
function paramInfo(
name: string
): { paramName: string; isRest: boolean } | null {
let body = name;
if (body.startsWith('[[') && body.endsWith(']]')) {
body = body.slice(2, -2);
} else if (body.startsWith('[') && body.endsWith(']')) {
body = body.slice(1, -1);
} else return null;
const isRest = body.startsWith('...');
if (isRest) body = body.slice(3);
const eq = body.indexOf('=');
const paramName = eq >= 0 ? body.slice(0, eq) : body;
return { paramName, isRest };
}
// --- Tree matching ------------------------------------------------------
type Match = { routeId: string; params: Array<[string, string]> };
function matchTree(
dir: Dir,
segs: HrefSeg[],
routePath: string[],
params: Array<[string, string]>
): Match[] {
if (segs.length === 0) {
const id = routePath.length === 0 ? '/' : '/' + routePath.join('/');
return [{ routeId: id, params }];
}
const [seg, ...rest] = segs;
const out: Match[] = [];
for (const sub of dir.subdirs) {
// Route groups are transparent — they don't consume a URL segment
// but DO appear in the route ID.
if (sub.name.startsWith('(') && sub.name.endsWith(')')) {
out.push(...matchTree(sub, segs, [...routePath, sub.name], params));
continue;
}
if (seg.kind === 'literal') {
if (sub.name === seg.text) {
out.push(
...matchTree(sub, rest, [...routePath, sub.name], params)
);
}
} else {
const info = paramInfo(sub.name);
if (info && !info.isRest) {
out.push(
...matchTree(sub, rest, [...routePath, sub.name], [
...params,
[info.paramName, seg.expr]
])
);
}
}
}
return out;
}
// --- Output formatting --------------------------------------------------
function isIdentifier(s: string): boolean {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
}
function formatParams(params: Array<[string, string]>): string {
if (params.length === 0) return '';
const items = params.map(([name, expr]) => {
const trimmed = expr.trim();
if (isIdentifier(trimmed) && trimmed === name) return name;
return `${name}: ${trimmed}`;
});
return `, { ${items.join(', ')} }`;
}
// --- Rewrite ------------------------------------------------------------
const HREF_RE =
/(<([A-Za-z][\w.-]*)\b[^>]*?\s)href="(\/[^"]*\{[^"]*\}[^"]*)"/gs;
type Skip = { path: string; reason: string };
function rewriteHrefs(src: string): {
code: string;
changed: number;
skipped: Skip[];
} {
let changed = 0;
const skipped: Skip[] = [];
const code = src.replace(HREF_RE, (full, prefix, tag, path) => {
if (SKIP_TAGS.has(tag.toLowerCase())) return full;
const segs = parsePath(path);
if (!segs) {
skipped.push({ path, reason: 'unparsable (mixed/query/fragment)' });
return full;
}
const matches = matchTree(ROUTE_TREE, segs, [], []);
if (matches.length === 0) {
skipped.push({ path, reason: 'no route match' });
return full;
}
if (matches.length > 1) {
skipped.push({
path,
reason: `${matches.length} ambiguous matches: ${matches.map((m) => m.routeId).join(' | ')}`
});
return full;
}
const { routeId, params } = matches[0];
changed++;
return `${prefix}href={resolve('${routeId}'${formatParams(params)})}`;
});
return { code, changed, skipped };
}
// --- Import injection ---------------------------------------------------
const SCRIPT_RE = /<script\b([^>]*)>([\s\S]*?)<\/script>/;
const PATHS_IMPORT_RE =
/import\s*\{([^}]*)\}\s*from\s*['"]\$app\/paths['"]\s*;?/;
function ensureResolveImport(src: string): string {
const m = SCRIPT_RE.exec(src);
if (!m) {
return `<script lang="ts">\n\timport { resolve } from '$app/paths';\n</script>\n\n${src}`;
}
const [scriptFull, attrs, body] = m;
const pm = PATHS_IMPORT_RE.exec(body);
if (pm) {
const inner = pm[1];
if (/\bresolve\b/.test(inner)) return src;
const merged = inner.trim().replace(/,?\s*$/, '') + ', resolve';
const newImport = `import { ${merged} } from '$app/paths';`;
const newBody = body.replace(PATHS_IMPORT_RE, newImport);
return src.replace(scriptFull, `<script${attrs}>${newBody}</script>`);
}
const im = body.match(/^([ \t]*)import\b/m);
const indent = im ? im[1] : '\t';
const opening = `<script${attrs}>`;
return src.replace(
scriptFull,
`${opening}\n${indent}import { resolve } from '$app/paths';${body}</script>`
);
}
// --- Driver -------------------------------------------------------------
function walk(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
const s = statSync(p);
if (s.isDirectory()) walk(p, out);
else if (extname(p) === '.svelte') out.push(p);
}
return out;
}
const files = walk(SRC);
let totalFiles = 0;
let totalReplacements = 0;
const allSkipped: Array<{ file: string } & Skip> = [];
for (const f of files) {
const orig = readFileSync(f, 'utf8');
const { code, changed, skipped } = rewriteHrefs(orig);
for (const s of skipped) allSkipped.push({ file: f, ...s });
if (changed === 0) continue;
const final = ensureResolveImport(code);
if (!DRY) writeFileSync(f, final);
totalFiles++;
totalReplacements += changed;
console.log(`${changed.toString().padStart(3)} ${f}`);
}
console.log(
`\n${DRY ? '[dry] ' : ''}${totalReplacements} replacements across ${totalFiles} files`
);
if (allSkipped.length > 0) {
console.log(`\n--- ${allSkipped.length} skipped hrefs ---`);
for (const s of allSkipped) {
console.log(` ${s.file}\n ${s.path} [${s.reason}]`);
}
}
+105
View File
@@ -0,0 +1,105 @@
/**
* Bucket 1 codemod: replace literal href="/path" with href={resolve('/path')}
* in .svelte files, and inject `import { resolve } from '$app/paths'`.
*
* Skips:
* - non-anchor tags: <link>, <image> (svg), <use>
* - external/protocol URLs: http(s)://, //host, mailto:, tel:
* - fragments (#...) and empty values
* - existing dynamic hrefs ({...})
*
* Run: pnpm exec vite-node scripts/codemod-href-resolve.ts [--dry]
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import { join, extname } from 'node:path';
const ROOT = 'src';
const DRY = process.argv.includes('--dry');
const SKIP_TAGS = new Set(['link', 'image', 'use']);
function walk(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
const s = statSync(p);
if (s.isDirectory()) walk(p, out);
else if (extname(p) === '.svelte') out.push(p);
}
return out;
}
/**
* Match: opening of element, then its attributes, then href="/...".
* Group 1 = full prefix incl. tag-name, Group 2 = tag name, Group 3 = path.
*/
// Excludes `{` and `}` so Svelte template interpolations inside the
// attribute value (e.g. href="/{lang}/foo") are NOT treated as literals.
const HREF_RE =
/(<([A-Za-z][\w.-]*)\b[^>]*?\s)href="(\/[^"{}]*)"/gs;
function rewriteHrefs(src: string): { code: string; changed: number } {
let changed = 0;
const code = src.replace(HREF_RE, (full, prefix, tag, path) => {
if (SKIP_TAGS.has(tag.toLowerCase())) return full;
// Skip protocol-relative just in case
if (path.startsWith('//')) return full;
changed++;
return `${prefix}href={resolve('${path}')}`;
});
return { code, changed };
}
const SCRIPT_RE = /<script\b([^>]*)>([\s\S]*?)<\/script>/;
const PATHS_IMPORT_RE =
/import\s*\{([^}]*)\}\s*from\s*['"]\$app\/paths['"]\s*;?/;
function ensureResolveImport(src: string): string {
const scriptMatch = SCRIPT_RE.exec(src);
if (!scriptMatch) {
// No script tag — prepend a TS one.
return `<script lang="ts">\n\timport { resolve } from '$app/paths';\n</script>\n\n${src}`;
}
const [scriptFull, attrs, body] = scriptMatch;
const pathsMatch = PATHS_IMPORT_RE.exec(body);
if (pathsMatch) {
const inner = pathsMatch[1];
if (/\bresolve\b/.test(inner)) return src; // already imported
const merged = inner.trim().replace(/,?\s*$/, '') + ', resolve';
const newImport = `import { ${merged} } from '$app/paths';`;
const newBody = body.replace(PATHS_IMPORT_RE, newImport);
return src.replace(scriptFull, `<script${attrs}>${newBody}</script>`);
}
// Inject new import line. Detect indent from first import line if present.
const importMatch = body.match(/^([ \t]*)import\b/m);
const indent = importMatch ? importMatch[1] : '\t';
// Insert right after the opening script tag's newline.
const opening = `<script${attrs}>`;
const insertion = `\n${indent}import { resolve } from '$app/paths';`;
const newScript = opening + insertion + body + '</script>';
return src.replace(scriptFull, newScript);
}
function processFile(path: string): { changed: number } {
const orig = readFileSync(path, 'utf8');
const { code: rewritten, changed } = rewriteHrefs(orig);
if (changed === 0) return { changed: 0 };
const final = ensureResolveImport(rewritten);
if (!DRY) writeFileSync(path, final);
return { changed };
}
const files = walk(ROOT);
let totalFiles = 0;
let totalReplacements = 0;
for (const f of files) {
const { changed } = processFile(f);
if (changed > 0) {
totalFiles++;
totalReplacements += changed;
console.log(`${changed.toString().padStart(3)} ${f}`);
}
}
console.log(
`\n${DRY ? '[dry] ' : ''}${totalReplacements} replacements across ${totalFiles} files`
);
+2 -2
View File
@@ -1,12 +1,12 @@
<script lang="ts">
import { browser } from '$app/environment';
import { enhance } from '$app/forms';
import { page } from '$app/stores';
import { page } from '$app/state';
import Heart from '@lucide/svelte/icons/heart';
let { recipeId, isFavorite = $bindable(false), isLoggedIn = false } = $props<{ recipeId: string, isFavorite?: boolean, isLoggedIn?: boolean }>();
const recipeLang = $derived($page.url.pathname.split('/')[1] || 'rezepte');
const recipeLang = $derived(page.url.pathname.split('/')[1] || 'rezepte');
let isLoading = $state(false);
+2 -1
View File
@@ -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()}
+9 -9
View File
@@ -1,8 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { recipeTranslationStore } from '$lib/stores/recipeTranslation';
import { languageStore } from '$lib/stores/language';
import { page } from '$app/state';
import { recipeTranslationStore } from '$lib/stores/recipeTranslation.svelte';
import { languageStore } from '$lib/stores/language.svelte';
import { convertFitnessPath } from '$lib/js/fitnessI18n';
import { convertCospendPath } from '$lib/js/cospendI18n';
import { onMount } from 'svelte';
@@ -10,7 +10,7 @@
let { lang = undefined }: { lang?: 'de' | 'en' | 'la' } = $props();
// Use prop for display if provided (SSR-safe), otherwise fall back to store
const displayLang = $derived(lang ?? $languageStore);
const displayLang = $derived(lang ?? languageStore.value);
let currentPath = $state('');
let langButton: HTMLButtonElement;
@@ -33,14 +33,14 @@
};
// Whether the current page is a faith route (show LA option)
const faithPath = $derived(currentPath || $page.url.pathname);
const faithPath = $derived(currentPath || page.url.pathname);
const isFaithRoute = $derived(
faithPath.startsWith('/glaube') || faithPath.startsWith('/faith') || faithPath.startsWith('/fides')
);
$effect(() => {
// Update current language and path when page changes (reactive to browser navigation)
const path = $page.url.pathname;
const path = page.url.pathname;
currentPath = path;
if (path.startsWith('/recipes') || path.startsWith('/faith')) {
@@ -87,7 +87,7 @@
// Compute target paths for each language (used as href for no-JS)
function computeTargetPath(targetLang: 'de' | 'en' | 'la'): string {
const path = currentPath || $page.url.pathname;
const path = currentPath || page.url.pathname;
if (path.startsWith('/glaube') || path.startsWith('/faith') || path.startsWith('/fides')) {
return convertFaithPath(path, targetLang);
@@ -102,7 +102,7 @@
}
// Use translated recipe slugs from page data when available (works during SSR)
const pageData = $page.data;
const pageData = page.data;
if (targetLang === 'en' && path.startsWith('/rezepte')) {
if (pageData?.englishShortName) {
return `/recipes/${pageData.englishShortName}`;
@@ -171,7 +171,7 @@
}
// If we have recipe translation data from store, use the correct short names
const recipeData = $recipeTranslationStore;
const recipeData = recipeTranslationStore.value;
if (recipeData) {
if (lang === 'en' && recipeData.englishShortName) {
await goto(`/recipes/${recipeData.englishShortName}`);
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import ErrorView from './ErrorView.svelte';
import { getErrorTitle, getErrorDescription, errorLabels, pick } from '$lib/js/errorStrings';
@@ -18,8 +18,8 @@
let { sectionHref, sectionLabel, isEnglish: isEnglishProp, extraActions }: Props = $props();
let status = $derived($page.status);
let error = $derived($page.error as any);
let status = $derived(page.status);
let error = $derived(page.error as any);
let bibleQuote = $derived(error?.bibleQuote);
let detectedEnglish = $derived(error?.lang === 'en');
let isEnglish = $derived(isEnglishProp ?? detectedEnglish);
+6 -4
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { onMount } from "svelte";
import { page } from '$app/stores';
import { page } from '$app/state';
import { browser } from '$app/environment';
import LogIn from '@lucide/svelte/icons/log-in';
let { user, recipeLang = 'rezepte', lang = 'de' } = $props();
@@ -153,10 +155,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 +166,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 + (browser ? page.url.search : ''))}`}
aria-label={lang === 'de' ? 'Anmelden' : 'Login'}
title={lang === 'de' ? 'Anmelden' : 'Login'}
>
@@ -1,11 +1,11 @@
<script>
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import ProfilePicture from './ProfilePicture.svelte';
import { formatCurrency } from '$lib/utils/formatters';
import { detectCospendLang, locale, t } from '$lib/js/cospendI18n';
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const loc = $derived(locale(lang));
/**
@@ -1,11 +1,11 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import ProfilePicture from './ProfilePicture.svelte';
import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters';
import { detectCospendLang, locale, t } from '$lib/js/cospendI18n';
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const loc = $derived(locale(lang));
let { initialBalance = null, initialDebtData = null } = $props<{ initialBalance?: any, initialDebtData?: any }>();
@@ -1,7 +1,8 @@
<script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import ProfilePicture from './ProfilePicture.svelte';
import EditButton from '$lib/components/EditButton.svelte';
import { getCategoryEmoji } from '$lib/utils/categories';
@@ -12,9 +13,9 @@
let { paymentId, onclose, onpaymentDeleted } = $props();
// Get session from page store
let session = $derived($page.data?.session);
let session = $derived(page.data?.session);
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
@@ -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,9 +1,9 @@
<script>
import ProfilePicture from './ProfilePicture.svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectCospendLang, t } from '$lib/js/cospendI18n';
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
let {
splitMethod = $bindable('equal'),
+3 -2
View File
@@ -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,11 +1,12 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { getEnrichedExerciseById } from '$lib/data/exercisedb';
import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n';
let { exerciseId, plain = false } = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const exercise = $derived(getEnrichedExerciseById(exerciseId, lang));
const sl = $derived(fitnessSlugs(lang));
</script>
@@ -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>
@@ -9,10 +9,10 @@
import PersonStanding from '@lucide/svelte/icons/person-standing';
import Shapes from '@lucide/svelte/icons/shapes';
import Weight from '@lucide/svelte/icons/weight';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const isEn = $derived(lang === 'en');
/**
+4 -3
View File
@@ -1,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { browser } from '$app/environment';
import { untrack } from 'svelte';
import Heart from '@lucide/svelte/icons/heart';
@@ -49,7 +50,7 @@
initialResults = undefined,
} = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const s = $derived(fitnessSlugs(lang));
const isEn = $derived(lang === 'en');
const btnLabel = $derived(confirmLabel ?? t('log_food', lang));
@@ -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,6 +1,6 @@
<script>
import { detectFitnessLang } from '$lib/js/fitnessI18n';
import { page } from '$app/stores';
import { page } from '$app/state';
import Beef from '@lucide/svelte/icons/beef';
import Droplet from '@lucide/svelte/icons/droplet';
import Wheat from '@lucide/svelte/icons/wheat';
@@ -31,7 +31,7 @@
showDetailRows = true,
} = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const isEn = $derived(lang === 'en');
const macroPercent = $derived.by(() => {
@@ -1,5 +1,5 @@
<script>
import { page } from '$app/stores';
import { page } from '$app/state';
import { onMount } from 'svelte';
import { detectFitnessLang } from '$lib/js/fitnessI18n';
import frontSvgRaw from '$lib/assets/muscle-front.svg?raw';
@@ -13,7 +13,7 @@
/** @type {{ data?: { totals?: Record<string, MuscleTotals> } | null }} */
let { data } = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const isEn = $derived(lang === 'en');
/** @type {Record<string, MuscleTotals>} */
const totals = $derived(data?.totals ?? {});
@@ -1,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { getExerciseById, getExerciseMetrics } from '$lib/data/exercises';
import Clock from '@lucide/svelte/icons/clock';
import Weight from '@lucide/svelte/icons/weight';
@@ -9,7 +10,7 @@
import Flame from '@lucide/svelte/icons/flame';
import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const sl = $derived(fitnessSlugs(lang));
/**
@@ -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)} &middot; {formatTime(session.startTime)}</span>
+2 -2
View File
@@ -5,10 +5,10 @@
import Square from '@lucide/svelte/icons/square';
import { METRIC_LABELS } from '$lib/data/exercises';
import RestTimer from './RestTimer.svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
/**
* @type {{
@@ -2,10 +2,10 @@
import { getExerciseById } from '$lib/data/exercises';
import EllipsisVertical from '@lucide/svelte/icons/ellipsis-vertical';
import MapPin from '@lucide/svelte/icons/map-pin';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
/**
* @type {{
+2 -2
View File
@@ -4,10 +4,10 @@ import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import SyncIndicator from '$lib/components/fitness/SyncIndicator.svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
let { href, elapsed = '0:00', paused = false, syncStatus = 'idle', onPauseToggle,
restSeconds = 0, restTotal = 0, onRestAdjust = null, onRestSkip = null } = $props();
@@ -1,7 +1,7 @@
<script>
import { browser } from '$app/environment';
import { enhance } from '$app/forms';
import { page } from '$app/stores';
import { page } from '$app/state';
let { item, multiplier = 1, yeastId = 0, lang = 'de' } = $props();
@@ -11,7 +11,7 @@
: 'Zwischen Frischhefe und Trockenhefe wechseln');
// Get all current URL parameters to preserve state
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : $page.url.searchParams);
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : page.url.searchParams);
/** @param {Event} event */
function toggleHefe(event) {
+2 -1
View File
@@ -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>
@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { onNavigate } from "$app/navigation";
import { browser } from '$app/environment';
import { page } from '$app/stores';
import { page } from '$app/state';
import HefeSwapper from './HefeSwapper.svelte';
import NutritionSummary from './NutritionSummary.svelte';
import AddToFoodLogButton from './AddToFoodLogButton.svelte';
@@ -272,7 +272,7 @@ const yeastIds = $derived.by(() => {
});
// Get all current URL parameters to preserve state in multiplier forms
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : $page.url.searchParams);
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : page.url.searchParams);
// Progressive enhancement - use JS if available
onMount(() => {
@@ -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>
+26
View File
@@ -1581,6 +1581,32 @@ export function findArgument(id: string): Argument | undefined {
return ARGUMENTS.find((a) => a.id === id);
}
// Alex's curated picks per argument (archetype IDs). Same across locales.
export const ALEX_PICKS: Record<string, string[]> = {
evil: ["aquinas"],
evidence: ["logician", "pascal"],
science: ["logician", "lewis"],
morality: ["logician"],
"religion-violence": ["logician"],
miracles: ["logician", "pascal"],
hiddenness: ["pascal"],
hell: ["aquinas", "lewis"],
birth: ["lewis", "justin"],
bible: ["augustine", "newman"],
scale: ["chesterton"],
"natural-evil": ["lewis", "catechism"],
"many-gods": ["newman"],
neuroscience: ["mystic"],
prayer: ["lewis", "aquinas"],
pleasure: ["catechism"],
projection: ["lewis", "chesterton"],
"faith-reason": ["aquinas", "lewis"],
mythicism: ["historian"],
corruption: ["pastor", "chesterton"],
intelligence: ["logician"],
submission: ["lewis"],
};
// Locale-aware accessors. DE comes from auto-generated apologetik.de.ts;
// EN is the source of truth. Latin falls back to EN since DeepL doesn't
// support it — fill in apologetik.la.ts manually if/when desired.
+12
View File
@@ -0,0 +1,12 @@
export type Language = 'de' | 'en';
function createLanguage() {
let value = $state<Language>('de');
return {
get value() { return value; },
set: (v: Language) => { value = v; }
};
}
export const languageStore = createLanguage();
-27
View File
@@ -1,27 +0,0 @@
import { writable } from 'svelte/store';
type Language = 'de' | 'en';
function createLanguageStore() {
const { subscribe, set } = writable<Language>('de');
return {
subscribe,
set,
init: () => {
if (typeof window !== 'undefined') {
const path = window.location.pathname;
if (path.startsWith('/recipes') || path.startsWith('/faith')) {
set('en');
} else if (path.startsWith('/rezepte') || path.startsWith('/glaube')) {
set('de');
} else {
const preferredLanguage = localStorage.getItem('preferredLanguage');
set(preferredLanguage === 'en' ? 'en' : 'de');
}
}
}
};
}
export const languageStore = createLanguageStore();
@@ -0,0 +1,16 @@
export interface RecipeTranslationData {
germanShortName: string;
englishShortName?: string;
hasEnglishTranslation: boolean;
}
function createRecipeTranslation() {
let value = $state<RecipeTranslationData | null>(null);
return {
get value() { return value; },
set: (v: RecipeTranslationData | null) => { value = v; }
};
}
export const recipeTranslationStore = createRecipeTranslation();
-9
View File
@@ -1,9 +0,0 @@
import { writable } from 'svelte/store';
interface RecipeTranslationData {
germanShortName: string;
englishShortName?: string;
hasEnglishTranslation: boolean;
}
export const recipeTranslationStore = writable<RecipeTranslationData | null>(null);
+13 -12
View File
@@ -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>
+3 -3
View File
@@ -1,12 +1,12 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import Header from '$lib/components/Header.svelte';
import ErrorView from '$lib/components/ErrorView.svelte';
import { getErrorTitle, getErrorDescription, errorLabels, pick } from '$lib/js/errorStrings';
let status = $derived($page.status);
let error = $derived($page.error as any);
let status = $derived(page.status);
let error = $derived(page.error as any);
let bibleQuote = $derived(error?.bibleQuote);
let isEnglish = $derived(error?.lang === 'en');
@@ -1,14 +1,15 @@
<script lang="ts">
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import SectionError from '$lib/components/SectionError.svelte';
import { detectCospendLang, cospendRoot } from '$lib/js/cospendI18n';
let lang = $derived(detectCospendLang($page.url.pathname));
let lang = $derived(detectCospendLang(page.url.pathname));
let isEnglish = $derived(lang === 'en');
</script>
<SectionError
sectionHref={cospendRoot(lang)}
sectionHref={resolve('/[cospendRoot=cospendRoot]', { cospendRoot: cospendRoot(lang) })}
sectionLabel={{ en: 'Expenses', de: 'Kosten' }}
{isEnglish}
/>
@@ -1,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { fly } from 'svelte/transition';
@@ -16,7 +17,7 @@
let { data, children } = $props();
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const labels = $derived(cospendLabels(lang));
@@ -28,9 +29,9 @@
$effect(() => {
// Check if URL contains payment view route OR if we have paymentId in state
const match = $page.url.pathname.match(/\/(cospend|expenses)\/payments\/view\/([^\/]+)/);
const statePaymentId = $page.state?.paymentId;
const isOnDashboard = $page.route.id === '/[cospendRoot=cospendRoot]/dash';
const match = page.url.pathname.match(/\/(cospend|expenses)\/payments\/view\/([^\/]+)/);
const statePaymentId = page.state?.paymentId;
const isOnDashboard = page.route.id === '/[cospendRoot=cospendRoot]/dash';
// Only show modal if we're on the dashboard AND have a payment to show
if (isOnDashboard && (match || statePaymentId)) {
@@ -48,14 +49,14 @@
paymentId = null;
// Dispatch a custom event to trigger dashboard refresh
if ($page.route.id === '/[cospendRoot=cospendRoot]/dash') {
if (page.route.id === '/[cospendRoot=cospendRoot]/dash') {
window.dispatchEvent(new CustomEvent('dashboardRefresh'));
}
}
/** @param {string} path */
function isActive(path) {
const currentPath = $page.url.pathname;
const currentPath = page.url.pathname;
// Exact match for dash
if (path.endsWith('/dash')) {
return currentPath === path || currentPath === path + '/';
@@ -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,6 +1,7 @@
<script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { invalidateAll } from '$app/navigation';
import { pushState } from '$app/navigation';
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
@@ -16,7 +17,7 @@
import { detectCospendLang, cospendRoot, t, locale, paymentCategoryName } from '$lib/js/cospendI18n';
let { data } = $props(); // Contains session data and balance from server
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
@@ -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 {
@@ -34,7 +34,7 @@
import Copy from '@lucide/svelte/icons/copy';
import Check from '@lucide/svelte/icons/check';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectCospendLang, t, locale, categoryName, formatTTL as formatTTLi18n, ttlOptions } from '$lib/js/cospendI18n';
let { data } = $props();
@@ -50,7 +50,7 @@
}
});
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const loc = $derived(locale(lang));
/** @type {Record<string, { icon: typeof Plus, color: string }>} */
@@ -355,7 +355,7 @@
/** @param {{ id: string, token: string }} tok */
async function copyTokenLink(tok) {
const root = $page.url.pathname.split('/')[1];
const root = page.url.pathname.split('/')[1];
const url = new URL(`/${root}/list`, window.location.origin);
url.searchParams.set('token', tok.token);
await navigator.clipboard.writeText(url.toString());
@@ -1,7 +1,8 @@
<script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
import { getCategoryEmoji } from '$lib/utils/categories';
import { toast } from '$lib/js/toast.svelte';
@@ -13,7 +14,7 @@
import { formatCurrency } from '$lib/utils/formatters';
let { data } = $props();
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
@@ -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,7 +1,7 @@
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import { enhance } from '$app/forms';
import { detectCospendLang, cospendRoot, locale, t, getCategoryOptionsI18n, frequencyDescription } from '$lib/js/cospendI18n';
import { PREDEFINED_USERS, isPredefinedUsersMode } from '$lib/config/users';
@@ -16,7 +16,7 @@
let { data, form } = $props();
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
@@ -1,7 +1,7 @@
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectCospendLang, cospendRoot, locale, t, getCategoryOptionsI18n } from '$lib/js/cospendI18n';
import { confirm } from '$lib/js/confirmDialog.svelte';
import FormSection from '$lib/components/FormSection.svelte';
@@ -15,7 +15,7 @@
let { data } = $props();
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
@@ -1,7 +1,8 @@
<script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
import { getCategoryEmoji } from '$lib/utils/categories';
import EditButton from '$lib/components/EditButton.svelte';
@@ -12,7 +13,7 @@
let { data } = $props();
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
@@ -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';
@@ -7,12 +8,12 @@
import AddButton from '$lib/components/AddButton.svelte';
import { formatCurrency } from '$lib/utils/formatters';
import Toggle from '$lib/components/Toggle.svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectCospendLang, cospendRoot, t, locale, paymentCategoryName, frequencyDescription, formatNextExecutionI18n } from '$lib/js/cospendI18n';
let { data } = $props();
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
@@ -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,7 +1,7 @@
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import { detectCospendLang, cospendRoot, locale, t, getCategoryOptionsI18n, frequencyDescription } from '$lib/js/cospendI18n';
import { PREDEFINED_USERS, isPredefinedUsersMode } from '$lib/config/users';
import { validateCronExpression, calculateNextExecutionDate } from '$lib/utils/recurring';
@@ -13,7 +13,7 @@
let { data } = $props();
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
@@ -1,7 +1,8 @@
<script>
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import { enhance } from '$app/forms';
import { page } from '$app/stores';
import { page } from '$app/state';
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
import { PREDEFINED_USERS, isPredefinedUsersMode } from '$lib/config/users';
import { detectCospendLang, cospendRoot, t, locale } from '$lib/js/cospendI18n';
@@ -10,7 +11,7 @@
let { data, form } = $props();
const lang = $derived(detectCospendLang($page.url.pathname));
const lang = $derived(detectCospendLang(page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
@@ -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';
import { page } from '$app/state';
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}
/>
+19 -11
View File
@@ -1,6 +1,7 @@
<script>
import { asset, resolve } from '$app/paths';
import '$lib/css/christ.css';
import { page } from '$app/stores';
import { page } from '$app/state';
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.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({
@@ -31,14 +39,14 @@ const typedLang = $derived(/** @type {'de' | 'en'} */ (data.lang));
/** @param {string} path */
function isActive(path) {
const currentPath = $page.url.pathname;
const currentPath = page.url.pathname;
return currentPath.startsWith(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>
+12 -7
View File
@@ -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>
@@ -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}#voice-{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};">
@@ -496,6 +497,15 @@
gap: 8px;
align-items: center;
margin-top: 12px;
width: max-content;
max-width: 100%;
}
@media (min-width: 760px) {
.answer-rail {
/* Extend past the 760px content column into the right gutter when space allows.
arg-body left = 50vw - 278px; available right width capped 24px from viewport edge. */
max-width: min(calc(100vw - 126px), calc(50vw + 254px));
}
}
.answer-rail .label {
font-size: 0.72rem;
@@ -1,16 +0,0 @@
import { error } from '@sveltejs/kit';
import { findArgumentLang, getArchetypes, getArguments } from '$lib/data/apologetik';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, parent }) => {
const { lang } = await parent();
const [arg, archetypes, args] = await Promise.all([
findArgumentLang(params.argId, lang),
getArchetypes(lang),
getArguments(lang)
]);
if (!arg) {
error(404, 'Argument not found');
}
return { argument: arg, archetypes, args };
};
@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { resolve } from '$app/paths';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
let { data } = $props();
@@ -9,6 +9,10 @@
const isGerman = $derived(data?.lang === 'de');
const arg = $derived(data.argument);
const ARCHETYPES = $derived(data.archetypes);
const alexPicks = $derived<string[]>(data.alexPicks ?? []);
const alexPickLabel = $derived(
isLatin ? 'Alexandri delectus' : isGerman ? "Alex' Wahl" : "Alex's pick"
);
const tocLabel = $derived(
isLatin ? 'Obiectiones' : isGerman ? 'Einwände' : 'Objections'
@@ -24,10 +28,13 @@
);
const archIds = $derived(Object.keys(arg.counters));
let userSelected = $state<string | null>(null);
const activeId = $derived(
userSelected && archIds.includes(userSelected) ? userSelected : (archIds[0] ?? '')
);
let selectedByArg = $state<Record<string, string>>({});
const activeId = $derived.by(() => {
const sel = selectedByArg[arg.id];
if (sel && archIds.includes(sel)) return sel;
if (data.initialArchId && archIds.includes(data.initialArchId)) return data.initialArchId;
return archIds[0] ?? '';
});
const arch = $derived(ARCHETYPES[activeId]);
const counter = $derived(arg.counters[activeId]);
@@ -72,21 +79,12 @@
}
function selectArch(id: string) {
userSelected = id;
selectedByArg = { ...selectedByArg, [arg.id]: id };
if (typeof window !== 'undefined') {
history.replaceState(null, '', `#voice-${id}`);
history.replaceState(null, '', `/${faithLang}/${slug}/contra/${arg.id}/${id}`);
}
}
onMount(() => {
const hash = window.location.hash;
if (hash.startsWith('#voice-')) {
const id = hash.slice('#voice-'.length);
if (archIds.includes(id)) {
userSelected = id;
}
}
});
</script>
<svelte:head>
@@ -103,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}
@@ -123,12 +121,14 @@
{#each archIds as id (id)}
{@const a = ARCHETYPES[id]}
{@const isActive = id === activeId}
{@const isPick = alexPicks.includes(id)}
<a
href="#voice-{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"
class:active={isActive}
class:has-pick={isPick}
style:border-bottom-color={isActive ? a.colorHex : 'transparent'}
onclick={(e) => {
e.preventDefault();
@@ -137,6 +137,20 @@
>
<span class="glyph" aria-hidden="true" style="background:{a.color};">{a.glyph}</span>
<span>{a.name}</span>
{#if isPick}
<span class="alex-mark" aria-label={alexPickLabel}>
<img
class="alex-pfp"
src="https://bocken.org/static/user/thumb/alexander.webp"
alt=""
loading="lazy"
decoding="async"
width="14"
height="14"
/>
<span class="alex-label">{alexPickLabel}</span>
</span>
{/if}
</a>
{/each}
</div>
@@ -185,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>
@@ -345,6 +359,59 @@
color: var(--color-text-primary);
}
.alex-mark {
position: absolute;
top: -4px;
right: -6px;
z-index: 2;
display: inline-flex;
align-items: center;
border-radius: var(--radius-pill);
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
padding: 1px;
box-shadow: var(--shadow-sm);
pointer-events: none;
transform: translateY(0);
transition:
padding var(--transition-normal),
transform var(--transition-normal);
}
.alex-pfp {
width: 14px;
height: 14px;
border-radius: 50%;
object-fit: cover;
display: block;
flex: none;
}
.alex-label {
display: inline-block;
white-space: nowrap;
font-size: 0.62rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
max-width: 0;
opacity: 0;
margin-left: 0;
overflow: hidden;
color: var(--color-text-primary);
transition:
max-width var(--transition-normal),
opacity var(--transition-fast),
margin-left var(--transition-normal);
}
.tab.active.has-pick .alex-mark {
padding: 1px 8px 1px 1px;
transform: translateY(-8px);
}
.tab.active.has-pick .alex-label {
max-width: 140px;
opacity: 1;
margin-left: 5px;
}
.glyph {
display: inline-flex;
align-items: center;
@@ -0,0 +1,27 @@
import { error } from '@sveltejs/kit';
import { ALEX_PICKS, findArgumentLang, getArchetypes, getArguments } from '$lib/data/apologetik';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, parent }) => {
const { lang } = await parent();
const [arg, archetypes, args] = await Promise.all([
findArgumentLang(params.argId, lang),
getArchetypes(lang),
getArguments(lang)
]);
if (!arg) {
error(404, 'Argument not found');
}
const archIds = Object.keys(arg.counters);
if (params.archId && !archIds.includes(params.archId)) {
error(404, 'Voice not found');
}
const initialArchId = params.archId ?? null;
return {
argument: arg,
archetypes,
args,
alexPicks: ALEX_PICKS[params.argId] ?? [],
initialArchId
};
};
@@ -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}#voice-{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};"
@@ -21,6 +21,12 @@ export const load: PageServerLoad = async ({ params, parent }) => {
error(404, 'Argument not found');
}
const voiceIds = Object.keys(arg.voices);
if (params.voiceId && !voiceIds.includes(params.voiceId)) {
error(404, 'Voice not found');
}
const initialVoiceId = params.voiceId ?? null;
const lng: 'en' | 'de' = lang === 'de' ? 'de' : 'en';
const enArg = EN_POS_ARGUMENTS.find((x) => x.id === arg.id);
const argument = enArg
@@ -40,5 +46,5 @@ export const load: PageServerLoad = async ({ params, parent }) => {
return { ...a, scripture: resolved.text ? resolved : a.scripture };
});
return { argument, voices, layers, args: argsWithScripture };
return { argument, voices, layers, args: argsWithScripture, initialVoiceId };
};
@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { resolve } from '$app/paths';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
let { data } = $props();
@@ -35,10 +35,13 @@
);
const voiceIds = $derived(Object.keys(arg.voices));
let userSelected = $state<string | null>(null);
const activeId = $derived(
userSelected && voiceIds.includes(userSelected) ? userSelected : (voiceIds[0] ?? '')
);
let selectedByArg = $state<Record<string, string>>({});
const activeId = $derived.by(() => {
const sel = selectedByArg[arg.id];
if (sel && voiceIds.includes(sel)) return sel;
if (data.initialVoiceId && voiceIds.includes(data.initialVoiceId)) return data.initialVoiceId;
return voiceIds[0] ?? '';
});
const voice = $derived(POS_VOICES[activeId]);
const counter = $derived(arg.voices[activeId]);
@@ -83,21 +86,11 @@
}
function selectVoice(id: string) {
userSelected = id;
selectedByArg = { ...selectedByArg, [arg.id]: id };
if (typeof window !== 'undefined') {
history.replaceState(null, '', `#voice-${id}`);
history.replaceState(null, '', `/${faithLang}/${slug}/pro/${arg.id}/${id}`);
}
}
onMount(() => {
const hash = window.location.hash;
if (hash.startsWith('#voice-')) {
const id = hash.slice('#voice-'.length);
if (voiceIds.includes(id)) {
userSelected = id;
}
}
});
</script>
<svelte:head>
@@ -114,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>
@@ -150,7 +143,7 @@
{@const v = POS_VOICES[id]}
{@const isActive = id === activeId}
<a
href="#voice-{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"
@@ -211,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>
@@ -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>
@@ -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,13 +1,14 @@
<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';
import { page } from '$app/stores';
import { page } from '$app/state';
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
/** @type {number | string | null} */
let expanded = $state(null);
const isGerman = $derived($page.url.pathname.startsWith('/glaube'));
const isLatin = $derived($page.url.pathname.startsWith('/fides'));
const isGerman = $derived(page.url.pathname.startsWith('/glaube'));
const isLatin = $derived(page.url.pathname.startsWith('/fides'));
/** @param {number | string} id */
function toggle(id) {
@@ -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';
import { page } from '$app/state';
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,6 +1,7 @@
<script>
import { resolve } from '$app/paths';
import '$lib/css/recipe-links.css';
import { page } from '$app/stores';
import { page } from '$app/state';
import { onNavigate } from '$app/navigation';
import Header from '$lib/components/Header.svelte'
@@ -67,7 +68,7 @@ const labels = $derived({
/** @param {string} path */
function isActive(path) {
const currentPath = $page.url.pathname;
const currentPath = page.url.pathname;
// Exact match for recipe lang root
if (path === `/${data.recipeLang}`) {
return currentPath === `/${data.recipeLang}` || currentPath === `/${data.recipeLang}/`;
@@ -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,15 +1,16 @@
<script lang="ts">
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import ErrorView from '$lib/components/ErrorView.svelte';
import { getErrorTitle, getErrorDescription, errorLabels, pick } from '$lib/js/errorStrings';
let status = $derived($page.status);
let error = $derived($page.error as any);
let recipeLang = $derived($page.params.recipeLang);
let recipeName = $derived($page.params.name);
let user = $derived($page.data?.session?.user);
let status = $derived(page.status);
let error = $derived(page.error as any);
let recipeLang = $derived(page.params.recipeLang);
let recipeName = $derived(page.params.name);
let user = $derived(page.data?.session?.user);
let isEnglishRoute = $derived(recipeLang === 'recipes');
let isEnglish = $derived(error?.lang === 'en' || isEnglishRoute);
@@ -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);
@@ -12,7 +13,7 @@
import RecipeNote from '$lib/components/recipes/RecipeNote.svelte';
import FavoriteButton from '$lib/components/FavoriteButton.svelte';
import { onDestroy } from 'svelte';
import { recipeTranslationStore } from '$lib/stores/recipeTranslation';
import { recipeTranslationStore } from '$lib/stores/recipeTranslation.svelte';
let { data } = $props<{ data: PageData }>();
@@ -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} &rarr;</a></p>
<p class="to-try-link"><a href={resolve('/[recipeLang=recipeLang]/to-try', { recipeLang: data.recipeLang })}>{labels.toTry} &rarr;</a></p>
<Search favoritesOnly={true} lang={data.lang} recipes={data.favorites} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
@@ -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,7 +1,7 @@
<script lang="ts">
import { onMount, tick } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
let { data } = $props();
@@ -13,7 +13,7 @@ let { data } = $props();
onMount(() => {
// Only proceed if we're actually offline or have a redirect target
// This prevents issues if someone navigates here directly while online
const targetUrl = $page.url.searchParams.get('redirect');
const targetUrl = page.url.searchParams.get('redirect');
if (!targetUrl) {
// No redirect target - just go to main recipe list
@@ -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>
+4 -3
View File
@@ -1,14 +1,15 @@
<script lang="ts">
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import SectionError from '$lib/components/SectionError.svelte';
import { detectFitnessLang } from '$lib/js/fitnessI18n';
let lang = $derived(detectFitnessLang($page.url.pathname));
let lang = $derived(detectFitnessLang(page.url.pathname));
let isEnglish = $derived(lang === 'en');
</script>
<SectionError
sectionHref={isEnglish ? '/fitness/workout' : '/fitness/training'}
sectionHref={resolve('/fitness/[workout=fitnessWorkout]', { workout: isEnglish ? 'workout' : 'training' })}
sectionLabel={{ en: 'Fitness', de: 'Fitness' }}
{isEnglish}
/>
+16 -15
View File
@@ -1,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { onMount, onDestroy } from 'svelte';
import Header from '$lib/components/Header.svelte';
import UserHeader from '$lib/components/UserHeader.svelte';
@@ -22,7 +23,7 @@
const workout = getWorkout();
const sync = getWorkoutSync();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const s = $derived(fitnessSlugs(lang));
const labels = $derived(fitnessLabels(lang));
@@ -62,22 +63,22 @@
/** @param {string} path */
function isActive(path) {
const currentPath = $page.url.pathname;
const currentPath = page.url.pathname;
return currentPath.startsWith(path);
}
const activePath = $derived(`/fitness/${s.workout}/${s.active}`);
const isOnActivePage = $derived($page.url.pathname === activePath);
const isOnActivePage = $derived(page.url.pathname === activePath);
const isNutritionPage = $derived(
$page.url.pathname.startsWith(`/fitness/${s.nutrition}`) &&
!$page.url.pathname.startsWith(`/fitness/${s.nutrition}/food`) &&
!$page.url.pathname.startsWith(`/fitness/${s.nutrition}/meals`)
page.url.pathname.startsWith(`/fitness/${s.nutrition}`) &&
!page.url.pathname.startsWith(`/fitness/${s.nutrition}/food`) &&
!page.url.pathname.startsWith(`/fitness/${s.nutrition}/meals`)
);
const isMeasureIndex = $derived(
/^\/fitness\/(check-in|erfassung)\/?$/.test($page.url.pathname)
/^\/fitness\/(check-in|erfassung)\/?$/.test(page.url.pathname)
);
const isExercisesIndex = $derived(
/^\/fitness\/(exercises|uebungen)\/?$/.test($page.url.pathname)
/^\/fitness\/(exercises|uebungen)\/?$/.test(page.url.pathname)
);
/** @param {number} secs */
function formatElapsed(secs) {
@@ -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,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import Pencil from '@lucide/svelte/icons/pencil';
import Trash2 from '@lucide/svelte/icons/trash-2';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
@@ -27,7 +28,7 @@
`viewBox="0 ${BP_VIEW_TOP} 660.46 ${BP_VIEW_H}"`
);
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const checkinSlug = $derived(lang === 'en' ? 'check-in' : 'erfassung');
import { getWorkout } from '$lib/js/workout.svelte';
import PeriodTracker from '$lib/components/fitness/PeriodTracker.svelte';
@@ -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,5 +1,5 @@
<script>
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import Minus from '@lucide/svelte/icons/minus';
import Plus from '@lucide/svelte/icons/plus';
@@ -23,7 +23,7 @@
let { data } = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const checkinSlug = $derived(lang === 'en' ? 'check-in' : 'erfassung');
/** @typedef {{ key: string, labelKey: string, img: string | null, paired: boolean, tipKey: string, dbSingle?: string, dbLeft?: string, dbRight?: string }} Step */
@@ -1,5 +1,5 @@
<script>
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte';
@@ -8,7 +8,7 @@
import SaveFab from '$lib/components/SaveFab.svelte';
import DatePicker from '$lib/components/DatePicker.svelte';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const checkinSlug = $derived(lang === 'en' ? 'check-in' : 'erfassung');
let { data } = $props();
@@ -1,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import Search from '@lucide/svelte/icons/search';
import Cable from '@lucide/svelte/icons/cable';
import Cog from '@lucide/svelte/icons/cog';
@@ -15,7 +16,7 @@
import { MUSCLE_GROUPS, MUSCLE_GROUP_DE } from '$lib/data/muscleMap';
import MuscleFilter from '$lib/components/fitness/MuscleFilter.svelte';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const isEn = $derived(lang === 'en');
const sl = $derived(fitnessSlugs(lang));
@@ -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,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
/** @param {string | undefined | null} type @param {'en'|'de'} lang */
function exerciseTypeInfo(type, lang) {
@@ -21,7 +22,7 @@
}
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const s = $derived(fitnessSlugs(lang));
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
import MuscleMap from '$lib/components/fitness/MuscleMap.svelte';
@@ -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,11 +1,12 @@
<script>
import { page as appPage } from '$app/stores';
import { resolve } from '$app/paths';
import { page as appPage } from '$app/state';
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import SessionCard from '$lib/components/fitness/SessionCard.svelte';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($appPage.url.pathname));
const lang = $derived(detectFitnessLang(appPage.url.pathname));
const s = $derived(fitnessSlugs(lang));
let { data } = $props();
@@ -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,6 +1,6 @@
<script>
import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import Clock from '@lucide/svelte/icons/clock';
import Weight from '@lucide/svelte/icons/weight';
import Trophy from '@lucide/svelte/icons/trophy';
@@ -20,7 +20,7 @@
import { confirm } from '$lib/js/confirmDialog.svelte';
import { toast } from '$lib/js/toast.svelte';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const sl = $derived(fitnessSlugs(lang));
import { getExerciseById, getExerciseMetrics, METRIC_LABELS } from '$lib/data/exercises';
import { formatPaceRangeLabel, formatPaceValue } from '$lib/data/cardioPrRanges';
@@ -1,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { goto, invalidateAll } from '$app/navigation';
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
@@ -69,7 +70,7 @@
* }} FoodSelection
*/
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const s = $derived(fitnessSlugs(lang));
const isEn = $derived(lang === 'en');
@@ -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
@@ -632,10 +641,10 @@
let inlineTab = $state('search'); // 'search' | 'favorites' | 'meals'
// --- FAB modal (route-based via ?add param) ---
const showFabModal = $derived($page.url.searchParams.has('add'));
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,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import ChevronDown from '@lucide/svelte/icons/chevron-down';
import ExternalLink from '@lucide/svelte/icons/external-link';
import Heart from '@lucide/svelte/icons/heart';
@@ -13,7 +14,7 @@
let { data } = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const s = $derived(fitnessSlugs(lang));
const isEn = $derived(lang === 'en');
@@ -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,5 +1,5 @@
<script>
import { page } from '$app/stores';
import { page } from '$app/state';
import { untrack } from 'svelte';
import Plus from '@lucide/svelte/icons/plus';
import Trash2 from '@lucide/svelte/icons/trash-2';
@@ -14,7 +14,7 @@
/** @typedef {import('$models/CustomMeal').ICustomMeal & { _id?: string }} Meal */
/** @typedef {import('$models/CustomMeal').ICustomMealIngredient} MealIngredient */
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const s = $derived(fitnessSlugs(lang));
const isEn = $derived(lang === 'en');
@@ -1,6 +1,7 @@
<script>
import { resolve } from '$app/paths';
import { invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
import MuscleHeatmap from '$lib/components/fitness/MuscleHeatmap.svelte';
import Dumbbell from '@lucide/svelte/icons/dumbbell';
@@ -22,7 +23,7 @@
import StatsRingGraph from '$lib/components/fitness/StatsRingGraph.svelte';
import { BODY_PART_CARDS, bodyPartSlug, bodyPartAccent } from '$lib/js/fitnessBodyParts';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const statsSlug = $derived(lang === 'en' ? 'stats' : 'statistik');
const historySlug = $derived(lang === 'en' ? 'history' : 'verlauf');
@@ -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}
@@ -1,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import Ruler from '@lucide/svelte/icons/ruler';
import TrendingUp from '@lucide/svelte/icons/trending-up';
@@ -11,7 +12,7 @@
let { data } = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const statsSlug = $derived(lang === 'en' ? 'stats' : 'statistik');
const checkinSlug = $derived(lang === 'en' ? 'check-in' : 'erfassung');
const card = $derived(data.card);
@@ -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,6 +1,6 @@
<script>
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import { onMount } from 'svelte';
import Plus from '@lucide/svelte/icons/plus';
import Trash2 from '@lucide/svelte/icons/trash-2';
@@ -23,7 +23,7 @@
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const sl = $derived(fitnessSlugs(lang));
import { getExerciseById, getExerciseMetrics, METRIC_LABELS } from '$lib/data/exercises';
import TemplateCard from '$lib/components/fitness/TemplateCard.svelte';
@@ -1,6 +1,7 @@
<script>
import { resolve } from '$app/paths';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import Trash2 from '@lucide/svelte/icons/trash-2';
import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
@@ -24,7 +25,7 @@
import { confirm } from '$lib/js/confirmDialog.svelte';
import { toast } from '$lib/js/toast.svelte';
const lang = $derived(detectFitnessLang($page.url.pathname));
const lang = $derived(detectFitnessLang(page.url.pathname));
const isEn = $derived(lang === 'en');
const sl = $derived(fitnessSlugs(lang));
import { getWorkout } from '$lib/js/workout.svelte';
@@ -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}
+2 -1
View File
@@ -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}
/>
+5 -4
View File
@@ -1,5 +1,6 @@
<script>
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import Header from '$lib/components/Header.svelte';
import UserHeader from '$lib/components/UserHeader.svelte';
import ClipboardList from '@lucide/svelte/icons/clipboard-list';
@@ -9,7 +10,7 @@
/** @param {string} path */
function isActive(path) {
const currentPath = $page.url.pathname;
const currentPath = page.url.pathname;
if (path === '/tasks') {
return currentPath === '/tasks' || currentPath === '/tasks/';
}
@@ -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}