diff --git a/src/lib/components/LanguageSelector.svelte b/src/lib/components/LanguageSelector.svelte index b95dadf..4b524a2 100644 --- a/src/lib/components/LanguageSelector.svelte +++ b/src/lib/components/LanguageSelector.svelte @@ -6,7 +6,7 @@ import { convertFitnessPath } from '$lib/js/fitnessI18n'; import { onMount } from 'svelte'; - let { lang = undefined }: { lang?: 'de' | 'en' } = $props(); + 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); @@ -17,10 +17,17 @@ // Faith subroute mappings const faithSubroutes: Record> = { - en: { gebete: 'prayers', rosenkranz: 'rosary' }, - de: { prayers: 'gebete', rosary: 'rosenkranz' } + en: { gebete: 'prayers', rosenkranz: 'rosary', rosarium: 'rosary', orationes: 'prayers' }, + de: { prayers: 'gebete', rosary: 'rosenkranz', rosarium: 'rosenkranz', orationes: 'gebete' }, + la: { prayers: 'orationes', gebete: 'orationes', rosary: 'rosarium', rosenkranz: 'rosarium' } }; + // Whether the current page is a faith route (show LA option) + 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; @@ -30,6 +37,8 @@ languageStore.set('en'); } else if (path.startsWith('/rezepte') || path.startsWith('/glaube')) { languageStore.set('de'); + } else if (path.startsWith('/fides')) { + // Latin route — no language switching needed } else if (path.startsWith('/fitness')) { // Language is determined by sub-route slugs; don't override store } else { @@ -45,11 +54,11 @@ isOpen = !isOpen; } - function convertFaithPath(path: string, targetLang: 'de' | 'en'): string { - const faithMatch = path.match(/^\/(glaube|faith)(\/(.+))?$/); + function convertFaithPath(path: string, targetLang: 'de' | 'en' | 'la'): string { + const faithMatch = path.match(/^\/(glaube|faith|fides)(\/(.+))?$/); if (!faithMatch) return path; - const targetBase = targetLang === 'en' ? 'faith' : 'glaube'; + const targetBase = targetLang === 'la' ? 'fides' : targetLang === 'en' ? 'faith' : 'glaube'; const rest = faithMatch[3]; // e.g., "gebete", "rosenkranz/sub", "angelus" if (!rest) { @@ -63,14 +72,14 @@ } // Compute target paths for each language (used as href for no-JS) - function computeTargetPath(targetLang: 'de' | 'en'): string { + function computeTargetPath(targetLang: 'de' | 'en' | 'la'): string { const path = currentPath || $page.url.pathname; - if (path.startsWith('/glaube') || path.startsWith('/faith')) { + if (path.startsWith('/glaube') || path.startsWith('/faith') || path.startsWith('/fides')) { return convertFaithPath(path, targetLang); } - if (path.startsWith('/fitness')) { + if (path.startsWith('/fitness') && targetLang !== 'la') { return convertFitnessPath(path, targetLang); } @@ -94,15 +103,18 @@ const dePath = $derived(computeTargetPath('de')); const enPath = $derived(computeTargetPath('en')); + const laPath = $derived(computeTargetPath('la')); - async function switchLanguage(lang: 'de' | 'en') { + async function switchLanguage(lang: 'de' | 'en' | 'la') { isOpen = false; - // Update the shared language store immediately - languageStore.set(lang); + // Update the shared language store immediately (la not tracked in store) + if (lang !== 'la') { + languageStore.set(lang); + } // Store preference - if (typeof localStorage !== 'undefined') { + if (typeof localStorage !== 'undefined' && lang !== 'la') { localStorage.setItem('preferredLanguage', lang); } @@ -112,14 +124,14 @@ // For pages that handle their own translations inline (not recipe/faith routes), // dispatch event and stay on the page if (!path.startsWith('/rezepte') && !path.startsWith('/recipes') - && !path.startsWith('/glaube') && !path.startsWith('/faith') + && !path.startsWith('/glaube') && !path.startsWith('/faith') && !path.startsWith('/fides') && !path.startsWith('/fitness')) { window.dispatchEvent(new CustomEvent('languagechange', { detail: { lang } })); return; } // Handle faith pages - if (path.startsWith('/glaube') || path.startsWith('/faith')) { + if (path.startsWith('/glaube') || path.startsWith('/faith') || path.startsWith('/fides')) { const newPath = convertFaithPath(path, lang); await goto(newPath); return; @@ -313,6 +325,15 @@ > EN + {#if isFaithRoute} + { e.preventDefault(); switchLanguage('la'); }} + > + LA + + {/if} diff --git a/src/lib/components/faith/AngelusStreakCounter.svelte b/src/lib/components/faith/AngelusStreakCounter.svelte new file mode 100644 index 0000000..64a78df --- /dev/null +++ b/src/lib/components/faith/AngelusStreakCounter.svelte @@ -0,0 +1,273 @@ + + +
+
+ + + {displayStreak}{#if showFraction}{partialCount}/3{/if} + + + {labels.days} +
+ +
+
+ {#each slots as slot} + + {/each} +
+ +
{ e.preventDefault(); pray(); }}> + + +
+
+
+ + diff --git a/src/lib/components/faith/StickyImage.svelte b/src/lib/components/faith/StickyImage.svelte index e4404bc..f70a922 100644 --- a/src/lib/components/faith/StickyImage.svelte +++ b/src/lib/components/faith/StickyImage.svelte @@ -8,7 +8,7 @@ * - 'layout': flex row on desktop (image sticky right, content left). Use as page-level wrapper. * - 'overlay': image floats over the page (fixed position, IntersectionObserver show/hide). Use when nested inside existing layouts. */ - let { src, alt = '', mode = 'layout', children } = $props(); + let { src, alt = '', mode = 'layout', caption = '', children } = $props(); /** @type {HTMLDivElement | null} */ let pipEl = $state(null); @@ -86,6 +86,9 @@
+ {#if caption} +
{@html caption}
+ {/if}
@@ -107,6 +110,11 @@ .image-wrap-desktop { display: none; } +.image-caption { + font-size: 0.8rem; + color: var(--color-text-secondary); + margin-top: 0.4rem; +} .content-scroll { width: 100%; max-width: 700px; diff --git a/src/lib/components/faith/StreakAura.svelte b/src/lib/components/faith/StreakAura.svelte index bcc739e..ea19cb9 100644 --- a/src/lib/components/faith/StreakAura.svelte +++ b/src/lib/components/faith/StreakAura.svelte @@ -1,12 +1,14 @@ @@ -31,8 +38,13 @@ function isActive(path) {
{#snippet links()} {/snippet} diff --git a/src/routes/[faithLang=faithLang]/+page.svelte b/src/routes/[faithLang=faithLang]/+page.svelte index 9431631..e03cdfe 100644 --- a/src/routes/[faithLang=faithLang]/+page.svelte +++ b/src/routes/[faithLang=faithLang]/+page.svelte @@ -1,18 +1,23 @@ @@ -71,8 +76,15 @@

{labels.rosary}

- - -

Angelus

-
+ {#if eastertide} + + +

Regína Cæli

+
+ {:else} + + +

Angelus

+
+ {/if} diff --git a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/+page.svelte b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/+page.svelte index 45a9e6c..f7d636a 100644 --- a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/+page.svelte +++ b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/+page.svelte @@ -32,41 +32,47 @@ let { data } = $props(); // Create language context for prayer components - const langContext = createLanguageContext({ urlLang: /** @type {'de' | 'en'} */(data.lang), initialLatin: data.initialLatin }); + const langContext = createLanguageContext({ urlLang: /** @type {'de' | 'en'} */(data.lang), initialLatin: data.lang === 'la' ? true : data.initialLatin }); // Update lang store when data.lang changes (e.g., after navigation) $effect(() => { langContext.lang.set(data.lang); + if (data.lang === 'la') { + langContext.showLatin.set(true); + } }); // Reactive isEnglish based on data.lang const isEnglish = $derived(data.lang === 'en'); + const isLatin = $derived(data.lang === 'la'); const labels = $derived({ - title: isEnglish ? 'Prayers' : 'Gebete', - description: isEnglish - ? 'Catholic prayers in Latin and English.' - : 'Katholische Gebete auf Deutsch und Latein.', - signOfCross: isEnglish ? 'The Sign of the Cross' : 'Das heilige Kreuzzeichen', + title: isLatin ? 'Orationes' : isEnglish ? 'Prayers' : 'Gebete', + description: isLatin + ? 'Orationes catholicae in lingua Latina.' + : isEnglish + ? 'Catholic prayers in Latin and English.' + : 'Katholische Gebete auf Deutsch und Latein.', + signOfCross: isLatin ? 'Signum Crucis' : isEnglish ? 'The Sign of the Cross' : 'Das heilige Kreuzzeichen', gloriaPatri: 'Glória Patri', - paternoster: isEnglish ? 'Our Father' : 'Paternoster', - credo: isEnglish ? 'Nicene Creed' : 'Credo', - aveMaria: isEnglish ? 'Hail Mary' : 'Ave Maria', + paternoster: isLatin ? 'Pater Noster' : isEnglish ? 'Our Father' : 'Paternoster', + credo: 'Credo', + aveMaria: 'Ave Maria', salveRegina: 'Salve Regina', - fatima: isEnglish ? 'Fatima Prayer' : 'Das Fatimagebet', + fatima: isLatin ? 'Oratio Fatimensis' : isEnglish ? 'Fatima Prayer' : 'Das Fatimagebet', gloria: 'Glória', - michael: isEnglish ? 'Prayer to St. Michael the Archangel' : 'Gebet zum hl. Erzengel Michael', + michael: isLatin ? 'Oratio ad S. Michaëlem Archangelum' : isEnglish ? 'Prayer to St. Michael the Archangel' : 'Gebet zum hl. Erzengel Michael', bruderKlaus: isEnglish ? 'Prayer of St. Nicholas of Flüe' : 'Bruder Klaus Gebet', joseph: isEnglish ? 'Prayer to St. Joseph by Pope St. Pius X' : 'Josephgebet des hl. Papst Pius X', - 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', - postcommunio: isEnglish ? 'Postcommunio Prayers' : 'Nachkommuniongebete', + confiteor: isLatin ? 'Confiteor' : isEnglish ? 'The Confiteor' : 'Das Confiteor', + searchPlaceholder: isLatin ? 'Orationes quaerere...' : isEnglish ? 'Search prayers...' : 'Gebete suchen...', + clearSearch: isLatin ? 'Quaestionem delere' : isEnglish ? 'Clear search' : 'Suche löschen', + textMatch: isLatin ? 'In textu orationis' : isEnglish ? 'Match in prayer text' : 'Treffer im Gebetstext', + postcommunio: isLatin ? 'Orationes post Communionem' : isEnglish ? 'Postcommunio Prayers' : 'Nachkommuniongebete', animachristi: 'Ánima Christi', - prayerbeforeacrucifix: isEnglish ? 'Prayer Before a Crucifix' : 'Gebet vor einem Kruzifix', - guardianAngel: isEnglish ? 'Guardian Angel Prayer' : 'Schutzengel-Gebet', - apostlesCreed: isEnglish ? "Apostles' Creed" : 'Apostolisches Glaubensbekenntnis', + prayerbeforeacrucifix: isLatin ? 'Oratio ante Crucifixum' : isEnglish ? 'Prayer Before a Crucifix' : 'Gebet vor einem Kruzifix', + guardianAngel: isLatin ? 'Angele Dei' : isEnglish ? 'Guardian Angel Prayer' : 'Schutzengel-Gebet', + apostlesCreed: isLatin ? 'Symbolum Apostolorum' : isEnglish ? "Apostles' Creed" : 'Apostolisches Glaubensbekenntnis', tantumErgo: 'Tantum Ergo', angelus: 'Angelus', reginaCaeli: 'Regína Cæli' @@ -76,12 +82,12 @@ // when corresponding prayers are added to the collection const categories = [ - { id: 'essential', de: 'Grundgebete', en: 'Essential' }, - { id: 'marian', de: 'Marianisch', en: 'Marian' }, - { id: 'saints', de: 'Heilige', en: 'Saints' }, - { id: 'eucharistic', de: 'Eucharistie', en: 'Eucharistic' }, - { id: 'praise', de: 'Lobpreis', en: 'Praise' }, - { id: 'penitential', de: 'Busse', en: 'Penitential' }, + { id: 'essential', de: 'Grundgebete', en: 'Essential', la: 'Fundamentales' }, + { id: 'marian', de: 'Marianisch', en: 'Marian', la: 'Mariana' }, + { id: 'saints', de: 'Heilige', en: 'Saints', la: 'Sancti' }, + { id: 'eucharistic', de: 'Eucharistie', en: 'Eucharistic', la: 'Eucharistica' }, + { id: 'praise', de: 'Lobpreis', en: 'Praise', la: 'Laudatio' }, + { id: 'penitential', de: 'Busse', en: 'Penitential', la: 'Paenitentialia' }, ]; const prayerCategories = { @@ -163,7 +169,7 @@ ]); // Base URL for prayer links - const baseUrl = $derived(isEnglish ? '/faith/prayers' : '/glaube/gebete'); + const baseUrl = $derived(isLatin ? '/fides/orationes' : isEnglish ? '/faith/prayers' : '/glaube/gebete'); // Get prayer name by ID (reactive based on language) /** @param {string} id */ @@ -268,6 +274,10 @@ // Filtered by category, then sorted by search match const filteredPrayers = $derived.by(() => { let result = prayers; + // Latin route: only show prayers that have Latin text + if (isLatin) { + result = result.filter(p => prayerMeta[p.id]?.bilingue !== false); + } if (selectedCategory) { const cat = selectedCategory; result = result.filter(p => /** @type {Record} */(prayerCategories)[p.id]?.includes(cat)); @@ -475,6 +485,7 @@ h1{

{labels.title}

+{#if !isLatin}
+{/if} -
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 424c496..eda8f0d 100644 --- a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.server.ts +++ b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.server.ts @@ -1,8 +1,10 @@ -import type { PageServerLoad } from "./$types"; +import type { PageServerLoad, Actions } from "./$types"; import { error } from "@sveltejs/kit"; import { validPrayerSlugs } from '$lib/data/prayerSlugs'; -export const load: PageServerLoad = async ({ params, url }) => { +const angelusSlugs = new Set(['angelus', 'regina-caeli']); + +export const load: PageServerLoad = async ({ params, url, locals, fetch }) => { if (!validPrayerSlugs.has(params.prayer)) { throw error(404, 'Prayer not found'); } @@ -11,9 +13,89 @@ export const load: PageServerLoad = async ({ params, url }) => { const hasUrlLatin = latinParam !== null; const initialLatin = hasUrlLatin ? latinParam !== '0' : true; - return { + const result: Record = { prayer: params.prayer, initialLatin, hasUrlLatin }; + + // Fetch angelus streak data for angelus/regina-caeli pages + if (angelusSlugs.has(params.prayer)) { + const session = await locals.auth(); + if (session?.user?.nickname) { + try { + const res = await fetch('/api/glaube/angelus-streak'); + if (res.ok) { + result.angelusStreak = await res.json(); + } + } catch { + // Fail silently — streak will use localStorage + } + } + } + + return result; +}; + +export const actions: Actions = { + 'pray-angelus': async ({ request, locals, fetch }) => { + const session = await locals.auth(); + if (!session?.user?.nickname) { + throw error(401, 'Authentication required'); + } + + const formData = await request.formData(); + const time = formData.get('time') as string; + + if (!['morning', 'noon', 'evening'].includes(time)) { + throw error(400, 'Invalid time slot'); + } + + const bits: Record = { morning: 1, noon: 2, evening: 4 }; + const today = new Date().toISOString().split('T')[0]; + + // Fetch current state + let current = { streak: 0, lastComplete: null as string | null, todayPrayed: 0, todayDate: null as string | null }; + try { + const res = await fetch('/api/glaube/angelus-streak'); + if (res.ok) { + current = await res.json(); + } + } catch { + // Start fresh + } + + // Reset if date rolled over + if (current.todayDate !== today) { + current.todayPrayed = 0; + current.todayDate = today; + } + + // Set the bit + current.todayPrayed |= bits[time]; + current.todayDate = today; + + // Check completion + if (current.todayPrayed === 7) { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayStr = yesterday.toISOString().split('T')[0]; + + if (current.lastComplete === yesterdayStr) { + current.streak += 1; + } else if (current.lastComplete !== today) { + current.streak = 1; + } + current.lastComplete = today; + } + + // Save + await fetch('/api/glaube/angelus-streak', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(current) + }); + + return { success: true }; + } }; diff --git a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.svelte b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.svelte index 001e2a7..667ef6d 100644 --- a/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.svelte +++ b/src/routes/[faithLang=faithLang]/[prayers=prayersLang]/[prayer]/+page.svelte @@ -23,16 +23,21 @@ import AngelusComponent from "$lib/components/faith/prayers/Angelus.svelte"; import ReginaCaeli from "$lib/components/faith/prayers/ReginaCaeli.svelte"; import StickyImage from "$lib/components/faith/StickyImage.svelte"; + import AngelusStreakCounter from "$lib/components/faith/AngelusStreakCounter.svelte"; let { data } = $props(); - const langContext = createLanguageContext({ urlLang: /** @type {'de' | 'en'} */(data.lang), initialLatin: data.initialLatin }); + const langContext = createLanguageContext({ urlLang: /** @type {'de' | 'en'} */(data.lang), initialLatin: data.lang === 'la' ? true : data.initialLatin }); $effect(() => { langContext.lang.set(data.lang); + if (data.lang === 'la') { + langContext.showLatin.set(true); + } }); const isEnglish = $derived(data.lang === 'en'); + const isLatin = $derived(data.lang === 'la'); // Prayer definitions with slugs const prayerDefs = $derived({ @@ -74,6 +79,12 @@ const prayerName = $derived(prayer?.name || data.prayer); const isBilingue = $derived(prayer?.bilingue ?? true); const prayerId = $derived(prayer?.id); + const isAngelusPage = $derived(prayerId === 'angelus' || prayerId === 'reginaCaeli'); + + const angelusImageCaption = $derived(prayerId === 'reginaCaeli' + ? { artist: 'Diego Velázquez', title: isEnglish ? 'Coronation of the Virgin' : 'Die Krönung der Jungfrau', year: 1641 } + : { artist: 'Bartolomé Esteban Murillo', title: isEnglish ? 'The Annunciation' : 'Die Verkündigung', year: /** @type {number | null} */(null) } + ); const gloriaIntro = $derived(isEnglish ? 'This ancient hymn begins with the words the angels used to celebrate the newborn Savior. It first praises God the Father, then God the Son; it concludes with homage to the Most Holy Trinity, during which one makes the sign of the cross.' @@ -154,10 +165,11 @@ h1 { background-color: var(--nord5); } - {#if prayerId === 'postcommunio' || prayerId === 'prayerbeforeacrucifix'} + {#if prayerId === 'postcommunio' || prayerId === 'prayerbeforeacrucifix' || isAngelusPage}

{prayerName}

+ {#if !isLatin}
+ {/if} - + {#if isAngelusPage} + + {/if} + + ${angelusImageCaption.title}${angelusImageCaption.year ? `, ${angelusImageCaption.year}` : ''}` : ''} + >
{#if prayerId === 'postcommunio'} - {:else} + {:else if prayerId === 'prayerbeforeacrucifix'} + {:else if prayerId === 'angelus'} + + {:else if prayerId === 'reginaCaeli'} + {/if}
@@ -181,6 +210,7 @@ h1 {

{prayerName}

+ {#if !isLatin}
+ {/if}
diff --git a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.server.ts b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.server.ts index 8f23f3d..482b993 100644 --- a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.server.ts +++ b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.server.ts @@ -88,7 +88,7 @@ export const load: PageServerLoad = async ({ url, fetch, locals, params }) => { } return { - mysteryDescriptions: params.faithLang === 'faith' ? mysteryVerseDataEn : mysteryVerseDataDe, + mysteryDescriptions: params.faithLang === 'glaube' ? mysteryVerseDataDe : mysteryVerseDataEn, streakData, isLoggedIn: !!session?.user?.nickname, initialMystery, diff --git a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte index d338a61..85f04cf 100644 --- a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte +++ b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte @@ -24,7 +24,7 @@ import RosarySvg from "./RosarySvg.svelte"; import MysterySelector from "./MysterySelector.svelte"; import MysteryImageColumn from "./MysteryImageColumn.svelte"; /** @typedef {import('./rosaryData.js').MysteryType} MysteryType */ -import { mysteries, mysteriesLatin, mysteriesEnglish, mysteryTitles, mysteryTitlesEnglish, allMysteryImages, getLabels, getMysteryForWeekday, BEAD_SPACING, DECADE_OFFSET, sectionPositions } from "./rosaryData.js"; +import { mysteries, mysteriesLatin, mysteriesEnglish, mysteryTitles, mysteryTitlesEnglish, mysteryTitlesLatin, allMysteryImages, getLabels, getLabelsLatin, getMysteryForWeekday, BEAD_SPACING, DECADE_OFFSET, sectionPositions } from "./rosaryData.js"; import { isEastertide, getLiturgicalSeason } from "$lib/js/easter.svelte"; import { setupScrollSync } from "./rosaryScrollSync.js"; let { data } = $props(); @@ -39,18 +39,24 @@ let showImages = $state(data.initialShowImages); let hasLoadedFromStorage = $state(false); // Create language context for prayer components (LanguageToggle will use this) -const langContext = createLanguageContext({ urlLang: /** @type {'en'|'de'} */ (data.lang), initialLatin: data.initialLatin }); +// For Latin route, force showLatin on so only Latin prayers render +const langContext = createLanguageContext({ urlLang: /** @type {'en'|'de'} */ (data.lang), initialLatin: data.lang === 'la' ? true : data.initialLatin }); // Update lang store when data.lang changes (e.g., after navigation) +// For Latin route, force showLatin on — Latin text is only rendered when showLatin is true $effect(() => { langContext.lang.set(data.lang); + if (data.lang === 'la') { + langContext.showLatin.set(true); + } }); // UI labels based on URL language (reactive) const isEnglish = $derived(data.lang === 'en'); -/** @type {'en'|'de'} */ -const lang = $derived(isEnglish ? 'en' : 'de'); -const labels = $derived(getLabels(isEnglish)); +const isLatin = $derived(data.lang === 'la'); +/** @type {'en'|'de'|'la'} */ +const lang = $derived(isLatin ? 'la' : isEnglish ? 'en' : 'de'); +const labels = $derived(isLatin ? getLabelsLatin() : getLabels(isEnglish)); // Save toggle states to localStorage whenever they change (but only after initial load) $effect(() => { @@ -72,7 +78,7 @@ let todaysMystery = $state(/** @type {MysteryType} */ (data.todaysMystery)); let currentMysteries = $derived(mysteries[selectedMystery]); let currentMysteriesLatin = $derived(mysteriesLatin[selectedMystery]); let currentMysteriesEnglish = $derived(mysteriesEnglish[selectedMystery]); -let currentMysteryTitles = $derived(isEnglish ? mysteryTitlesEnglish[selectedMystery] : mysteryTitles[selectedMystery]); +let currentMysteryTitles = $derived(isLatin ? mysteryTitlesLatin[selectedMystery] : isEnglish ? mysteryTitlesEnglish[selectedMystery] : mysteryTitles[selectedMystery]); let currentMysteryDescriptions = $derived(data.mysteryDescriptions[selectedMystery] || []); // Function to switch mysteries @@ -530,7 +536,8 @@ onMount(() => { } /* Reduce min-height in monolingual mode since content is shorter */ -.prayer-section.decade:has(:global(.prayer-wrapper.monolingual)) { +.prayer-section.decade:has(:global(.prayer-wrapper.monolingual)), +.prayer-section.decade:has(:global(.prayer-wrapper.lang-la)) { min-height: 30vh; } @@ -538,7 +545,8 @@ onMount(() => { .prayer-section.decade { padding-bottom: 1.5rem; } - .prayer-section.decade:has(:global(.prayer-wrapper.monolingual)) { + .prayer-section.decade:has(:global(.prayer-wrapper.monolingual)), + .prayer-section.decade:has(:global(.prayer-wrapper.lang-la)) { min-height: 20vh; } .prayer-section { @@ -802,12 +810,14 @@ h1 { href={imagesToggleHref} /> - - + + {#if !isLatin} + + {/if}
diff --git a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/rosaryData.js b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/rosaryData.js index 793cfb2..87a7d5d 100644 --- a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/rosaryData.js +++ b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/rosaryData.js @@ -127,6 +127,38 @@ export const mysteryTitles = { ] }; +// Latin short titles for mysteries +export const mysteryTitlesLatin = { + freudenreich: [ + "Annuntiatio", + "Visitatio", + "Nativitas", + "Præsentatio", + "Inventio in Templo" + ], + schmerzhaften: [ + "Agonia in Horto", + "Flagellatio", + "Coronatio Spinis", + "Baiulatio Crucis", + "Crucifixio" + ], + glorreichen: [ + "Resurrectio", + "Ascensio", + "Missio Spiritus Sancti", + "Assumptio Mariæ", + "Coronatio Mariæ" + ], + lichtreichen: [ + "Baptisma", + "Nuptiæ in Cana", + "Prædicatio Regni Dei", + "Transfiguratio", + "Institutio Eucharistiæ" + ] +}; + // English short titles for mysteries export const mysteryTitlesEnglish = { freudenreich: [ @@ -200,6 +232,44 @@ export function getLabels(isEnglish) { }; } +// Latin UI labels +export function getLabelsLatin() { + return { + pageTitle: 'Rosarium Vivum', + pageDescription: 'Versio digitalis Rosarii ad precandum.', + mysteries: 'Mysteria', + today: 'Hodie', + joyful: 'Gaudiosa', + sorrowful: 'Dolorosa', + glorious: 'Gloriosa', + luminous: 'Luminosa', + includeLuminous: 'Mysteria Luminosa includere', + showImages: 'Imagines monstrare', + beginning: 'Initium', + signOfCross: '♱ Signum Crucis', + ourFather: 'Pater Noster', + hailMary: 'Ave Maria', + faith: 'Fides', + hope: 'Spes', + love: 'Caritas', + decade: 'Decas', + optional: 'ad libitum', + gloriaPatri: 'Gloria Patri', + fatimaPrayer: 'Oratio Fatimensis', + conclusion: 'Conclusio', + finalPrayer: 'Oratio Finalis', + saintMichael: 'Oratio ad Sanctum Michaëlem Archangelum', + footnoteSign: 'Hic signum crucis fac', + footnoteBow: 'Hic caput inclina', + showBibleVerse: 'Versum biblicum monstrare', + mysteryFaith: 'Jesus, qui adáugeat nobis fidem', + mysteryHope: 'Jesus, qui corróboret nobis spem', + mysteryLove: 'Jesus, qui perficiat in nobis caritátem', + eastertide: 'Tempus Paschale', + lent: 'Quadragesima' + }; +} + // Get the appropriate mystery for a given weekday /** * @param {Date} date diff --git a/src/routes/[faithLang=faithLang]/angelus/+page.server.ts b/src/routes/[faithLang=faithLang]/angelus/+page.server.ts index 31b7629..f1e4215 100644 --- a/src/routes/[faithLang=faithLang]/angelus/+page.server.ts +++ b/src/routes/[faithLang=faithLang]/angelus/+page.server.ts @@ -2,6 +2,6 @@ import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ params }) => { - const prayersPath = params.faithLang === 'faith' ? 'prayers' : 'gebete'; + const prayersPath = params.faithLang === 'fides' ? 'orationes' : params.faithLang === 'faith' ? 'prayers' : 'gebete'; redirect(301, `/${params.faithLang}/${prayersPath}/angelus`); }; diff --git a/src/routes/api/glaube/angelus-streak/+server.ts b/src/routes/api/glaube/angelus-streak/+server.ts new file mode 100644 index 0000000..9daabfc --- /dev/null +++ b/src/routes/api/glaube/angelus-streak/+server.ts @@ -0,0 +1,73 @@ +import { json, error, type RequestHandler } from '@sveltejs/kit'; +import { AngelusStreak } from '$models/AngelusStreak'; +import { dbConnect } from '$utils/db'; + +export const GET: RequestHandler = async ({ locals }) => { + const session = await locals.auth(); + + if (!session?.user?.nickname) { + throw error(401, 'Authentication required'); + } + + await dbConnect(); + + try { + const streak = await AngelusStreak.findOne({ + username: session.user.nickname + }).lean() as any; + + return json({ + streak: streak?.streak ?? 0, + lastComplete: streak?.lastComplete ?? null, + todayPrayed: streak?.todayPrayed ?? 0, + todayDate: streak?.todayDate ?? null + }); + } catch (e) { + throw error(500, 'Failed to fetch angelus streak'); + } +}; + +export const POST: RequestHandler = async ({ request, locals }) => { + const session = await locals.auth(); + + if (!session?.user?.nickname) { + throw error(401, 'Authentication required'); + } + + const { streak, lastComplete, todayPrayed, todayDate } = await request.json(); + + if (typeof streak !== 'number' || streak < 0) { + throw error(400, 'Valid streak required'); + } + + if (lastComplete !== null && typeof lastComplete !== 'string') { + throw error(400, 'Invalid lastComplete format'); + } + + if (typeof todayPrayed !== 'number' || todayPrayed < 0 || todayPrayed > 7) { + throw error(400, 'Invalid todayPrayed bitmask'); + } + + if (todayDate !== null && typeof todayDate !== 'string') { + throw error(400, 'Invalid todayDate format'); + } + + await dbConnect(); + + try { + const updated = await AngelusStreak.findOneAndUpdate( + { username: session.user.nickname }, + { streak, lastComplete, todayPrayed, todayDate }, + { upsert: true, new: true } + ).lean() as any; + + return json({ + streak: updated?.streak ?? 0, + lastComplete: updated?.lastComplete ?? null, + todayPrayed: updated?.todayPrayed ?? 0, + todayDate: updated?.todayDate ?? null + }); + } catch (e) { + throw error(500, 'Failed to update angelus streak'); + } +};