rosary: progressive enhancement for no-JS browsers
All checks were successful
CI / update (push) Successful in 1m33s
All checks were successful
CI / update (push) Successful in 1m33s
SVG beads are now anchor links to prayer sections, with CSS :has(:target) highlighting the active bead. Inline mystery images render in each decade by default and hide when JS takes over. StreakCounter uses a form action fallback for logged-in users and hides entirely for anonymous no-JS users. Show images toggle now works via ?images= URL param like the other toggles.
This commit is contained in:
@@ -10,9 +10,10 @@ let streak = $state<ReturnType<typeof getRosaryStreak> | null>(null);
|
|||||||
interface Props {
|
interface Props {
|
||||||
streakData?: { length: number; lastPrayed: string | null } | null;
|
streakData?: { length: number; lastPrayed: string | null } | null;
|
||||||
lang?: 'de' | 'en';
|
lang?: 'de' | 'en';
|
||||||
|
isLoggedIn?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { streakData = null, lang = 'de' }: Props = $props();
|
let { streakData = null, lang = 'de', isLoggedIn = false }: Props = $props();
|
||||||
|
|
||||||
const isEnglish = $derived(lang === 'en');
|
const isEnglish = $derived(lang === 'en');
|
||||||
|
|
||||||
@@ -42,23 +43,25 @@ async function pray() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="streak-container">
|
<div class="streak-container" class:no-js-hidden={!isLoggedIn}>
|
||||||
<div class="streak-display">
|
<div class="streak-display">
|
||||||
<StreakAura value={displayLength} {burst} />
|
<StreakAura value={displayLength} {burst} />
|
||||||
<span class="streak-label">{labels.days}</span>
|
<span class="streak-label">{labels.days}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<form method="POST" action="?/pray" onsubmit={(e) => { e.preventDefault(); pray(); }}>
|
||||||
class="streak-button"
|
<button
|
||||||
onclick={pray}
|
class="streak-button"
|
||||||
disabled={prayedToday}
|
type="submit"
|
||||||
aria-label={labels.ariaLabel}
|
disabled={prayedToday}
|
||||||
>
|
aria-label={labels.ariaLabel}
|
||||||
{#if prayedToday}
|
>
|
||||||
{labels.prayedToday}
|
{#if prayedToday}
|
||||||
{:else}
|
{labels.prayedToday}
|
||||||
{labels.prayed}
|
{:else}
|
||||||
{/if}
|
{labels.prayed}
|
||||||
</button>
|
{/if}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -119,6 +122,15 @@ async function pray() {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide for non-logged-in users without JS (no form action available) */
|
||||||
|
.no-js-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js-enabled) .no-js-hidden {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
.streak-button:disabled {
|
.streak-button:disabled {
|
||||||
background: var(--nord4);
|
background: var(--nord4);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mysteryVerseDataDe, mysteryVerseDataEn } from '$lib/data/mysteryVerseData';
|
import { mysteryVerseDataDe, mysteryVerseDataEn } from '$lib/data/mysteryVerseData';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
|
|
||||||
interface StreakData {
|
interface StreakData {
|
||||||
length: number;
|
length: number;
|
||||||
@@ -43,13 +43,16 @@ export const load: PageServerLoad = async ({ url, fetch, locals, params }) => {
|
|||||||
const luminousParam = url.searchParams.get('luminous');
|
const luminousParam = url.searchParams.get('luminous');
|
||||||
const latinParam = url.searchParams.get('latin');
|
const latinParam = url.searchParams.get('latin');
|
||||||
const mysteryParam = url.searchParams.get('mystery');
|
const mysteryParam = url.searchParams.get('mystery');
|
||||||
|
const imagesParam = url.searchParams.get('images');
|
||||||
|
|
||||||
const hasUrlLuminous = luminousParam !== null;
|
const hasUrlLuminous = luminousParam !== null;
|
||||||
const hasUrlLatin = latinParam !== null;
|
const hasUrlLatin = latinParam !== null;
|
||||||
const hasUrlMystery = mysteryParam !== null;
|
const hasUrlMystery = mysteryParam !== null;
|
||||||
|
const hasUrlImages = imagesParam !== null;
|
||||||
|
|
||||||
const initialLuminous = hasUrlLuminous ? luminousParam !== '0' : true;
|
const initialLuminous = hasUrlLuminous ? luminousParam !== '0' : true;
|
||||||
const initialLatin = hasUrlLatin ? latinParam !== '0' : true;
|
const initialLatin = hasUrlLatin ? latinParam !== '0' : true;
|
||||||
|
const initialShowImages = hasUrlImages ? imagesParam !== '0' : true;
|
||||||
|
|
||||||
const todaysMystery = getMysteryForWeekday(new Date(), initialLuminous);
|
const todaysMystery = getMysteryForWeekday(new Date(), initialLuminous);
|
||||||
|
|
||||||
@@ -78,12 +81,46 @@ export const load: PageServerLoad = async ({ url, fetch, locals, params }) => {
|
|||||||
return {
|
return {
|
||||||
mysteryDescriptions: params.faithLang === 'faith' ? mysteryVerseDataEn : mysteryVerseDataDe,
|
mysteryDescriptions: params.faithLang === 'faith' ? mysteryVerseDataEn : mysteryVerseDataDe,
|
||||||
streakData,
|
streakData,
|
||||||
|
isLoggedIn: !!session?.user?.nickname,
|
||||||
initialMystery,
|
initialMystery,
|
||||||
todaysMystery,
|
todaysMystery,
|
||||||
initialLuminous,
|
initialLuminous,
|
||||||
initialLatin,
|
initialLatin,
|
||||||
hasUrlMystery,
|
hasUrlMystery,
|
||||||
hasUrlLuminous,
|
hasUrlLuminous,
|
||||||
hasUrlLatin
|
hasUrlLatin,
|
||||||
|
initialShowImages,
|
||||||
|
hasUrlImages
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
pray: async ({ locals, fetch }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
if (!session?.user?.nickname) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch('/api/glaube/rosary-streak');
|
||||||
|
const current = res.ok ? await res.json() : { length: 0, lastPrayed: null };
|
||||||
|
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
if (current.lastPrayed === today) {
|
||||||
|
return { success: true, alreadyPrayed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const newLength = current.lastPrayed === yesterdayStr ? current.length + 1 : 1;
|
||||||
|
|
||||||
|
await fetch('/api/glaube/rosary-streak', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ length: newLength, lastPrayed: today })
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ let { data } = $props();
|
|||||||
// Toggle for including Luminous mysteries (initialized from URL param or default)
|
// Toggle for including Luminous mysteries (initialized from URL param or default)
|
||||||
let includeLuminous = $state(data.initialLuminous);
|
let includeLuminous = $state(data.initialLuminous);
|
||||||
|
|
||||||
// Toggle for showing mystery images
|
// Toggle for showing mystery images (initialized from URL param or default)
|
||||||
let showImages = $state(true);
|
let showImages = $state(data.initialShowImages);
|
||||||
|
|
||||||
// Flag to prevent saving before we've loaded from localStorage
|
// Flag to prevent saving before we've loaded from localStorage
|
||||||
let hasLoadedFromStorage = $state(false);
|
let hasLoadedFromStorage = $state(false);
|
||||||
@@ -76,11 +76,12 @@ function selectMystery(mysteryType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build URLs preserving full state (for no-JS fallback)
|
// Build URLs preserving full state (for no-JS fallback)
|
||||||
function buildHref({ mystery = selectedMystery, luminous = includeLuminous, latin = data.initialLatin } = {}) {
|
function buildHref({ mystery = selectedMystery, luminous = includeLuminous, latin = data.initialLatin, images = showImages } = {}) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('mystery', mystery);
|
params.set('mystery', mystery);
|
||||||
if (!luminous) params.set('luminous', '0');
|
if (!luminous) params.set('luminous', '0');
|
||||||
if (!latin) params.set('latin', '0');
|
if (!latin) params.set('latin', '0');
|
||||||
|
if (!images) params.set('images', '0');
|
||||||
return `?${params.toString()}`;
|
return `?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ function mysteryHref(mystery) {
|
|||||||
// Toggle hrefs navigate to opposite state (for no-JS self-submit)
|
// Toggle hrefs navigate to opposite state (for no-JS self-submit)
|
||||||
let luminousToggleHref = $derived(buildHref({ luminous: !includeLuminous }));
|
let luminousToggleHref = $derived(buildHref({ luminous: !includeLuminous }));
|
||||||
let latinToggleHref = $derived(buildHref({ latin: !data.initialLatin }));
|
let latinToggleHref = $derived(buildHref({ latin: !data.initialLatin }));
|
||||||
|
let imagesToggleHref = $derived(buildHref({ images: !showImages }));
|
||||||
|
|
||||||
// When luminous toggle changes, update today's mystery and fix invalid selection
|
// When luminous toggle changes, update today's mystery and fix invalid selection
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -264,9 +266,11 @@ onMount(() => {
|
|||||||
includeLuminous = savedIncludeLuminous === 'true';
|
includeLuminous = savedIncludeLuminous === 'true';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const savedShowImages = localStorage.getItem('rosary_showImages');
|
if (!data.hasUrlImages) {
|
||||||
if (savedShowImages !== null) {
|
const savedShowImages = localStorage.getItem('rosary_showImages');
|
||||||
showImages = savedShowImages === 'true';
|
if (savedShowImages !== null) {
|
||||||
|
showImages = savedShowImages === 'true';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no mystery was specified in URL, recompute based on loaded preferences
|
// If no mystery was specified in URL, recompute based on loaded preferences
|
||||||
@@ -283,6 +287,9 @@ onMount(() => {
|
|||||||
// Now allow saving to localStorage
|
// Now allow saving to localStorage
|
||||||
hasLoadedFromStorage = true;
|
hasLoadedFromStorage = true;
|
||||||
|
|
||||||
|
// Mark JS as active so no-JS fallback images hide
|
||||||
|
document.documentElement.classList.add('js-enabled');
|
||||||
|
|
||||||
// PiP resize handler — show/hide when crossing the breakpoint
|
// PiP resize handler — show/hide when crossing the breakpoint
|
||||||
const onPipResize = () => {
|
const onPipResize = () => {
|
||||||
if (!rosaryPipEl) return;
|
if (!rosaryPipEl) return;
|
||||||
@@ -306,6 +313,7 @@ onMount(() => {
|
|||||||
return () => {
|
return () => {
|
||||||
cleanupScrollSync();
|
cleanupScrollSync();
|
||||||
window.removeEventListener('resize', onPipResize);
|
window.removeEventListener('resize', onPipResize);
|
||||||
|
document.documentElement.classList.remove('js-enabled');
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -407,6 +415,7 @@ onMount(() => {
|
|||||||
|
|
||||||
.prayer-section {
|
.prayer-section {
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
|
scroll-margin-top: 3rem;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
background: var(--accent-dark);
|
background: var(--accent-dark);
|
||||||
@@ -415,6 +424,40 @@ onMount(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* No-JS: highlight SVG beads when a prayer section is :target */
|
||||||
|
.rosary-layout:has(#start1:target) :global(.bead[data-section="start1"]),
|
||||||
|
.rosary-layout:has(#start2:target) :global(.bead[data-section="start2"]),
|
||||||
|
.rosary-layout:has(#start3:target) :global(.bead[data-section="start3"]),
|
||||||
|
.rosary-layout:has(#secret1:target) :global(.bead[data-section="secret1"]),
|
||||||
|
.rosary-layout:has(#secret2:target) :global(.bead[data-section="secret2"]),
|
||||||
|
.rosary-layout:has(#secret3:target) :global(.bead[data-section="secret3"]),
|
||||||
|
.rosary-layout:has(#secret4:target) :global(.bead[data-section="secret4"]),
|
||||||
|
.rosary-layout:has(#secret5:target) :global(.bead[data-section="secret5"]),
|
||||||
|
.rosary-layout:has(#final_salve:target) :global(.bead[data-section="final_salve"]),
|
||||||
|
.rosary-layout:has(#final_schlussgebet:target) :global(.bead[data-section="final_schlussgebet"]),
|
||||||
|
.rosary-layout:has(#final_michael:target) :global(.bead[data-section="final_michael"]) {
|
||||||
|
fill: var(--nord11) !important;
|
||||||
|
filter: drop-shadow(0 0 8px var(--nord11));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rosary-layout:has(#lbead1:target) :global(.large-bead[data-section="lbead1"]),
|
||||||
|
.rosary-layout:has(#lbead2:target) :global(.large-bead[data-section="lbead2"]),
|
||||||
|
.rosary-layout:has(#secret1_transition:target) :global(.large-bead[data-section="secret1_transition"]),
|
||||||
|
.rosary-layout:has(#secret2_transition:target) :global(.large-bead[data-section="secret2_transition"]),
|
||||||
|
.rosary-layout:has(#secret3_transition:target) :global(.large-bead[data-section="secret3_transition"]),
|
||||||
|
.rosary-layout:has(#secret4_transition:target) :global(.large-bead[data-section="secret4_transition"]),
|
||||||
|
.rosary-layout:has(#final_transition:target) :global(.large-bead[data-section="final_transition"]),
|
||||||
|
.rosary-layout:has(#final_paternoster:target) :global(.large-bead[data-section="final_paternoster"]) {
|
||||||
|
fill: var(--nord13) !important;
|
||||||
|
filter: drop-shadow(0 0 10px var(--nord13));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rosary-layout:has(#cross:target) :global([data-section="cross"] .cross-symbol),
|
||||||
|
.rosary-layout:has(#final_cross:target) :global([data-section="final_cross"] .cross-symbol) {
|
||||||
|
fill: var(--nord11) !important;
|
||||||
|
filter: drop-shadow(0 0 10px var(--nord11));
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
.prayer-section {
|
.prayer-section {
|
||||||
background: var(--nord5);
|
background: var(--nord5);
|
||||||
@@ -597,6 +640,28 @@ h1 {
|
|||||||
height: 50vh;
|
height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inline mystery images: visible without JS, hidden when JS takes over */
|
||||||
|
.decade-inline-image {
|
||||||
|
margin: 1rem auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decade-inline-image img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 40vh;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decade-inline-image figcaption {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--nord4);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.js-enabled) .decade-inline-image {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mystery images: third grid column (desktop), PiP (mobile) */
|
/* Mystery images: third grid column (desktop), PiP (mobile) */
|
||||||
.mystery-image-column {
|
.mystery-image-column {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -633,7 +698,7 @@ h1 {
|
|||||||
|
|
||||||
<!-- Toggle Controls & Streak Counter -->
|
<!-- Toggle Controls & Streak Counter -->
|
||||||
<div class="controls-row">
|
<div class="controls-row">
|
||||||
<StreakCounter streakData={data.streakData} lang={data.lang} />
|
<StreakCounter streakData={data.streakData} lang={data.lang} isLoggedIn={data.isLoggedIn} />
|
||||||
<div class="toggle-controls">
|
<div class="toggle-controls">
|
||||||
<!-- Luminous Mysteries Toggle (link for no-JS, enhanced with onclick for JS) -->
|
<!-- Luminous Mysteries Toggle (link for no-JS, enhanced with onclick for JS) -->
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -645,6 +710,7 @@ h1 {
|
|||||||
<Toggle
|
<Toggle
|
||||||
bind:checked={showImages}
|
bind:checked={showImages}
|
||||||
label={labels.showImages}
|
label={labels.showImages}
|
||||||
|
href={imagesToggleHref}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Language Toggle (link for no-JS, enhanced with onclick for JS) -->
|
<!-- Language Toggle (link for no-JS, enhanced with onclick for JS) -->
|
||||||
@@ -669,6 +735,7 @@ h1 {
|
|||||||
<!-- Cross & Credo -->
|
<!-- Cross & Credo -->
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="cross"
|
||||||
bind:this={sectionElements.cross}
|
bind:this={sectionElements.cross}
|
||||||
data-section="cross"
|
data-section="cross"
|
||||||
>
|
>
|
||||||
@@ -686,6 +753,7 @@ h1 {
|
|||||||
<!-- First Large Bead -->
|
<!-- First Large Bead -->
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="lbead1"
|
||||||
bind:this={sectionElements.lbead1}
|
bind:this={sectionElements.lbead1}
|
||||||
data-section="lbead1"
|
data-section="lbead1"
|
||||||
>
|
>
|
||||||
@@ -696,6 +764,7 @@ h1 {
|
|||||||
<!-- First Ave Maria (Faith) -->
|
<!-- First Ave Maria (Faith) -->
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="start1"
|
||||||
bind:this={sectionElements.start1}
|
bind:this={sectionElements.start1}
|
||||||
data-section="start1"
|
data-section="start1"
|
||||||
>
|
>
|
||||||
@@ -710,6 +779,7 @@ h1 {
|
|||||||
<!-- Second Ave Maria (Hope) -->
|
<!-- Second Ave Maria (Hope) -->
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="start2"
|
||||||
bind:this={sectionElements.start2}
|
bind:this={sectionElements.start2}
|
||||||
data-section="start2"
|
data-section="start2"
|
||||||
>
|
>
|
||||||
@@ -724,6 +794,7 @@ h1 {
|
|||||||
<!-- Third Ave Maria (Love) -->
|
<!-- Third Ave Maria (Love) -->
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="start3"
|
||||||
bind:this={sectionElements.start3}
|
bind:this={sectionElements.start3}
|
||||||
data-section="start3"
|
data-section="start3"
|
||||||
>
|
>
|
||||||
@@ -738,6 +809,7 @@ h1 {
|
|||||||
<!-- Gloria Patri before decades -->
|
<!-- Gloria Patri before decades -->
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="lbead2"
|
||||||
bind:this={sectionElements.lbead2}
|
bind:this={sectionElements.lbead2}
|
||||||
data-section="lbead2"
|
data-section="lbead2"
|
||||||
>
|
>
|
||||||
@@ -752,11 +824,20 @@ h1 {
|
|||||||
<!-- Ave Maria decade (Gesätz) -->
|
<!-- Ave Maria decade (Gesätz) -->
|
||||||
<div
|
<div
|
||||||
class="prayer-section decade"
|
class="prayer-section decade"
|
||||||
|
id={`secret${decadeNum}`}
|
||||||
bind:this={sectionElements[`secret${decadeNum}`]}
|
bind:this={sectionElements[`secret${decadeNum}`]}
|
||||||
data-section={`secret${decadeNum}`}
|
data-section={`secret${decadeNum}`}
|
||||||
>
|
>
|
||||||
<h2>{decadeNum}. {labels.decade}: {currentMysteryTitles[decadeNum - 1]}</h2>
|
<h2>{decadeNum}. {labels.decade}: {currentMysteryTitles[decadeNum - 1]}</h2>
|
||||||
|
|
||||||
|
{#if showImages && allMysteryImages[selectedMystery]?.get(decadeNum)}
|
||||||
|
{@const img = allMysteryImages[selectedMystery].get(decadeNum)}
|
||||||
|
<figure class="decade-inline-image">
|
||||||
|
<img src={img.src} alt={isEnglish ? img.title : img.titleDe} loading="lazy" />
|
||||||
|
<figcaption>{img.artist ? `${img.artist}, ` : ''}<em>{isEnglish ? img.title : img.titleDe}</em>{img.year ? `, ${img.year}` : ''}</figcaption>
|
||||||
|
</figure>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<h3>{labels.hailMary} <span class="repeat-count">(10×)</span></h3>
|
<h3>{labels.hailMary} <span class="repeat-count">(10×)</span></h3>
|
||||||
<AveMaria
|
<AveMaria
|
||||||
mysteryLatin={currentMysteriesLatin[decadeNum - 1]}
|
mysteryLatin={currentMysteriesLatin[decadeNum - 1]}
|
||||||
@@ -784,6 +865,7 @@ h1 {
|
|||||||
{#if decadeNum < 5}
|
{#if decadeNum < 5}
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id={`secret${decadeNum}_transition`}
|
||||||
bind:this={sectionElements[`secret${decadeNum}_transition`]}
|
bind:this={sectionElements[`secret${decadeNum}_transition`]}
|
||||||
data-section={`secret${decadeNum}_transition`}
|
data-section={`secret${decadeNum}_transition`}
|
||||||
>
|
>
|
||||||
@@ -802,6 +884,7 @@ h1 {
|
|||||||
<!-- Final prayers after 5th decade -->
|
<!-- Final prayers after 5th decade -->
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="final_transition"
|
||||||
bind:this={sectionElements.final_transition}
|
bind:this={sectionElements.final_transition}
|
||||||
data-section="final_transition"
|
data-section="final_transition"
|
||||||
>
|
>
|
||||||
@@ -816,6 +899,7 @@ h1 {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="final_salve"
|
||||||
bind:this={sectionElements.final_salve}
|
bind:this={sectionElements.final_salve}
|
||||||
data-section="final_salve"
|
data-section="final_salve"
|
||||||
>
|
>
|
||||||
@@ -825,6 +909,7 @@ h1 {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="final_schlussgebet"
|
||||||
bind:this={sectionElements.final_schlussgebet}
|
bind:this={sectionElements.final_schlussgebet}
|
||||||
data-section="final_schlussgebet"
|
data-section="final_schlussgebet"
|
||||||
>
|
>
|
||||||
@@ -834,6 +919,7 @@ h1 {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="final_michael"
|
||||||
bind:this={sectionElements.final_michael}
|
bind:this={sectionElements.final_michael}
|
||||||
data-section="final_michael"
|
data-section="final_michael"
|
||||||
>
|
>
|
||||||
@@ -843,6 +929,7 @@ h1 {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="final_paternoster"
|
||||||
bind:this={sectionElements.final_paternoster}
|
bind:this={sectionElements.final_paternoster}
|
||||||
data-section="final_paternoster"
|
data-section="final_paternoster"
|
||||||
>
|
>
|
||||||
@@ -852,6 +939,7 @@ h1 {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="prayer-section"
|
class="prayer-section"
|
||||||
|
id="final_cross"
|
||||||
bind:this={sectionElements.final_cross}
|
bind:this={sectionElements.final_cross}
|
||||||
data-section="final_cross"
|
data-section="final_cross"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -128,34 +128,34 @@
|
|||||||
class="cross-symbol" class:active-cross={activeSection === 'final_cross'} />
|
class="cross-symbol" class:active-cross={activeSection === 'final_cross'} />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Invisible hitboxes for larger tap targets -->
|
<!-- Invisible hitboxes for larger tap targets (anchor links for no-JS fallback) -->
|
||||||
<g class="hitboxes">
|
<g class="hitboxes">
|
||||||
<!-- Cross hitbox -->
|
<!-- Cross hitbox -->
|
||||||
<rect x="-15" y="-30" width="80" height="80" data-section="cross" />
|
<a href="#cross"><rect x="-15" y="-30" width="80" height="80" data-section="cross" /></a>
|
||||||
|
|
||||||
<!-- Individual bead hitboxes -->
|
<!-- Individual bead hitboxes -->
|
||||||
<circle cx="25" cy={pos.lbead1} r="25" data-section="lbead1" />
|
<a href="#lbead1"><circle cx="25" cy={pos.lbead1} r="25" data-section="lbead1" /></a>
|
||||||
<circle cx="25" cy={pos.start1} r="20" data-section="start1" />
|
<a href="#start1"><circle cx="25" cy={pos.start1} r="20" data-section="start1" /></a>
|
||||||
<circle cx="25" cy={pos.start2} r="20" data-section="start2" />
|
<a href="#start2"><circle cx="25" cy={pos.start2} r="20" data-section="start2" /></a>
|
||||||
<circle cx="25" cy={pos.start3} r="20" data-section="start3" />
|
<a href="#start3"><circle cx="25" cy={pos.start3} r="20" data-section="start3" /></a>
|
||||||
<circle cx="25" cy={pos.lbead2} r="25" data-section="lbead2" />
|
<a href="#lbead2"><circle cx="25" cy={pos.lbead2} r="25" data-section="lbead2" /></a>
|
||||||
|
|
||||||
<!-- Decade hitboxes -->
|
<!-- Decade hitboxes -->
|
||||||
{#each [1, 2, 3, 4, 5] as d (d)}
|
{#each [1, 2, 3, 4, 5] as d (d)}
|
||||||
{@const decadePos = pos[`secret${d}`]}
|
{@const decadePos = pos[`secret${d}`]}
|
||||||
<rect x="-15" y={decadePos - 2} width="80" height={DECADE_OFFSET + 9 * BEAD_SPACING + 12} data-section={`secret${d}`} />
|
<a href={`#secret${d}`}><rect x="-15" y={decadePos - 2} width="80" height={DECADE_OFFSET + 9 * BEAD_SPACING + 12} data-section={`secret${d}`} /></a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<!-- Transition bead hitboxes -->
|
<!-- Transition bead hitboxes -->
|
||||||
{#each [1, 2, 3, 4] as d (d)}
|
{#each [1, 2, 3, 4] as d (d)}
|
||||||
<circle cx="25" cy={pos[`secret${d}_transition`]} r="25" data-section={`secret${d}_transition`} />
|
<a href={`#secret${d}_transition`}><circle cx="25" cy={pos[`secret${d}_transition`]} r="25" data-section={`secret${d}_transition`} /></a>
|
||||||
{/each}
|
{/each}
|
||||||
<circle cx="25" cy={pos.final_transition} r="25" data-section="final_transition" />
|
<a href="#final_transition"><circle cx="25" cy={pos.final_transition} r="25" data-section="final_transition" /></a>
|
||||||
<circle cx="25" cy={pos.final_salve} r="20" data-section="final_salve" />
|
<a href="#final_salve"><circle cx="25" cy={pos.final_salve} r="20" data-section="final_salve" /></a>
|
||||||
<circle cx="25" cy={pos.final_schlussgebet} r="20" data-section="final_schlussgebet" />
|
<a href="#final_schlussgebet"><circle cx="25" cy={pos.final_schlussgebet} r="20" data-section="final_schlussgebet" /></a>
|
||||||
<circle cx="25" cy={pos.final_michael} r="20" data-section="final_michael" />
|
<a href="#final_michael"><circle cx="25" cy={pos.final_michael} r="20" data-section="final_michael" /></a>
|
||||||
<circle cx="25" cy={pos.final_paternoster} r="25" data-section="final_paternoster" />
|
<a href="#final_paternoster"><circle cx="25" cy={pos.final_paternoster} r="25" data-section="final_paternoster" /></a>
|
||||||
<rect x="-15" y={pos.final_cross - 50} width="80" height="80" data-section="final_cross" />
|
<a href="#final_cross"><rect x="-15" y={pos.final_cross - 50} width="80" height="80" data-section="final_cross" /></a>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export function setupScrollSync({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle clicks on SVG elements to jump to prayers
|
// Handle clicks on SVG elements to jump to prayers
|
||||||
|
// preventDefault() overrides the anchor-link fallback when JS is enabled
|
||||||
const handleSvgClick = (e) => {
|
const handleSvgClick = (e) => {
|
||||||
const svgContainer = getSvgContainer();
|
const svgContainer = getSvgContainer();
|
||||||
const sectionElements = getSectionElements();
|
const sectionElements = getSectionElements();
|
||||||
@@ -201,6 +202,7 @@ export function setupScrollSync({
|
|||||||
while (target && target !== svgContainer) {
|
while (target && target !== svgContainer) {
|
||||||
const section = target.dataset.section;
|
const section = target.dataset.section;
|
||||||
if (section && sectionElements[section]) {
|
if (section && sectionElements[section]) {
|
||||||
|
e.preventDefault();
|
||||||
setActiveSection(section);
|
setActiveSection(section);
|
||||||
setScrollLock('click', 1500);
|
setScrollLock('click', 1500);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user