faith: progressive enhancement for all faith pages without JS
All checks were successful
CI / update (push) Successful in 1m29s

- Rosary: mystery selection, luminous toggle, and latin toggle fall back
  to URL params (?mystery=, ?luminous=, ?latin=) for no-JS navigation
- Prayers/Angelus: latin toggle uses URL param fallback
- Search on prayers page hidden without JS (requires DOM queries)
- Toggle component supports href prop for link-based no-JS self-submit
- LanguageSelector uses <a> links with computed paths and :focus-within
  dropdown for no-JS; displays correct language via server-provided prop
- Recipe language links use translated slugs from $page.data
- URL params cleaned via replaceState after hydration to avoid clutter
This commit is contained in:
2026-02-04 14:14:11 +01:00
parent 1c100a4534
commit 7d6a80442a
13 changed files with 347 additions and 90 deletions

View File

@@ -180,14 +180,14 @@ const mysteryTitlesEnglish = {
]
};
// Toggle for including Luminous mysteries
let includeLuminous = $state(true);
// Toggle for including Luminous mysteries (initialized from URL param or default)
let includeLuminous = $state(data.initialLuminous);
// Flag to prevent saving before we've loaded from localStorage
let hasLoadedFromStorage = false;
// Create language context for prayer components (LanguageToggle will use this)
const langContext = createLanguageContext({ urlLang: data.lang });
const langContext = createLanguageContext({ urlLang: data.lang, initialLatin: data.initialLatin });
// Update lang store when data.lang changes (e.g., after navigation)
$effect(() => {
@@ -268,10 +268,9 @@ function getMysteryForWeekday(date, includeLuminous) {
}
}
// Determine which mystery to use based on current weekday
const initialMystery = getMysteryForWeekday(new Date(), true); // Use literal true to avoid capturing reactive state
let selectedMystery = $state(initialMystery);
let todaysMystery = $state(initialMystery); // Track today's auto-selected mystery
// Use server-computed initial values (supports no-JS via URL params)
let selectedMystery = $state(data.initialMystery);
let todaysMystery = $state(data.todaysMystery);
// Derive these values from selectedMystery so they update automatically
let currentMysteries = $derived(mysteries[selectedMystery]);
@@ -285,6 +284,23 @@ function selectMystery(mysteryType) {
selectedMystery = mysteryType;
}
// Build URLs preserving full state (for no-JS fallback)
function buildHref({ mystery = selectedMystery, luminous = includeLuminous, latin = data.initialLatin } = {}) {
const params = new URLSearchParams();
params.set('mystery', mystery);
if (!luminous) params.set('luminous', '0');
if (!latin) params.set('latin', '0');
return `?${params.toString()}`;
}
function mysteryHref(mystery) {
return buildHref({ mystery });
}
// Toggle hrefs navigate to opposite state (for no-JS self-submit)
let luminousToggleHref = $derived(buildHref({ luminous: !includeLuminous }));
let latinToggleHref = $derived(buildHref({ latin: !data.initialLatin }));
// When luminous toggle changes, update today's mystery and fix invalid selection
$effect(() => {
todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
@@ -385,16 +401,24 @@ for (let d = 1; d < 5; d++) {
const pos = sectionPositions;
onMount(() => {
// Load toggle state from localStorage
const savedIncludeLuminous = localStorage.getItem('rosary_includeLuminous');
if (savedIncludeLuminous !== null) {
includeLuminous = savedIncludeLuminous === 'true';
// Load toggle state from localStorage only if not overridden by URL params
if (!data.hasUrlLuminous) {
const savedIncludeLuminous = localStorage.getItem('rosary_includeLuminous');
if (savedIncludeLuminous !== null) {
includeLuminous = savedIncludeLuminous === 'true';
}
}
// Recalculate mystery based on loaded includeLuminous value
todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
selectMystery(todaysMystery);
// If no mystery was specified in URL, recompute based on loaded preferences
if (!data.hasUrlMystery) {
todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
selectMystery(todaysMystery);
}
// Clean up URL params after hydration (state is now in component state)
if (window.location.search) {
history.replaceState({}, '', window.location.pathname);
}
// Now allow saving to localStorage
hasLoadedFromStorage = true;
@@ -1095,6 +1119,8 @@ h1 {
align-items: center;
gap: 1rem;
position: relative;
text-decoration: none;
color: inherit;
}
@media(prefers-color-scheme: light) {
@@ -1266,49 +1292,53 @@ h1 {
<h2 style="text-align:center;">{labels.mysteries}</h2>
<!-- Mystery Selector -->
<!-- Mystery Selector (links for no-JS, enhanced with onclick for JS) -->
<div class="mystery-selector" class:four-mysteries={includeLuminous}>
<button
<a
class="mystery-button"
class:selected={selectedMystery === 'freudenreich'}
onclick={() => selectMystery('freudenreich')}
href={mysteryHref('freudenreich')}
onclick={(e) => { e.preventDefault(); selectMystery('freudenreich'); }}
>
{#if todaysMystery === 'freudenreich'}
<span class="today-badge">{labels.today}</span>
{/if}
<MysteryIcon type="joyful" />
<h3>{labels.joyful}</h3>
</button>
</a>
<button
<a
class="mystery-button"
class:selected={selectedMystery === 'schmerzhaften'}
onclick={() => selectMystery('schmerzhaften')}
href={mysteryHref('schmerzhaften')}
onclick={(e) => { e.preventDefault(); selectMystery('schmerzhaften'); }}
>
{#if todaysMystery === 'schmerzhaften'}
<span class="today-badge">{labels.today}</span>
{/if}
<MysteryIcon type="sorrowful" />
<h3>{labels.sorrowful}</h3>
</button>
</a>
<button
<a
class="mystery-button"
class:selected={selectedMystery === 'glorreichen'}
onclick={() => selectMystery('glorreichen')}
href={mysteryHref('glorreichen')}
onclick={(e) => { e.preventDefault(); selectMystery('glorreichen'); }}
>
{#if todaysMystery === 'glorreichen'}
<span class="today-badge">{labels.today}</span>
{/if}
<MysteryIcon type="glorious" />
<h3>{labels.glorious}</h3>
</button>
</a>
{#if includeLuminous}
<button
<a
class="mystery-button"
class:selected={selectedMystery === 'lichtreichen'}
onclick={() => selectMystery('lichtreichen')}
href={mysteryHref('lichtreichen')}
onclick={(e) => { e.preventDefault(); selectMystery('lichtreichen'); }}
>
{#if todaysMystery === 'lichtreichen'}
<span class="today-badge">{labels.today}</span>
@@ -1316,7 +1346,7 @@ h1 {
<MysteryIcon type="luminous" />
<h3>{labels.luminous}</h3>
</button>
</a>
{/if}
</div>
@@ -1324,14 +1354,19 @@ h1 {
<div class="controls-row">
<StreakCounter streakData={data.streakData} lang={data.lang} />
<div class="toggle-controls">
<!-- Luminous Mysteries Toggle -->
<!-- Luminous Mysteries Toggle (link for no-JS, enhanced with onclick for JS) -->
<Toggle
bind:checked={includeLuminous}
label={labels.includeLuminous}
href={luminousToggleHref}
/>
<!-- Language Toggle -->
<LanguageToggle />
<!-- Language Toggle (link for no-JS, enhanced with onclick for JS) -->
<LanguageToggle
initialLatin={data.initialLatin}
hasUrlLatin={data.hasUrlLatin}
href={latinToggleHref}
/>
</div>
</div>