refactor: extract sub-components and modules from rosary +page.svelte
All checks were successful
CI / update (push) Successful in 3m9s
All checks were successful
CI / update (push) Successful in 3m9s
Break the 1889-line rosary page into focused modules: - rosaryData.js: mystery data, labels, weekday schedule, SVG positions - rosaryScrollSync.js: bidirectional scroll sync (prayers ↔ SVG ↔ images) - RosarySvg.svelte: SVG bead visualization - MysterySelector.svelte: mystery picker grid - MysteryImageColumn.svelte: desktop image column Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
<script>
|
||||
let { images, isEnglish } = $props();
|
||||
</script>
|
||||
<style>
|
||||
@media (min-width: 1200px) {
|
||||
.mystery-image-pad {
|
||||
height: calc(100vh - 5rem);
|
||||
}
|
||||
.mystery-image-pad[data-target="before"],
|
||||
.mystery-image-pad[data-target="after"] {
|
||||
height: 100vh;
|
||||
}
|
||||
figure {
|
||||
margin: 0;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
img {
|
||||
max-height: calc(100vh - 5rem);
|
||||
width: auto;
|
||||
max-width: 25vw;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
}
|
||||
figcaption {
|
||||
font-size: 0.8rem;
|
||||
color: var(--nord4);
|
||||
margin-top: 0.4rem;
|
||||
max-width: 25vw;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) and (prefers-color-scheme: light) {
|
||||
figcaption {
|
||||
color: var(--nord2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="mystery-image-pad" data-target="before"></div>
|
||||
{#each [...images.entries()] as [num, img], i (num)}
|
||||
{#if i > 0}<div class="mystery-image-pad" data-target="between{i}"></div>{/if}
|
||||
<figure data-target={num}>
|
||||
<img src={img.src} alt="{img.artist ? `${img.artist} — ` : ''}{isEnglish ? img.title : img.titleDe}">
|
||||
<figcaption>{#if img.artist}{img.artist}, {/if}<em>{isEnglish ? img.title : img.titleDe}</em>{#if img.year}, {img.year}{/if}</figcaption>
|
||||
</figure>
|
||||
{/each}
|
||||
<div class="mystery-image-pad" data-target="after"></div>
|
||||
@@ -0,0 +1,219 @@
|
||||
<script>
|
||||
import MysteryIcon from "$lib/components/faith/MysteryIcon.svelte";
|
||||
|
||||
let { selectedMystery, todaysMystery, includeLuminous, labels, mysteryHref, selectMystery } = $props();
|
||||
</script>
|
||||
<style>
|
||||
/* Mystery selector grid */
|
||||
.mystery-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
max-width: 750px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mystery-selector.four-mysteries {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.mystery-selector.four-mysteries {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
max-width: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.mystery-selector,
|
||||
.mystery-selector.four-mysteries {
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-inline: 0;
|
||||
max-width: none;
|
||||
}
|
||||
.mystery-selector :global(svg) {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.mystery-button {
|
||||
padding: 1rem 0.75rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.mystery-button h3 {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.today-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 410px) {
|
||||
.mystery-selector,
|
||||
.mystery-selector.four-mysteries {
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-inline: 0;
|
||||
max-width: none;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.mystery-selector:not(.four-mysteries) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
.mystery-button {
|
||||
padding: 0.25rem 0.15rem;
|
||||
gap: 0.15rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.mystery-button h3 {
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
.today-badge {
|
||||
font-size: 0.6rem;
|
||||
padding: 0.15rem 0.35rem;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 350px) {
|
||||
.mystery-selector,
|
||||
.mystery-selector.four-mysteries {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.mystery-button {
|
||||
background: var(--nord1);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 2rem 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: light) {
|
||||
.mystery-button {
|
||||
background: var(--nord6);
|
||||
}
|
||||
}
|
||||
|
||||
.mystery-button:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.mystery-button.selected {
|
||||
border-color: var(--nord10);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.mystery-button:hover,
|
||||
.mystery-button.selected { background: var(--nord4); }
|
||||
|
||||
|
||||
.mystery-button h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--nord6);
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: light) {
|
||||
.mystery-button h3 {
|
||||
color: var(--nord0);
|
||||
}
|
||||
}
|
||||
|
||||
.mystery-button.selected h3,
|
||||
.mystery-button:hover h3
|
||||
{
|
||||
color: var(--nord10);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Today's mystery badge */
|
||||
.today-badge {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: var(--nord11);
|
||||
color: white;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="mystery-selector" class:four-mysteries={includeLuminous}>
|
||||
<a
|
||||
class="mystery-button"
|
||||
class:selected={selectedMystery === '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>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="mystery-button"
|
||||
class:selected={selectedMystery === '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>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="mystery-button"
|
||||
class:selected={selectedMystery === '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>
|
||||
</a>
|
||||
|
||||
{#if includeLuminous}
|
||||
<a
|
||||
class="mystery-button"
|
||||
class:selected={selectedMystery === 'lichtreichen'}
|
||||
href={mysteryHref('lichtreichen')}
|
||||
onclick={(e) => { e.preventDefault(); selectMystery('lichtreichen'); }}
|
||||
>
|
||||
{#if todaysMystery === 'lichtreichen'}
|
||||
<span class="today-badge">{labels.today}</span>
|
||||
{/if}
|
||||
<MysteryIcon type="luminous" />
|
||||
|
||||
<h3>{labels.luminous}</h3>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,161 @@
|
||||
<script>
|
||||
let { pos, BEAD_SPACING, DECADE_OFFSET, activeSection, decadeCounters } = $props();
|
||||
</script>
|
||||
<style>
|
||||
.linear-rosary {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.bead {
|
||||
fill: var(--nord10);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.large-bead {
|
||||
fill: var(--nord12);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chain {
|
||||
stroke: var(--nord4);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cross-symbol {
|
||||
fill: var(--nord4);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.cross-symbol {
|
||||
fill: var(--nord3);
|
||||
}
|
||||
.chain {
|
||||
stroke: var(--nord3);
|
||||
}
|
||||
}
|
||||
|
||||
.hitboxes {
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
/* Active states */
|
||||
.active-bead {
|
||||
fill: var(--nord11) !important;
|
||||
filter: drop-shadow(0 0 8px var(--nord11));
|
||||
}
|
||||
|
||||
.active-large-bead {
|
||||
fill: var(--nord13) !important;
|
||||
filter: drop-shadow(0 0 10px var(--nord13));
|
||||
}
|
||||
|
||||
.cross-symbol.active-cross {
|
||||
fill: var(--nord11) !important;
|
||||
filter: drop-shadow(0 0 10px var(--nord11));
|
||||
}
|
||||
|
||||
/* Highlighted bead (orange for counting) */
|
||||
.counted-bead {
|
||||
fill: var(--nord13) !important;
|
||||
filter: drop-shadow(0 0 8px var(--nord13));
|
||||
}
|
||||
</style>
|
||||
|
||||
<svg class="linear-rosary" viewBox="-100 -100 250 2200" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMin meet">
|
||||
<defs>
|
||||
<symbol id="cross-glyph" viewBox="0 0 1304 1497">
|
||||
<path transform="translate(-132, 1497) scale(1, -1)" d="M315 948Q293 903 245 857Q292 813 315 768L410 835V881ZM1159 880V834L1253 767Q1276 813 1323 857Q1276 903 1253 948ZM875 1314Q831 1337 784 1384Q740 1337 695 1314L763 1219H808ZM807 277H762L695 182Q740 159 784 112Q830 159 875 182ZM868 941H1096L1304 1067Q1314 962 1436 856Q1314 752 1304 648L1096 774H868V340L994 132Q888 121 783 0Q679 121 575 132L701 340V774H473L265 648Q254 752 132 857Q254 962 265 1067L473 941H701V1155L575 1364Q679 1375 784 1497Q888 1375 994 1364L868 1155ZM758 1040V881H642V835H758V451H812V835H928V881H812V1040Z" />
|
||||
</symbol>
|
||||
</defs>
|
||||
|
||||
<!-- Vertical chain -->
|
||||
<line x1="25" y1={pos.cross} x2="25" y2={pos.final_paternoster + 40} class="chain" />
|
||||
|
||||
<!-- Cross (at top) -->
|
||||
<g id="cross-section" data-section="cross">
|
||||
<use href="#cross-glyph" x={25 - 25} y={pos.cross - 58} width="50" height="58"
|
||||
class="cross-symbol" class:active-cross={activeSection === 'cross'} />
|
||||
</g>
|
||||
|
||||
<!-- First large bead (Paternoster) -->
|
||||
<circle cx="25" cy={pos.lbead1} r="15" class="large-bead" class:active-large-bead={activeSection === 'lbead1'} data-section="lbead1" />
|
||||
|
||||
<!-- Three small beads -->
|
||||
<circle cx="25" cy={pos.start1} r="10" class="bead" class:active-bead={activeSection === 'start1'} data-section="start1" />
|
||||
<circle cx="25" cy={pos.start2} r="10" class="bead" class:active-bead={activeSection === 'start2'} data-section="start2" />
|
||||
<circle cx="25" cy={pos.start3} r="10" class="bead" class:active-bead={activeSection === 'start3'} data-section="start3" />
|
||||
|
||||
<!-- Large bead before decades -->
|
||||
<circle cx="25" cy={pos.lbead2} r="15" class="large-bead" class:active-large-bead={activeSection === 'lbead2'} data-section="lbead2" />
|
||||
|
||||
<!-- Benedictus Medal -->
|
||||
<image href="/glaube/benedictus.svg" x="5" y={pos.lbead2 + 25} width="40" height="40" />
|
||||
|
||||
<!-- 5 Decades -->
|
||||
{#each [1, 2, 3, 4, 5] as d (d)}
|
||||
{@const decadePos = pos[`secret${d}`]}
|
||||
{@const transPos = pos[`secret${d}_transition`]}
|
||||
<!-- Decade {d}: Ave Maria (10 beads) -->
|
||||
{#each Array(10) as _, i (i)}
|
||||
<circle cx="25" cy={decadePos + DECADE_OFFSET + i * BEAD_SPACING} r="10" class="bead"
|
||||
class:active-bead={activeSection === `secret${d}`}
|
||||
class:counted-bead={i < decadeCounters[`secret${d}`]}
|
||||
data-section={`secret${d}`} />
|
||||
{/each}
|
||||
<!-- Transition: Gloria + Fatima + Paternoster (large bead) -->
|
||||
{#if d < 5}
|
||||
<circle cx="25" cy={transPos} r="15" class="large-bead" class:active-large-bead={activeSection === `secret${d}_transition`} data-section={`secret${d}_transition`} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<image href="/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" />
|
||||
|
||||
<circle cx="25" cy={pos.final_salve} r="10" class="bead" class:active-bead={activeSection === 'final_salve'} data-section="final_salve" />
|
||||
<circle cx="25" cy={pos.final_schlussgebet} r="10" class="bead" class:active-bead={activeSection === 'final_schlussgebet'} data-section="final_schlussgebet" />
|
||||
<circle cx="25" cy={pos.final_michael} r="10" class="bead" class:active-bead={activeSection === 'final_michael'} data-section="final_michael" />
|
||||
|
||||
<circle cx="25" cy={pos.final_paternoster} r="15" class="large-bead" class:active-large-bead={activeSection === 'final_paternoster'} data-section="final_paternoster" />
|
||||
<g data-section="final_cross">
|
||||
<use href="#cross-glyph" x={25 - 25} y={pos.final_cross - 58} width="50" height="58"
|
||||
class="cross-symbol" class:active-cross={activeSection === 'final_cross'} />
|
||||
</g>
|
||||
|
||||
<!-- Invisible hitboxes for larger tap targets -->
|
||||
<g class="hitboxes">
|
||||
<!-- Cross hitbox -->
|
||||
<rect x="-15" y="-30" width="80" height="80" data-section="cross" />
|
||||
|
||||
<!-- Individual bead hitboxes -->
|
||||
<circle cx="25" cy={pos.lbead1} r="25" data-section="lbead1" />
|
||||
<circle cx="25" cy={pos.start1} r="20" data-section="start1" />
|
||||
<circle cx="25" cy={pos.start2} r="20" data-section="start2" />
|
||||
<circle cx="25" cy={pos.start3} r="20" data-section="start3" />
|
||||
<circle cx="25" cy={pos.lbead2} r="25" data-section="lbead2" />
|
||||
|
||||
<!-- Decade hitboxes -->
|
||||
{#each [1, 2, 3, 4, 5] as d (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}`} />
|
||||
{/each}
|
||||
|
||||
<!-- Transition bead hitboxes -->
|
||||
{#each [1, 2, 3, 4] as d (d)}
|
||||
<circle cx="25" cy={pos[`secret${d}_transition`]} r="25" data-section={`secret${d}_transition`} />
|
||||
{/each}
|
||||
<circle cx="25" cy={pos.final_transition} r="25" data-section="final_transition" />
|
||||
<circle cx="25" cy={pos.final_salve} r="20" data-section="final_salve" />
|
||||
<circle cx="25" cy={pos.final_schlussgebet} r="20" data-section="final_schlussgebet" />
|
||||
<circle cx="25" cy={pos.final_michael} r="20" data-section="final_michael" />
|
||||
<circle cx="25" cy={pos.final_paternoster} r="25" data-section="final_paternoster" />
|
||||
<rect x="-15" y={pos.final_cross - 50} width="80" height="80" data-section="final_cross" />
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
@@ -0,0 +1,288 @@
|
||||
// Mystery variations for each type of rosary
|
||||
export const mysteries = {
|
||||
freudenreich: [
|
||||
"Jesus, den du, o Jungfrau, vom Heiligen Geist empfangen hast.",
|
||||
"Jesus, den du, o Jungfrau, zu Elisabeth getragen hast.",
|
||||
"Jesus, den du, o Jungfrau, in Betlehem geboren hast.",
|
||||
"Jesus, den du, o Jungfrau, im Tempel geopfert hast.",
|
||||
"Jesus, den du, o Jungfrau, im Tempel wiedergefunden hast."
|
||||
],
|
||||
schmerzhaften: [
|
||||
"Jesus, der für uns Blut geschwitzt hat.",
|
||||
"Jesus, der für uns gegeisselt worden ist.",
|
||||
"Jesus, der für uns mit Dornen gekrönt worden ist.",
|
||||
"Jesus, der für uns das schwere Kreuz getragen hat.",
|
||||
"Jesus, der für uns gekreuzigt worden ist."
|
||||
],
|
||||
glorreichen: [
|
||||
"Jesus, der von den Toten auferstanden ist.",
|
||||
"Jesus, der in den Himmel aufgefahren ist.",
|
||||
"Jesus, der uns den Heiligen Geist gesandt hat.",
|
||||
"Jesus, der dich, o Jungfrau, in den Himmel aufgenommen hat.",
|
||||
"Jesus, der dich, o Jungfrau, im Himmel gekrönt hat."
|
||||
],
|
||||
lichtreichen: [
|
||||
"Jesus, der von Johannes getauft worden ist.",
|
||||
"Jesus, der sich bei der Hochzeit in Kana geoffenbart hat.",
|
||||
"Jesus, der uns das Reich Gottes verkündet hat.",
|
||||
"Jesus, der auf dem Berg verklärt worden ist.",
|
||||
"Jesus, der uns die Eucharistie geschenkt hat."
|
||||
]
|
||||
};
|
||||
|
||||
export const mysteriesLatin = {
|
||||
freudenreich: [
|
||||
"Jesus, quem, virgo, concepísti.",
|
||||
"Jesus, quem visitándo Elísabeth portásti.",
|
||||
"Jesus, quem, virgo, genuísti.",
|
||||
"Jesus, quem in templo præsentásti.",
|
||||
"Jesus, quem in templo invenisti."
|
||||
],
|
||||
schmerzhaften: [
|
||||
"Jesus, qui pro nobis sánguinem sudavit.",
|
||||
"Jesus, qui pro nobis flagellátus est.",
|
||||
"Jesus, qui pro nobis spinis coronátus est.",
|
||||
"Jesus, qui pro nobis crucem baiulávit.",
|
||||
"Jesus, qui pro nobis crucifixus est."
|
||||
],
|
||||
glorreichen: [
|
||||
"Jesus, qui resurréxit a mórtuis.",
|
||||
"Jesus, qui ascendit in cælum.",
|
||||
"Jesus, qui misit Spíritum Sanctum.",
|
||||
"Jesus, qui te, virgo, in cælum assúmpsit.",
|
||||
"Jesus, qui te, virgo, in cælo coronávit."
|
||||
],
|
||||
lichtreichen: [
|
||||
"Jesus, qui a Ioánne baptizátus est.",
|
||||
"Jesus, qui se in Cana revelávit.",
|
||||
"Jesus, qui regnum Dei prædicávit.",
|
||||
"Jesus, qui in monte transfigurátus est.",
|
||||
"Jesus, Sacraméntum Altáris instítuit."
|
||||
]
|
||||
};
|
||||
|
||||
// English mysteries
|
||||
export const mysteriesEnglish = {
|
||||
freudenreich: [
|
||||
"Jesus, whom thou, O Virgin, didst conceive of the Holy Spirit.",
|
||||
"Jesus, whom thou, O Virgin, didst carry to Elizabeth.",
|
||||
"Jesus, whom thou, O Virgin, didst bring forth in Bethlehem.",
|
||||
"Jesus, whom thou, O Virgin, didst present in the Temple.",
|
||||
"Jesus, whom thou, O Virgin, didst find in the Temple."
|
||||
],
|
||||
schmerzhaften: [
|
||||
"Jesus, who sweat blood for us.",
|
||||
"Jesus, who was scourged for us.",
|
||||
"Jesus, who was crowned with thorns for us.",
|
||||
"Jesus, who carried the heavy cross for us.",
|
||||
"Jesus, who was crucified for us."
|
||||
],
|
||||
glorreichen: [
|
||||
"Jesus, who rose from the dead.",
|
||||
"Jesus, who ascended into heaven.",
|
||||
"Jesus, who sent us the Holy Spirit.",
|
||||
"Jesus, who took thee, O Virgin, into heaven.",
|
||||
"Jesus, who crowned thee, O Virgin, in heaven."
|
||||
],
|
||||
lichtreichen: [
|
||||
"Jesus, who was baptized by John.",
|
||||
"Jesus, who revealed Himself at the wedding in Cana.",
|
||||
"Jesus, who proclaimed the Kingdom of God.",
|
||||
"Jesus, who was transfigured on the mountain.",
|
||||
"Jesus, who gave us the Eucharist."
|
||||
]
|
||||
};
|
||||
|
||||
// Short titles for mysteries (for display in headings)
|
||||
export const mysteryTitles = {
|
||||
freudenreich: [
|
||||
"Verkündigung",
|
||||
"Heimsuchung",
|
||||
"Geburt",
|
||||
"Darstellung",
|
||||
"Wiederfindung"
|
||||
],
|
||||
schmerzhaften: [
|
||||
"Todesangst",
|
||||
"Geisselung",
|
||||
"Dornenkrönung",
|
||||
"Kreuzweg",
|
||||
"Kreuzigung"
|
||||
],
|
||||
glorreichen: [
|
||||
"Auferstehung",
|
||||
"Himmelfahrt",
|
||||
"Geistsendung",
|
||||
"Aufnahme Mariens",
|
||||
"Krönung Mariens"
|
||||
],
|
||||
lichtreichen: [
|
||||
"Taufe",
|
||||
"Hochzeit zu Kana",
|
||||
"Verkündigung des Reiches",
|
||||
"Verklärung",
|
||||
"Einsetzung der Eucharistie"
|
||||
]
|
||||
};
|
||||
|
||||
// English short titles for mysteries
|
||||
export const mysteryTitlesEnglish = {
|
||||
freudenreich: [
|
||||
"Annunciation",
|
||||
"Visitation",
|
||||
"Nativity",
|
||||
"Presentation",
|
||||
"Finding in the Temple"
|
||||
],
|
||||
schmerzhaften: [
|
||||
"Agony in the Garden",
|
||||
"Scourging",
|
||||
"Crowning with Thorns",
|
||||
"Carrying of the Cross",
|
||||
"Crucifixion"
|
||||
],
|
||||
glorreichen: [
|
||||
"Resurrection",
|
||||
"Ascension",
|
||||
"Descent of the Holy Spirit",
|
||||
"Assumption of Mary",
|
||||
"Coronation of Mary"
|
||||
],
|
||||
lichtreichen: [
|
||||
"Baptism",
|
||||
"Wedding at Cana",
|
||||
"Proclamation of the Kingdom",
|
||||
"Transfiguration",
|
||||
"Institution of the Eucharist"
|
||||
]
|
||||
};
|
||||
|
||||
// UI labels based on language
|
||||
export function getLabels(isEnglish) {
|
||||
return {
|
||||
pageTitle: isEnglish ? 'Interactive Rosary' : 'Interaktiver Rosenkranz',
|
||||
pageDescription: isEnglish
|
||||
? 'Interactive digital version of the Rosary for praying along. Scroll through the prayers and follow the visualization.'
|
||||
: 'Interaktive digitale Version des Rosenkranzes zum Mitbeten. Scrolle durch die Gebete und folge der Visualisierung.',
|
||||
mysteries: isEnglish ? 'Mysteries' : 'Geheimnisse',
|
||||
today: isEnglish ? 'Today' : 'Heutige',
|
||||
joyful: isEnglish ? 'Joyful' : 'Freudenreiche',
|
||||
sorrowful: isEnglish ? 'Sorrowful' : 'Schmerzhaften',
|
||||
glorious: isEnglish ? 'Glorious' : 'Glorreichen',
|
||||
luminous: isEnglish ? 'Luminous' : 'Lichtreichen',
|
||||
includeLuminous: isEnglish ? 'Include Luminous Mysteries' : 'Lichtreiche Geheimnisse einbeziehen',
|
||||
showImages: isEnglish ? 'Show Images' : 'Bilder anzeigen',
|
||||
beginning: isEnglish ? 'Beginning' : 'Anfang',
|
||||
signOfCross: isEnglish ? '♱ Sign of the Cross' : '♱ Das Kreuzzeichen',
|
||||
ourFather: isEnglish ? 'Our Father' : 'Vater unser',
|
||||
hailMary: isEnglish ? 'Hail Mary' : 'Ave Maria',
|
||||
faith: isEnglish ? 'Faith' : 'Glaube',
|
||||
hope: isEnglish ? 'Hope' : 'Hoffnung',
|
||||
love: isEnglish ? 'Love' : 'Liebe',
|
||||
decade: isEnglish ? 'Decade' : 'Gesätz',
|
||||
optional: isEnglish ? 'optional' : 'optional',
|
||||
gloriaPatri: 'Gloria Patri',
|
||||
fatimaPrayer: isEnglish ? 'Fatima Prayer' : 'Das Fatima Gebet',
|
||||
conclusion: isEnglish ? 'Conclusion' : 'Abschluss',
|
||||
finalPrayer: isEnglish ? 'Final Prayer' : 'Schlussgebet',
|
||||
saintMichael: isEnglish ? 'Prayer to St. Michael the Archangel' : 'Gebet zum hl. Erzengel Michael',
|
||||
footnoteSign: isEnglish ? 'Make the Sign of the Cross here' : 'Hier das Kreuzzeichen machen',
|
||||
footnoteBow: isEnglish ? 'Bow the head here' : 'Hier den Kopf senken',
|
||||
showBibleVerse: isEnglish ? 'Show Bible verse' : 'Bibelstelle anzeigen',
|
||||
mysteryFaith: isEnglish ? 'Jesus, who may increase our faith' : 'Jesus, der in uns den Glauben vermehre',
|
||||
mysteryHope: isEnglish ? 'Jesus, who may strengthen our hope' : 'Jesus, der in uns die Hoffnung stärke',
|
||||
mysteryLove: isEnglish ? 'Jesus, who may kindle our love' : 'Jesus, der in uns die Liebe entzünde'
|
||||
};
|
||||
}
|
||||
|
||||
// Get the appropriate mystery for a given weekday
|
||||
export function getMysteryForWeekday(date, includeLuminous) {
|
||||
const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
||||
|
||||
if (includeLuminous) {
|
||||
const schedule = {
|
||||
0: 'glorreichen', // Sunday
|
||||
1: 'freudenreich', // Monday
|
||||
2: 'schmerzhaften', // Tuesday
|
||||
3: 'glorreichen', // Wednesday
|
||||
4: 'lichtreichen', // Thursday
|
||||
5: 'schmerzhaften', // Friday
|
||||
6: 'freudenreich' // Saturday
|
||||
};
|
||||
return schedule[dayOfWeek];
|
||||
} else {
|
||||
const schedule = {
|
||||
0: 'glorreichen', // Sunday
|
||||
1: 'freudenreich', // Monday
|
||||
2: 'schmerzhaften', // Tuesday
|
||||
3: 'glorreichen', // Wednesday
|
||||
4: 'freudenreich', // Thursday
|
||||
5: 'schmerzhaften', // Friday
|
||||
6: 'glorreichen' // Saturday
|
||||
};
|
||||
return schedule[dayOfWeek];
|
||||
}
|
||||
}
|
||||
|
||||
// SVG layout constants
|
||||
export const BEAD_SPACING = 22;
|
||||
export const DECADE_OFFSET = 10;
|
||||
|
||||
// Map sections to their vertical positions in the SVG
|
||||
export const sectionPositions = {
|
||||
cross: 35,
|
||||
lbead1: 75,
|
||||
start1: 110,
|
||||
start2: 135,
|
||||
start3: 160,
|
||||
lbead2: 195,
|
||||
secret1: 270,
|
||||
secret2: 560,
|
||||
secret3: 840,
|
||||
secret4: 1120,
|
||||
secret5: 1400,
|
||||
final_transition: 1685,
|
||||
final_salve: 1720,
|
||||
final_schlussgebet: 1745,
|
||||
final_michael: 1770,
|
||||
final_paternoster: 1805,
|
||||
final_cross: 1900
|
||||
};
|
||||
// Center transition beads between last bead of decade d and first bead of decade d+1
|
||||
for (let d = 1; d < 5; d++) {
|
||||
const lastBead = sectionPositions[`secret${d}`] + DECADE_OFFSET + 9 * BEAD_SPACING;
|
||||
const nextFirst = sectionPositions[`secret${d + 1}`] + DECADE_OFFSET;
|
||||
sectionPositions[`secret${d}_transition`] = Math.round((lastBead + nextFirst) / 2);
|
||||
}
|
||||
|
||||
// Mystery images with captions (per mystery type, keyed by decade number 1-5)
|
||||
export const allMysteryImages = {
|
||||
freudenreich: new Map([
|
||||
[1, { src: "/glaube/joyful/1-murilllo-annunciation.webp", artist: "Bartolomé Esteban Murillo", title: "The Annunciation", titleDe: "Die Verkündigung" }],
|
||||
[2, { src: "/glaube/joyful/2-carl-bloch.the-visitation.1866.webp", artist: "Carl Bloch", title: "The Visitation", titleDe: "Die Heimsuchung", year: 1866 }],
|
||||
[3, { src: "/glaube/joyful/3-adoration-of-the-shepards.webp", title: "Adoration of the Shepherds", titleDe: "Die Anbetung der Hirten" }],
|
||||
[4, { src: "/glaube/joyful/4-vouet.presentation-in-the-temple.webp", artist: "Simon Vouet", title: "The Presentation in the Temple", titleDe: "Die Darstellung im Tempel" }],
|
||||
[5, { src: "/glaube/joyful/5-carl-bloch.the-twelve-year-old-jesus-in-the-temple.1869.webp", artist: "Carl Bloch", title: "The Twelve-Year-Old Jesus in the Temple", titleDe: "Der zwölfjährige Jesus im Tempel", year: 1869 }],
|
||||
]),
|
||||
schmerzhaften: new Map([
|
||||
[1, { src: "/glaube/sorrowful/1.carl-bloch.gethsemane.webp", artist: "Carl Bloch", title: "Gethsemane", titleDe: "Gethsemane", year: 1873 }],
|
||||
[2, { src: "/glaube/sorrowful/2.wiliam-bouguereau.flagellation.webp", artist: "William-Adolphe Bouguereau", title: "The Flagellation of Our Lord Jesus Christ", titleDe: "Die Geisselung unseres Herrn Jesus Christus", year: 1880 }],
|
||||
[3, { src: "/glaube/sorrowful/3.carl-bloch.mocking.webp", artist: "Carl Bloch", title: "The Mocking of Christ", titleDe: "Die Verspottung Christi", year: 1880 }],
|
||||
[4, { src: "/glaube/sorrowful/4.lorenzo-lotto.carrying-the-cross.webp", artist: "Lorenzo Lotto", title: "Carrying the Cross", titleDe: "Die Kreuztragung", year: 1526 }],
|
||||
[5, { src: "/glaube/sorrowful/5.alonso-cano.the-crucifixion.webp", artist: "Diego Velázquez", title: "Christ Crucified", titleDe: "Der gekreuzigte Christus", year: 1632 }],
|
||||
]),
|
||||
glorreichen: new Map([
|
||||
[1, { src: "/glaube/glorious/1-carl-bloch.resurrection.webp", artist: "Carl Bloch", title: "The Resurrection", titleDe: "Die Auferstehung" }],
|
||||
[2, { src: "/glaube/glorious/2-ascension.webp", title: "The Ascension", titleDe: "Die Himmelfahrt" }],
|
||||
[3, { src: "/glaube/glorious/3-pentecost.webp", title: "Pentecost", titleDe: "Die Geistsendung" }],
|
||||
[4, { src: "/glaube/glorious/4-giovanni-tiepolo.the-immaculate-conception.webp", artist: "Giovanni Battista Tiepolo", title: "The Immaculate Conception", titleDe: "Die Aufnahme Mariens in den Himmel" }],
|
||||
[5, { src: "/glaube/glorious/5-diego-veazquez.coronation-mary.webp", artist: "Diego Velázquez", title: "Coronation of the Virgin", titleDe: "Die Krönung der Jungfrau", year: 1641 }],
|
||||
]),
|
||||
lichtreichen: new Map([
|
||||
[1, { src: "/glaube/luminous/1-carl-bloch.the-baptism-of-christ.1870.webp", artist: "Carl Bloch", title: "The Baptism of Christ", titleDe: "Die Taufe Christi", year: 1870 }],
|
||||
[2, { src: "/glaube/luminous/2-carl-bloch.the-wedding-at-cana.1870.webp", artist: "Carl Bloch", title: "The Wedding at Cana", titleDe: "Die Hochzeit zu Kana", year: 1870 }],
|
||||
[3, { src: "/glaube/luminous/3-carl-bloch.the-sermon-on-the-mount.1877.jpg", artist: "Carl Bloch", title: "The Sermon on the Mount", titleDe: "Die Bergpredigt", year: 1877 }],
|
||||
[4, { src: "/glaube/luminous/4-carl-bloch.transfiguration-of-christ.webp", artist: "Carl Bloch", title: "Transfiguration of Christ", titleDe: "Die Verklärung Christi" }],
|
||||
[5, { src: "/glaube/luminous/5-carl-bloch.the-last-supper.webp", artist: "Carl Bloch", title: "The Last Supper", titleDe: "Das letzte Abendmahl" }],
|
||||
]),
|
||||
};
|
||||
@@ -0,0 +1,331 @@
|
||||
import { sectionPositions } from './rosaryData.js';
|
||||
|
||||
/**
|
||||
* Sets up bidirectional scroll synchronization between the prayer sections,
|
||||
* the SVG rosary visualization, and the mystery image column.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {() => HTMLElement} opts.getSvgContainer - getter for the SVG scroll container
|
||||
* @param {() => object} opts.getSectionElements - getter for the section-name → DOM element map
|
||||
* @param {() => HTMLElement} opts.getMysteryImageContainer - getter for the image column container
|
||||
* @param {() => string} opts.getActiveSection - getter for current active section
|
||||
* @param {(s: string) => void} opts.setActiveSection - setter for active section
|
||||
* @returns {() => void} cleanup function
|
||||
*/
|
||||
export function setupScrollSync({
|
||||
getSvgContainer,
|
||||
getSectionElements,
|
||||
getMysteryImageContainer,
|
||||
getActiveSection,
|
||||
setActiveSection,
|
||||
}) {
|
||||
let scrollLock = null; // 'prayer', 'svg', or 'click'
|
||||
let scrollLockTimeout = null;
|
||||
|
||||
const setScrollLock = (source, duration = 1000) => {
|
||||
scrollLock = source;
|
||||
clearTimeout(scrollLockTimeout);
|
||||
scrollLockTimeout = setTimeout(() => {
|
||||
scrollLock = null;
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// Check if browser supports smooth scrolling
|
||||
const supportsNativeSmoothScroll = (() => {
|
||||
if (!('scrollBehavior' in document.documentElement.style)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const testElement = document.createElement('div');
|
||||
testElement.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
// Smooth scroll polyfill for window scrolling
|
||||
const smoothScrollTo = (targetY, duration = 500) => {
|
||||
if (supportsNativeSmoothScroll) {
|
||||
try {
|
||||
window.scrollTo({ top: targetY, behavior: 'smooth' });
|
||||
return;
|
||||
} catch (e) {
|
||||
// Fall through to polyfill
|
||||
}
|
||||
}
|
||||
|
||||
const startY = window.scrollY || window.pageYOffset;
|
||||
const distance = targetY - startY;
|
||||
const startTime = performance.now();
|
||||
|
||||
const easeInOutQuad = (t) => {
|
||||
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
};
|
||||
|
||||
const scroll = (currentTime) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const ease = easeInOutQuad(progress);
|
||||
window.scrollTo(0, startY + distance * ease);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(scroll);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(scroll);
|
||||
};
|
||||
|
||||
// Smooth scroll polyfill for element scrolling (for SVG container)
|
||||
const smoothScrollElement = (element, targetY, duration = 500) => {
|
||||
if (supportsNativeSmoothScroll) {
|
||||
try {
|
||||
element.scrollTo({ top: targetY, behavior: 'smooth' });
|
||||
return;
|
||||
} catch (e) {
|
||||
// Fall through to polyfill
|
||||
}
|
||||
}
|
||||
|
||||
const startY = element.scrollTop;
|
||||
const distance = targetY - startY;
|
||||
const startTime = performance.now();
|
||||
|
||||
const easeInOutQuad = (t) => {
|
||||
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
};
|
||||
|
||||
const scroll = (currentTime) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const ease = easeInOutQuad(progress);
|
||||
element.scrollTop = startY + distance * ease;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(scroll);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(scroll);
|
||||
};
|
||||
|
||||
// Helper: convert SVG section position to pixel scroll target
|
||||
function svgSectionToPixel(svg, section) {
|
||||
const svgYPosition = sectionPositions[section];
|
||||
if (svgYPosition === undefined) return null;
|
||||
const viewBox = svg.viewBox.baseVal;
|
||||
const svgHeight = svg.clientHeight;
|
||||
const viewBoxHeight = viewBox.height;
|
||||
|
||||
const computedStyle = window.getComputedStyle(svg);
|
||||
const matrix = new DOMMatrix(computedStyle.transform);
|
||||
const cssScale = matrix.a || 1;
|
||||
|
||||
const scale = (svgHeight / viewBoxHeight) * cssScale;
|
||||
return svgYPosition * scale;
|
||||
}
|
||||
|
||||
// Set up Intersection Observer for scroll tracking (prayers → SVG)
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
const svgContainer = getSvgContainer();
|
||||
const sectionElements = getSectionElements();
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && scrollLock !== 'svg' && scrollLock !== 'click') {
|
||||
// Skip observer updates when at the top — handleWindowScroll handles this
|
||||
const scrollY = window.scrollY || window.pageYOffset;
|
||||
if (scrollY < 50) return;
|
||||
|
||||
const section = entry.target.dataset.section;
|
||||
setActiveSection(section);
|
||||
|
||||
// Scroll SVG to keep active section visible at top
|
||||
if (svgContainer && sectionPositions[section] !== undefined) {
|
||||
const svg = svgContainer.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
const pixelPosition = svgSectionToPixel(svg, section);
|
||||
if (pixelPosition === null) return;
|
||||
const targetScroll = pixelPosition - 100;
|
||||
|
||||
setScrollLock('prayer');
|
||||
smoothScrollElement(svgContainer, Math.max(0, targetScroll));
|
||||
}
|
||||
}
|
||||
});
|
||||
}, {
|
||||
root: null,
|
||||
rootMargin: "-20% 0px -60% 0px",
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
// Observe all prayer sections
|
||||
const sectionElements = getSectionElements();
|
||||
Object.values(sectionElements).forEach((el) => {
|
||||
if (el) observer.observe(el);
|
||||
});
|
||||
|
||||
// Detect when user scrolls past all prayers and snap SVG to bottom or top
|
||||
const handleWindowScroll = () => {
|
||||
const svgContainer = getSvgContainer();
|
||||
const sectionElements = getSectionElements();
|
||||
const mysteryImageContainer = getMysteryImageContainer();
|
||||
if (scrollLock === 'svg' || scrollLock === 'click' || !svgContainer) return;
|
||||
|
||||
const viewportHeight = window.innerHeight;
|
||||
const scrollY = window.scrollY || window.pageYOffset;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
|
||||
const firstSection = sectionElements.cross;
|
||||
const finalSection = sectionElements.final_cross;
|
||||
if (!firstSection || !finalSection) return;
|
||||
|
||||
const firstSectionRect = firstSection.getBoundingClientRect();
|
||||
const finalSectionRect = finalSection.getBoundingClientRect();
|
||||
|
||||
if (scrollY < 50) {
|
||||
setActiveSection('cross');
|
||||
if (svgContainer.scrollTop > 10) {
|
||||
setScrollLock('prayer');
|
||||
svgContainer.scrollTop = 0;
|
||||
}
|
||||
if (mysteryImageContainer && mysteryImageContainer.scrollTop > 10) {
|
||||
mysteryImageContainer.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
else if (scrollY + viewportHeight >= documentHeight - 50) {
|
||||
const maxScroll = svgContainer.scrollHeight - svgContainer.clientHeight;
|
||||
if (svgContainer.scrollTop < maxScroll - 10) {
|
||||
setScrollLock('prayer');
|
||||
smoothScrollElement(svgContainer, maxScroll);
|
||||
}
|
||||
}
|
||||
else if (firstSectionRect.top > viewportHeight * 0.6) {
|
||||
if (svgContainer.scrollTop > 10) {
|
||||
setScrollLock('prayer');
|
||||
smoothScrollElement(svgContainer, 0);
|
||||
}
|
||||
if (mysteryImageContainer && mysteryImageContainer.scrollTop > 10) {
|
||||
smoothScrollElement(mysteryImageContainer, 0);
|
||||
}
|
||||
}
|
||||
else if (finalSectionRect.bottom < viewportHeight * 0.4) {
|
||||
const maxScroll = svgContainer.scrollHeight - svgContainer.clientHeight;
|
||||
if (svgContainer.scrollTop < maxScroll - 10) {
|
||||
setScrollLock('prayer');
|
||||
smoothScrollElement(svgContainer, maxScroll);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleWindowScroll, { passive: true });
|
||||
|
||||
// Debounce SVG scroll handler to avoid excessive updates
|
||||
let svgScrollTimeout = null;
|
||||
const handleSvgScroll = () => {
|
||||
const svgContainer = getSvgContainer();
|
||||
const sectionElements = getSectionElements();
|
||||
if (scrollLock === 'prayer' || scrollLock === 'click' || !svgContainer) return;
|
||||
|
||||
clearTimeout(svgScrollTimeout);
|
||||
svgScrollTimeout = setTimeout(() => {
|
||||
const svg = svgContainer.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
const scrollTop = svgContainer.scrollTop;
|
||||
const viewBox = svg.viewBox.baseVal;
|
||||
const svgHeight = svg.clientHeight;
|
||||
const viewBoxHeight = viewBox.height;
|
||||
|
||||
const computedStyle = window.getComputedStyle(svg);
|
||||
const matrix = new DOMMatrix(computedStyle.transform);
|
||||
const cssScale = matrix.a || 1;
|
||||
|
||||
const scale = (svgHeight / viewBoxHeight) * cssScale;
|
||||
const svgY = scrollTop / scale;
|
||||
|
||||
let closestSection = 'cross';
|
||||
let closestDistance = Infinity;
|
||||
|
||||
for (const [section, position] of Object.entries(sectionPositions)) {
|
||||
const distance = Math.abs(svgY - position);
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestSection = section;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestSection !== getActiveSection() && sectionElements[closestSection]) {
|
||||
setActiveSection(closestSection);
|
||||
setScrollLock('svg');
|
||||
|
||||
const element = sectionElements[closestSection];
|
||||
const elementTop = element.getBoundingClientRect().top + window.scrollY;
|
||||
const offset = parseFloat(getComputedStyle(document.documentElement).fontSize) * 3;
|
||||
|
||||
smoothScrollTo(elementTop - offset);
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const svgContainer = getSvgContainer();
|
||||
if (svgContainer) {
|
||||
svgContainer.addEventListener('scroll', handleSvgScroll);
|
||||
}
|
||||
|
||||
// Handle clicks on SVG elements to jump to prayers
|
||||
const handleSvgClick = (e) => {
|
||||
const svgContainer = getSvgContainer();
|
||||
const sectionElements = getSectionElements();
|
||||
let target = e.target;
|
||||
while (target && target !== svgContainer) {
|
||||
const section = target.dataset.section;
|
||||
if (section && sectionElements[section]) {
|
||||
setActiveSection(section);
|
||||
setScrollLock('click', 1500);
|
||||
|
||||
// Scroll the SVG visualization to the clicked section
|
||||
if (sectionPositions[section] !== undefined) {
|
||||
const svg = svgContainer.querySelector('svg');
|
||||
if (svg) {
|
||||
const pixelPosition = svgSectionToPixel(svg, section);
|
||||
if (pixelPosition !== null) {
|
||||
const targetScroll = pixelPosition - 100;
|
||||
smoothScrollElement(svgContainer, Math.max(0, targetScroll));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll the prayers to the corresponding section
|
||||
const element = sectionElements[section];
|
||||
const elementTop = element.getBoundingClientRect().top + window.scrollY;
|
||||
const offset = parseFloat(getComputedStyle(document.documentElement).fontSize) * 3;
|
||||
|
||||
smoothScrollTo(elementTop - offset);
|
||||
|
||||
break;
|
||||
}
|
||||
target = target.parentElement;
|
||||
}
|
||||
};
|
||||
|
||||
const svg = svgContainer?.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.addEventListener('click', handleSvgClick);
|
||||
svg.style.cursor = 'pointer';
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
clearTimeout(scrollLockTimeout);
|
||||
clearTimeout(svgScrollTimeout);
|
||||
window.removeEventListener('scroll', handleWindowScroll);
|
||||
if (svgContainer) {
|
||||
svgContainer.removeEventListener('scroll', handleSvgScroll);
|
||||
}
|
||||
if (svg) {
|
||||
svg.removeEventListener('click', handleSvgClick);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user