diff --git a/src/lib/components/prayers/AblassGebete.svelte b/src/lib/components/prayers/AblassGebete.svelte new file mode 100644 index 0000000..dc822bc --- /dev/null +++ b/src/lib/components/prayers/AblassGebete.svelte @@ -0,0 +1,64 @@ + + + + {#snippet children(showLatin, urlLang)} +
+

+ + Seele Christi, heilige mich. + + Leib Christi erlöse mich. + + Blut Christi, tränke mich. + + Wasser der Seite Christi, wasche mich. + + Leiden Christi, stärke mich. + + O gütiger Jesus, erhöre mich. + + Verbirg in Deine Wunden mich. + + Von Dir lass nimmer scheiden mich. + + In meiner Todesstunde rufe mich, + + Und heisse zur Dir kommen mich, + + Damit ich möge loben Dich + + Mit Deinen Heiligen ewiglich. Amen. + +

+
+

Vollkommener Ablass

+

Paternoster

+ {#if verbose } + + {/if} +

Ave Maria

+ {#if verbose } + + {/if} +

Gloria Patri

+ {#if verbose } + + {/if} +

+ {#if showLatin} + En ego, o bone et dulcíssime Jesu, ante contspéctum tuum génibusme provólvo ac máximo ánimi ardóre te oro atque obtéstor, ut meum in in cor vívidos fídei, spei et caritátis sensus, atque veram peccatórum meórum pœniténtiam, éaque emendándi firmíssimam voluntátem velis imprímere; dum magno ánimi afféctu et dolóre tua quinque vúlnera mecum ipse consídero ac mente contémplor, illud præ óculis habens, quod jam in ore ponébat tuo David Prophéta de te, o bone Jesu: Fodérunt manus meas et pedes meos; dinumeravérunt ómnio ossa mea (Ps. 21, 17-18) + {/if} + + Siehe, o gütiger und milder Jesus, ich werfe mich vor Deinen Augen auf die Knie. Inbrünstig bitte und beschwöre ich Dich: Präge meinem Herzen lebendige Gefühle des Glaubens, der Hoffung und der Liebe ein sowie wahre Reue über meine Sünden und den ganz festen Willen, mich zu bessern. Voll Liebe und Schwerz schaue ich Deine fünf Wunden und betrachte sie in meinem Geiste. Dabei halte ich mir vor Augen, was im Hinblick auf Dich, o guter Jesus, schon der Prophet David Dir in den Mund legte: «Sie haben Meine Hände und Meine Füsse durchbohrt; alle meine Gebeine haben sie gezählt.» (Ps. 21, 17-18) + + +

+ {/snippet} +
diff --git a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/+page.svelte b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/+page.svelte index 9f9a97f..98d904f 100644 --- a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/+page.svelte +++ b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/+page.svelte @@ -19,6 +19,8 @@ import BruderKlausGebet from "$lib/components/prayers/BruderKlausGebet.svelte"; import JosephGebet from "$lib/components/prayers/JosephGebet.svelte"; import Confiteor from "$lib/components/prayers/Confiteor.svelte"; + import AblassGebete from "$lib/components/prayers/AblassGebete.svelte"; + import Prayer from "$lib/components/prayers/Prayer.svelte"; let { data } = $props(); @@ -55,7 +57,8 @@ confiteor: isEnglish ? 'The Confiteor' : 'Das Confiteor', searchPlaceholder: isEnglish ? 'Search prayers...' : 'Gebete suchen...', clearSearch: isEnglish ? 'Clear search' : 'Suche löschen', - textMatch: isEnglish ? 'Match in prayer text' : 'Treffer im Gebetstext' + textMatch: isEnglish ? 'Match in prayer text' : 'Treffer im Gebetstext', + ablassgebete: 'Ablassgebete' }); // JS-only search (hidden without JS) @@ -88,7 +91,8 @@ { id: 'michael', searchTerms: ['michael', 'archangel', 'erzengel', 'satan', 'devil', 'teufel'], slug: isEnglish ? 'prayer-to-st-michael-the-archangel' : 'gebet-zum-hl-erzengel-michael' }, { id: 'bruderKlaus', searchTerms: ['bruder klaus', 'nicholas', 'niklaus', 'flüe'], slug: isEnglish ? 'prayer-of-st-nicholas-of-flue' : 'bruder-klaus-gebet' }, { id: 'joseph', searchTerms: ['joseph', 'josef', 'pius'], slug: isEnglish ? 'prayer-to-st-joseph-by-pope-st-pius-x' : 'josephgebet-des-hl-papst-pius-x' }, - { id: 'confiteor', searchTerms: ['confiteor', 'i confess', 'ich bekenne', 'mea culpa'], slug: isEnglish ? 'the-confiteor' : 'das-confiteor' } + { id: 'confiteor', searchTerms: ['confiteor', 'i confess', 'ich bekenne', 'mea culpa'], slug: isEnglish ? 'the-confiteor' : 'das-confiteor' }, + { id: 'ablassgebete', searchTerms: ['ablass', 'kommunion'], slug: 'ablassgebete' } ]); // Base URL for prayer links @@ -108,7 +112,8 @@ michael: labels.michael, bruderKlaus: labels.bruderKlaus, joseph: labels.joseph, - confiteor: labels.confiteor + confiteor: labels.confiteor, + ablassgebete: labels.ablassgebete }; return nameMap[id] || id; } @@ -316,6 +321,12 @@ h1{

{labels.gloriaIntro}

+ {:else if prayer.id === 'ablassgebete'} + {#if data.lang === 'de'} + + + + {/if} {:else} {#if prayer.id === 'signOfCross'} diff --git a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.server.ts b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.server.ts index 7ffb10d..71cbf22 100644 --- a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.server.ts +++ b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.server.ts @@ -14,7 +14,8 @@ const validSlugs = new Set([ 'gebet-zum-hl-erzengel-michael', 'prayer-to-st-michael-the-archangel', 'bruder-klaus-gebet', 'prayer-of-st-nicholas-of-flue', 'josephgebet-des-hl-papst-pius-x', 'prayer-to-st-joseph-by-pope-st-pius-x', - 'das-confiteor', 'the-confiteor' + 'das-confiteor', 'the-confiteor', + 'ablassgebete', ]); export const load: PageServerLoad = async ({ params, url }) => { @@ -22,6 +23,10 @@ export const load: PageServerLoad = async ({ params, url }) => { throw error(404, 'Prayer not found'); } + if (params.faithLang === 'faith' && params.prayer === 'ablassgebete') { + throw error(404, 'Prayer not found'); + } + const latinParam = url.searchParams.get('latin'); const hasUrlLatin = latinParam !== null; const initialLatin = hasUrlLatin ? latinParam !== '0' : true; diff --git a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.svelte b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.svelte index 17aa601..2c9c03f 100644 --- a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.svelte +++ b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.svelte @@ -16,6 +16,7 @@ import BruderKlausGebet from "$lib/components/prayers/BruderKlausGebet.svelte"; import JosephGebet from "$lib/components/prayers/JosephGebet.svelte"; import Confiteor from "$lib/components/prayers/Confiteor.svelte"; + import AblassGebete from "$lib/components/prayers/AblassGebete.svelte"; let { data } = $props(); @@ -49,7 +50,8 @@ 'josephgebet-des-hl-papst-pius-x': { id: 'joseph', name: isEnglish ? 'Prayer to St. Joseph by Pope St. Pius X' : 'Josephgebet des hl. Papst Pius X', bilingue: false }, 'prayer-to-st-joseph-by-pope-st-pius-x': { id: 'joseph', name: isEnglish ? 'Prayer to St. Joseph by Pope St. Pius X' : 'Josephgebet des hl. Papst Pius X', bilingue: false }, 'das-confiteor': { id: 'confiteor', name: isEnglish ? 'The Confiteor' : 'Das Confiteor', bilingue: true }, - 'the-confiteor': { id: 'confiteor', name: isEnglish ? 'The Confiteor' : 'Das Confiteor', bilingue: true } + 'the-confiteor': { id: 'confiteor', name: isEnglish ? 'The Confiteor' : 'Das Confiteor', bilingue: true }, + 'ablassgebete': { id: 'ablassgebete', name: 'Ablassgebete', bilingue: true } }); const prayer = $derived(prayerDefs[data.prayer]); @@ -64,11 +66,150 @@ // Toggle href for no-JS fallback (navigates to opposite latin state) const latinToggleHref = $derived(data.initialLatin ? '?latin=0' : '?'); + // PiP drag-to-corner logic + let pipEl = $state(null); + let corner = $state('bottom-right'); + let dragging = $state(false); + let enlarged = $state(false); + let dragOffset = { x: 0, y: 0 }; + let dragPos = $state({ x: 0, y: 0 }); + let dragMoved = false; + let lastTapTime = 0; + const MARGIN = 16; + const TAP_THRESHOLD = 10; // px movement to distinguish tap from drag + const DOUBLE_TAP_MS = 400; + + function getCornerPos(c, el) { + const vw = window.innerWidth; + const vh = window.innerHeight; + const r = el.getBoundingClientRect(); + const positions = { + 'top-left': { x: MARGIN, y: MARGIN }, + 'top-right': { x: vw - r.width - MARGIN, y: MARGIN }, + 'bottom-left': { x: MARGIN, y: vh - r.height - MARGIN }, + 'bottom-right': { x: vw - r.width - MARGIN, y: vh - r.height - MARGIN }, + }; + return positions[c]; + } + + function snapToCorner(el, c) { + const pos = getCornerPos(c, el); + corner = c; + dragPos = pos; + el.style.transition = 'transform 0.25s ease'; + el.style.transform = `translate(${pos.x}px, ${pos.y}px)`; + el.addEventListener('transitionend', () => { el.style.transition = ''; }, { once: true }); + } + + function nearestCorner(x, y, el) { + const vw = window.innerWidth; + const vh = window.innerHeight; + const r = el.getBoundingClientRect(); + const cx = x + r.width / 2; + const cy = y + r.height / 2; + const left = cx < vw / 2; + const top = cy < vh / 2; + return `${top ? 'top' : 'bottom'}-${left ? 'left' : 'right'}`; + } + + function onPointerDown(e) { + if (!pipEl || window.matchMedia('(min-width: 1024px)').matches) return; + dragging = true; + dragMoved = false; + const r = pipEl.getBoundingClientRect(); + dragOffset = { x: e.clientX - r.left, y: e.clientY - r.top }; + pipEl.setPointerCapture(e.pointerId); + pipEl.style.transition = ''; + e.preventDefault(); + } + + function onPointerMove(e) { + if (!dragging || !pipEl) return; + const x = e.clientX - dragOffset.x; + const y = e.clientY - dragOffset.y; + if (!dragMoved) { + const dx = Math.abs(x - dragPos.x); + const dy = Math.abs(y - dragPos.y); + if (dx > TAP_THRESHOLD || dy > TAP_THRESHOLD) dragMoved = true; + } + dragPos = { x, y }; + pipEl.style.transform = `translate(${x}px, ${y}px)`; + } + + function toggleEnlarged() { + if (!pipEl) return; + const rect = pipEl.getBoundingClientRect(); + const vh = window.innerHeight / 100; + const currentH = enlarged ? 37.5 * vh : 25 * vh; + const targetH = enlarged ? 25 * vh : 37.5 * vh; + const ratio = targetH / currentH; + + enlarged = !enlarged; + + // Calculate new size and keep the anchored corner fixed + const newW = rect.width * ratio; + const newH = rect.height * ratio; + let newX = rect.left; + let newY = rect.top; + if (corner.includes('right')) newX = rect.right - newW; + if (corner.includes('bottom')) newY = rect.bottom - newH; + + dragPos = { x: newX, y: newY }; + pipEl.style.transition = 'transform 0.25s ease'; + pipEl.style.transform = `translate(${newX}px, ${newY}px)`; + pipEl.addEventListener('transitionend', () => { + pipEl.style.transition = ''; + }, { once: true }); + } + + function onPointerUp(e) { + if (!dragging || !pipEl) return; + dragging = false; + + if (!dragMoved) { + // It was a tap, check for double-tap + const now = Date.now(); + if (now - lastTapTime < DOUBLE_TAP_MS) { + lastTapTime = 0; + toggleEnlarged(); + return; + } + lastTapTime = now; + } + + const r = pipEl.getBoundingClientRect(); + snapToCorner(pipEl, nearestCorner(r.left, r.top, pipEl)); + } + + function onResize() { + if (!pipEl) return; + const isDesktop = window.matchMedia('(min-width: 1024px)').matches; + if (isDesktop) { + pipEl.style.opacity = ''; + return; + } + const pos = getCornerPos(corner, pipEl); + dragPos = pos; + pipEl.style.transform = `translate(${pos.x}px, ${pos.y}px)`; + pipEl.style.opacity = '1'; + } + onMount(() => { // Clean up URL params after hydration (state is now in component state) if (window.location.search) { history.replaceState({}, '', window.location.pathname); } + + // Initial position for PiP + if (pipEl && !window.matchMedia('(min-width: 1024px)').matches) { + const pos = getCornerPos(corner, pipEl); + dragPos = pos; + pipEl.style.transform = `translate(${pos.x}px, ${pos.y}px)`; + pipEl.style.opacity = '1'; + } + + window.addEventListener('resize', onResize); + return () => window.removeEventListener('resize', onResize); }); @@ -106,6 +247,12 @@ h1 { :global(.bilingue v:lang(de)) { color: grey; } +:global(.bilingue .monolingual v:lang(de)) { + color: inherit; +} +:global(.bilingue .monolingual v:lang(de) i) { + color: var(--nord11); +} :global(.gebet i) { font-style: normal; color: var(--nord11); @@ -126,8 +273,126 @@ h1 { background-color: var(--nord5); } } +.crucifix-layout { + display: flex; + flex-direction: column; + align-items: center; + margin: auto; + padding: 0 1em; +} +.crucifix-layout .crucifix-wrap { + position: fixed; + top: 0; + left: 0; + z-index: 10000; + width: auto; + opacity: 0; + touch-action: none; + cursor: grab; + user-select: none; +} +.crucifix-layout .crucifix-wrap:active { + cursor: grabbing; +} +.crucifix-layout .crucifix-wrap img { + height: 25vh; + width: auto; + object-fit: contain; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + pointer-events: none; + transition: height 0.25s ease; +} +.crucifix-layout .crucifix-wrap.enlarged img { + height: 37.5vh; +} +.crucifix-layout .prayer-scroll { + width: 100%; + max-width: 700px; +} +@media (min-width: 1024px) { + .crucifix-layout { + flex-direction: row; + align-items: flex-start; + gap: 2em; + } + .crucifix-layout .prayer-scroll { + flex: 0 1 700px; + } + .crucifix-layout .crucifix-wrap { + position: sticky; + top: 4rem; + left: auto; + transform: none !important; + opacity: 1; + flex: 1; + background-color: transparent; + padding: 0; + order: 1; + cursor: default; + touch-action: auto; + user-select: auto; + } + .crucifix-layout .crucifix-wrap img { + max-height: calc(100vh - 4rem); + height: auto; + width: 100%; + object-fit: contain; + border-radius: 0; + box-shadow: none; + } +} +@media (prefers-color-scheme: light) { + .crucifix-layout .crucifix-wrap { + background-color: var(--nord5); + } +} +@media (prefers-color-scheme: light) and (min-width: 1024px) { + .crucifix-layout .crucifix-wrap { + background-color: transparent; + } +} +@media (min-width: 1400px) { + .crucifix-layout::before { + content: ''; + flex: 1; + order: -1; + } +} + {#if prayerId === 'ablassgebete'} +

{prayerName}

+ +
+ +
+ +
+ +
+ Crucifix +
+
+
+
+ +
+
+
+
+ {:else}

{prayerName}

@@ -139,6 +404,7 @@ h1 { />
+
{#if prayerId === 'gloria'}

{gloriaIntro}

@@ -172,3 +438,4 @@ h1 {
+{/if} diff --git a/static/glaube/crucifix.jpg b/static/glaube/crucifix.jpg new file mode 100644 index 0000000..60473f1 Binary files /dev/null and b/static/glaube/crucifix.jpg differ diff --git a/static/glaube/crucifix.webp b/static/glaube/crucifix.webp new file mode 100644 index 0000000..b162486 Binary files /dev/null and b/static/glaube/crucifix.webp differ