feat: enhance rosary with interactive Bible citations and improved mystery selection
All checks were successful
CI / update (push) Successful in 25s

- Add clickable Bible reference buttons that open modal with full verses
- Create BibleModal component with backdrop blur and styled close button
- Implement build-time data fetching for Bible texts while maintaining reactivity
- Redesign mystery selector with responsive grid (3-in-row/4-in-row/2×2)
- Add "Heutige" badge to indicate today's auto-selected mystery
- Reposition luminous mysteries toggle below mystery selector
- Integrate Bible reference and counter buttons side-by-side
- Restructure Bible API under /api/glaube/bibel/ for better organization
This commit is contained in:
2025-12-16 15:45:33 +01:00
parent 01dd736bbc
commit 2be2e1977b
8 changed files with 771 additions and 80 deletions

View File

@@ -70,7 +70,7 @@ async function authorization({ event, resolve }) {
// Bible verse functionality for error pages // Bible verse functionality for error pages
async function getRandomVerse(fetch: typeof globalThis.fetch): Promise<any> { async function getRandomVerse(fetch: typeof globalThis.fetch): Promise<any> {
try { try {
const response = await fetch('/api/bible-quote'); const response = await fetch('/api/glaube/bibel/zufallszitat');
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }

View File

@@ -0,0 +1,278 @@
<script lang="ts">
import { onMount } from 'svelte';
export let reference: string = '';
export let title: string = '';
export let onClose: () => void;
let book: string = '';
let chapter: number = 0;
let verses: Array<{ verse: number; text: string }> = [];
let loading = true;
let error = '';
onMount(async () => {
if (!reference) return;
try {
const response = await fetch(`/api/glaube/bibel/${encodeURIComponent(reference)}`);
if (!response.ok) {
throw new Error('Failed to fetch verses');
}
const data = await response.json();
book = data.book;
chapter = data.chapter;
verses = data.verses;
} catch (err) {
console.error('Error fetching Bible verses:', err);
error = 'Fehler beim Laden der Bibelstelle';
} finally {
loading = false;
}
});
function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget) {
onClose();
}
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
onClose();
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
<div class="modal-backdrop" on:click={handleBackdropClick} role="presentation">
<div class="modal-content">
<div class="modal-header">
<div class="header-content">
{#if title}
<h3 class="modal-title">
{#if title.includes(':')}
{title.split(':')[0]}:<br>{title.split(':')[1]}
{:else}
{title}
{/if}
</h3>
{/if}
<p class="modal-reference">{reference}</p>
</div>
<button class="close-button" on:click={onClose} aria-label="Schließen">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
{#if loading}
<p class="loading">Lädt...</p>
{:else if error}
<p class="error">{error}</p>
{:else if verses.length > 0}
<div class="verses">
{#each verses as verse}
<p class="verse">
<span class="verse-number">{verse.verse}</span>
<span class="verse-text">{verse.text}</span>
</p>
{/each}
</div>
{:else}
<p class="error">Keine Verse gefunden</p>
{/if}
</div>
</div>
</div>
<style>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
backdrop-filter: blur(10px);
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
animation: show-backdrop 200ms ease forwards;
}
@keyframes show-backdrop {
from {
backdrop-filter: blur(0px);
background: rgba(0, 0, 0, 0);
}
to {
backdrop-filter: blur(10px);
background: rgba(0, 0, 0, 0.3);
}
}
@media(prefers-color-scheme: light) {
.modal-backdrop {
background: rgba(255, 255, 255, 0.3);
}
@keyframes show-backdrop {
from {
backdrop-filter: blur(0px);
background: rgba(255, 255, 255, 0);
}
to {
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.3);
}
}
}
.modal-content {
background: var(--nord0);
border-radius: 12px;
max-width: 600px;
width: 100%;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
position: relative;
}
@media(prefers-color-scheme: light) {
.modal-content {
background: var(--nord6);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 1.5rem;
border-bottom: 1px solid var(--nord3);
}
@media(prefers-color-scheme: light) {
.modal-header {
border-bottom: 1px solid var(--nord4);
}
}
.header-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.modal-title {
margin: 0;
color: var(--nord10);
font-size: 1.3rem;
font-weight: 700;
}
.modal-reference {
margin: 0;
color: var(--nord8);
font-size: 1rem;
font-weight: 600;
}
.close-button {
position: absolute;
top: -1rem;
right: -1rem;
background-color: var(--nord11);
border: none;
cursor: pointer;
padding: 1rem;
border-radius: 1000px;
color: white;
transition: 200ms;
box-shadow: 0 0 1em 0.2em rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 1001;
}
.close-button svg {
width: 2rem;
height: 2rem;
}
.close-button:hover {
background-color: var(--nord0);
transform: scale(1.2, 1.2);
box-shadow: 0 0 1em 0.4em rgba(0, 0, 0, 0.3);
}
.close-button:active {
transition: 50ms;
scale: 0.8 0.8;
}
.modal-body {
padding: 1rem;
overflow-y: auto;
}
.loading,
.error {
text-align: center;
color: var(--nord4);
font-style: italic;
}
@media(prefers-color-scheme: light) {
.loading,
.error {
color: var(--nord2);
}
}
.error {
color: var(--nord11);
}
.verses {
display: flex;
flex-direction: column;
gap: 0;
}
.verse {
display: flex;
gap: 0.75rem;
line-height: 1.6;
color: var(--nord4);
}
@media(prefers-color-scheme: light) {
.verse {
color: var(--nord0);
}
}
.verse-number {
color: var(--nord10);
font-weight: 700;
min-width: 2rem;
font-size: 0.9rem;
}
.verse-text {
flex: 1;
font-size: 1.1rem;
}
</style>

View File

@@ -10,9 +10,6 @@ export let onClick;
<style> <style>
.counter-button { .counter-button {
position: absolute;
bottom: 0.5rem;
right: 0.5rem;
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
border-radius: 50%; border-radius: 50%;

View File

@@ -0,0 +1,100 @@
export interface MysteryReference {
title: string;
reference: string;
}
export interface MysteryDescription extends MysteryReference {
text: string;
}
// Only store references - texts will be fetched at build time
export const mysteryReferences = {
lichtreichen: [
{
title: "Das erste lichtreiche Geheimnis: Die Taufe im Jordan.",
reference: "Mt 3, 16-17"
},
{
title: "Das zweite lichtreiche Geheimnis: Die Hochzeit von Kana.",
reference: "Joh 2, 1-5"
},
{
title: "Das dritte lichtreiche Geheimnis: Die Verkündigung des Reiches Gottes.",
reference: "Mk 1, 15"
},
{
title: "Das vierte lichtreiche Geheimnis: Die Verklärung.",
reference: "Mt 17, 1-2"
},
{
title: "Das fünfte lichtreiche Geheimnis: Die heiligste Eucharistie (Das Altarssakrament).",
reference: "Mt 26, 26"
}
],
freudenreich: [
{
title: "Das erste freudenreiche Geheimnis: Die Verkündigung des Erzengles Gabriel an die Jungfrau Maria.",
reference: "Lk 1, 26-27"
},
{
title: "Das zweite freudenreiche Geheimnis: Der Besuch Marias bei Elisabeth.",
reference: "Lk 1, 39-42"
},
{
title: "Das dritte freudenreiche Geheimnis: Die Geburt Jesu im Stall von Bethlehem.",
reference: "Lk 2, 1-7"
},
{
title: "Das vierte freudenreiche Geheimnis: Jesus wird von Maria und Josef im Tempel dargebracht.",
reference: "Lk 2, 21-24"
},
{
title: "Das fünfte freudenreiche Geheimnis: Jesus wird im Tempel wiedergefunden.",
reference: "Lk 2, 41-47"
}
],
schmerzhaften: [
{
title: "Das erste schmerzhafte Geheimnis: Die Todesangst Jesu.",
reference: "Mt 26, 36-39"
},
{
title: "Das zweite schmerzhafte Geheimnis: Die Geißelung Jesu.",
reference: "Mt 27, 26"
},
{
title: "Das dritte schmerzhafte Geheimnis: Die Dornenkrönung.",
reference: "Mt 27, 27-29"
},
{
title: "Das vierte schmerzhafte Geheimnis: Jesus trägt das schwere Kreuz.",
reference: "Mk 15, 21-22"
},
{
title: "Das fünfte schmerzhafte Geheimnis: Die Kreuzigung Jesu.",
reference: "Lk 23, 33-46"
}
],
glorreichen: [
{
title: "Das erste glorreiche Geheimnis: Die Auferstehung Jesu.",
reference: "Lk 24, 1-6"
},
{
title: "Das zweite glorreiche Geheimnis: Die Himmerfahrt Jesu.",
reference: "Mk 16, 19"
},
{
title: "Das dritte glorreiche Geheimnis: Die Herabkunft des Heiligen Geistes im Abendmahlssaal.",
reference: "Apg 2, 1-4"
},
{
title: "Das vierte glorreiche Geheimnis: Die Aufnahme Marias in den Himmel.",
reference: "Lk 1, 48-49"
},
{
title: "Das fünfte glorreiche Geheimnis: Die Krönung Marias zur Königin des Himmels und der Erde.",
reference: "Offb 12, 1"
}
]
} as const;

View File

@@ -0,0 +1,122 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
interface BibleVerse {
bookName: string;
abbreviation: string;
bookNumber: number;
chapter: number;
verseNumber: number;
text: string;
}
// Cache for parsed verses to avoid reading file repeatedly
let cachedVerses: BibleVerse[] | null = null;
async function loadVerses(fetch: typeof globalThis.fetch): Promise<BibleVerse[]> {
if (cachedVerses) {
return cachedVerses;
}
try {
const response = await fetch('/allioli.tsv');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const content = await response.text();
const lines = content.trim().split('\n');
cachedVerses = lines.map(line => {
const [bookName, abbreviation, bookNumber, chapter, verseNumber, text] = line.split('\t');
return {
bookName,
abbreviation,
bookNumber: parseInt(bookNumber),
chapter: parseInt(chapter),
verseNumber: parseInt(verseNumber),
text
};
});
return cachedVerses;
} catch (err) {
console.error('Error loading Bible verses:', err);
throw new Error('Failed to load Bible verses');
}
}
function parseReference(reference: string): { bookRef: string; isFullName: boolean; chapter: number; startVerse: number; endVerse: number } | null {
// Parse various reference formats:
// "Mt 3, 16-17", "Mt3:16-17", "Mt 3:16-17", "Lk1:3", "Matthäus 3, 16-17"
// Match book name (letters and umlauts), optional space, chapter, separator (: or ,), optional space, verse(s)
const match = reference.match(/^([A-Za-zäöüÄÖÜß]+)\s*(\d+)[\s,:]+(\d+)(?:[-:](\d+))?$/);
if (!match) {
return null;
}
const [, bookRef, chapterStr, startVerseStr, endVerseStr] = match;
// If book reference is longer than 5 characters, assume it's a full name
// Otherwise, assume it's an abbreviation
const isFullName = bookRef.length > 5;
return {
bookRef,
isFullName,
chapter: parseInt(chapterStr),
startVerse: parseInt(startVerseStr),
endVerse: endVerseStr ? parseInt(endVerseStr) : parseInt(startVerseStr)
};
}
function getVersesByReference(verses: BibleVerse[], reference: string): BibleVerse[] {
const parsed = parseReference(reference);
if (!parsed) {
return [];
}
return verses.filter(v => {
// Match based on whether we're using full name or abbreviation
const bookMatches = parsed.isFullName
? v.bookName === parsed.bookRef
: v.abbreviation === parsed.bookRef;
return bookMatches &&
v.chapter === parsed.chapter &&
v.verseNumber >= parsed.startVerse &&
v.verseNumber <= parsed.endVerse;
});
}
export const GET: RequestHandler = async ({ params, fetch }) => {
const reference = params.reference;
if (!reference) {
return error(400, 'Missing reference parameter');
}
try {
const verses = await loadVerses(fetch);
const matchedVerses = getVersesByReference(verses, reference);
if (matchedVerses.length === 0) {
return error(404, 'No verses found for the given reference');
}
// Extract book and chapter from first verse (they're all the same)
const firstVerse = matchedVerses[0];
return json({
reference,
book: firstVerse.bookName,
chapter: firstVerse.chapter,
verses: matchedVerses.map(v => ({
verse: v.verseNumber,
text: v.text
}))
});
} catch (err) {
console.error('Error fetching Bible verses:', err);
return error(500, 'Failed to fetch Bible verses');
}
};

View File

@@ -4,8 +4,8 @@ import type { RequestHandler } from './$types';
interface BibleVerse { interface BibleVerse {
bookName: string; bookName: string;
abbreviation: string; abbreviation: string;
bookNumber: number;
chapter: number; chapter: number;
verse: number;
verseNumber: number; verseNumber: number;
text: string; text: string;
} }
@@ -27,12 +27,12 @@ async function loadVerses(fetch: typeof globalThis.fetch): Promise<BibleVerse[]>
const lines = content.trim().split('\n'); const lines = content.trim().split('\n');
cachedVerses = lines.map(line => { cachedVerses = lines.map(line => {
const [bookName, abbreviation, chapter, verse, verseNumber, text] = line.split('\t'); const [bookName, abbreviation, bookNumber, chapter, verseNumber, text] = line.split('\t');
return { return {
bookName, bookName,
abbreviation, abbreviation,
bookNumber: parseInt(bookNumber),
chapter: parseInt(chapter), chapter: parseInt(chapter),
verse: parseInt(verse),
verseNumber: parseInt(verseNumber), verseNumber: parseInt(verseNumber),
text text
}; };

View File

@@ -0,0 +1,55 @@
import { mysteryReferences, type MysteryDescription } from '$lib/data/mysteryDescriptions';
import type { PageServerLoad } from './$types';
export const prerender = true;
async function fetchBibleText(reference: string, fetch: typeof globalThis.fetch): Promise<string> {
try {
const response = await fetch(`/api/glaube/bibel/${encodeURIComponent(reference)}`);
if (!response.ok) {
console.error(`Failed to fetch reference ${reference}:`, response.status);
return '';
}
const data = await response.json();
// Format the verses into a single text with guillemets
if (data.verses && data.verses.length > 0) {
const text = data.verses.map((v: { verse: number; text: string }) => v.text).join(' ');
return `«${text}»`;
}
return '';
} catch (err) {
console.error(`Error fetching reference ${reference}:`, err);
return '';
}
}
export const load: PageServerLoad = async ({ fetch }) => {
// Fetch Bible texts for all mysteries at build time
const mysteryDescriptions: Record<string, MysteryDescription[]> = {
lichtreichen: [],
freudenreich: [],
schmerzhaften: [],
glorreichen: []
};
// Process each mystery type
for (const [mysteryType, references] of Object.entries(mysteryReferences)) {
const descriptions: MysteryDescription[] = [];
for (const ref of references) {
const text = await fetchBibleText(ref.reference, fetch);
descriptions.push({
title: ref.title,
reference: ref.reference,
text
});
}
mysteryDescriptions[mysteryType] = descriptions;
}
return {
mysteryDescriptions
};
};

View File

@@ -12,6 +12,9 @@ import SalveRegina from "$lib/components/prayers/SalveRegina.svelte";
import RosaryFinalPrayer from "$lib/components/prayers/RosaryFinalPrayer.svelte"; import RosaryFinalPrayer from "$lib/components/prayers/RosaryFinalPrayer.svelte";
import BenedictusMedal from "$lib/components/BenedictusMedal.svelte"; import BenedictusMedal from "$lib/components/BenedictusMedal.svelte";
import CounterButton from "$lib/components/CounterButton.svelte"; import CounterButton from "$lib/components/CounterButton.svelte";
import BibleModal from "$lib/components/BibleModal.svelte";
export let data;
// Mystery variations for each type of rosary // Mystery variations for each type of rosary
const mysteries = { const mysteries = {
@@ -144,9 +147,14 @@ function getMysteryForWeekday(date, includeLuminous) {
// Determine which mystery to use based on current weekday // Determine which mystery to use based on current weekday
let selectedMystery = getMysteryForWeekday(new Date(), includeLuminous); let selectedMystery = getMysteryForWeekday(new Date(), includeLuminous);
let todaysMystery = selectedMystery; // Track today's auto-selected mystery
let currentMysteries = mysteries[selectedMystery]; let currentMysteries = mysteries[selectedMystery];
let currentMysteriesLatin = mysteriesLatin[selectedMystery]; let currentMysteriesLatin = mysteriesLatin[selectedMystery];
let currentMysteryTitles = mysteryTitles[selectedMystery]; let currentMysteryTitles = mysteryTitles[selectedMystery];
let currentMysteryDescriptions = data.mysteryDescriptions[selectedMystery] || [];
// Reactive statement to update mystery descriptions when selectedMystery changes
$: currentMysteryDescriptions = data.mysteryDescriptions[selectedMystery] || [];
// Function to switch mysteries // Function to switch mysteries
function selectMystery(mysteryType) { function selectMystery(mysteryType) {
@@ -159,7 +167,7 @@ function selectMystery(mysteryType) {
// Function to handle toggle change // Function to handle toggle change
function handleToggleChange() { function handleToggleChange() {
// Recalculate the default mystery for today // Recalculate the default mystery for today
const todaysMystery = getMysteryForWeekday(new Date(), includeLuminous); todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
// Update to today's mystery // Update to today's mystery
selectMystery(todaysMystery); selectMystery(todaysMystery);
} }
@@ -178,6 +186,11 @@ let decadeCounters = {
secret5: 0 secret5: 0
}; };
// Modal state for displaying Bible citations
let showModal = false;
let selectedReference = '';
let selectedTitle = '';
// Function to advance the counter for a specific decade // Function to advance the counter for a specific decade
function advanceDecade(decadeNum) { function advanceDecade(decadeNum) {
const key = `secret${decadeNum}`; const key = `secret${decadeNum}`;
@@ -211,6 +224,13 @@ function advanceDecade(decadeNum) {
} }
} }
// Function to handle citation click
function handleCitationClick(reference, title = '') {
selectedReference = reference;
selectedTitle = title;
showModal = true;
}
// Map sections to their vertical positions in the SVG // Map sections to their vertical positions in the SVG
const sectionPositions = { const sectionPositions = {
cross: 35, cross: 35,
@@ -662,12 +682,12 @@ onMount(() => {
.prayer-section.decade { .prayer-section.decade {
scroll-snap-align: start; scroll-snap-align: start;
min-height: 50vh; /* Only decades need minimum height for scroll-snap */ min-height: 50vh; /* Only decades need minimum height for scroll-snap */
padding-bottom: 4.5rem; /* Extra space for the counter button */ padding-bottom: 2rem;
} }
@media (max-width: 1023px) { @media (max-width: 1023px) {
.prayer-section.decade { .prayer-section.decade {
padding-bottom: 3.5rem; /* Adjusted for mobile padding */ padding-bottom: 1.5rem;
} }
.prayer-section { .prayer-section {
padding: 0.5rem; padding: 0.5rem;
@@ -796,35 +816,26 @@ h1 {
/* Luminous mysteries toggle */ /* Luminous mysteries toggle */
.luminous-toggle { .luminous-toggle {
text-align: center; display: flex;
justify-content: center;
margin-bottom: 2rem; margin-bottom: 2rem;
padding: 1rem; max-width: 1200px;
background: var(--nord1);
border-radius: 8px;
max-width: 600px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
@media(prefers-color-scheme: light) {
.luminous-toggle {
background: var(--nord5);
}
}
.luminous-toggle label { .luminous-toggle label {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 0.75rem;
gap: 1rem;
cursor: pointer; cursor: pointer;
font-size: 1.1rem; font-size: 0.95rem;
color: var(--nord4); color: var(--nord4);
} }
@media(prefers-color-scheme: light) { @media(prefers-color-scheme: light) {
.luminous-toggle label { .luminous-toggle label {
color: var(--nord0); color: var(--nord2);
} }
} }
@@ -845,6 +856,7 @@ h1 {
transition: background 0.3s ease; transition: background 0.3s ease;
outline: none; outline: none;
border: none; border: none;
flex-shrink: 0;
} }
@media(prefers-color-scheme: light) { @media(prefers-color-scheme: light) {
@@ -874,34 +886,43 @@ h1 {
transform: translateX(20px); transform: translateX(20px);
} }
.luminous-toggle .toggle-description {
margin-top: 1rem;
font-size: 0.95rem;
color: var(--nord8);
line-height: 1.6;
text-align: center;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
@media(prefers-color-scheme: light) {
.luminous-toggle .toggle-description {
color: var(--nord3);
}
}
/* Mystery selector grid */ /* Mystery selector grid */
.mystery-selector { .mystery-selector {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(3, 1fr);
gap: 1.5rem; gap: 1.5rem;
margin-bottom: 3rem; margin-bottom: 3rem;
max-width: 1000px; max-width: 750px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.mystery-selector.four-mysteries {
grid-template-columns: repeat(2, 1fr);
max-width: 500px;
}
@media (min-width: 1024px) {
.mystery-selector.four-mysteries {
grid-template-columns: repeat(4, 1fr);
max-width: 900px;
}
}
@media (max-width: 768px) {
.mystery-selector:not(.four-mysteries) {
grid-template-columns: 1fr;
max-width: 400px;
}
}
@media (max-width: 500px) {
.mystery-selector.four-mysteries {
grid-template-columns: 1fr;
max-width: 400px;
}
}
.mystery-button { .mystery-button {
background: var(--nord1); background: var(--nord1);
border: 2px solid transparent; border: 2px solid transparent;
@@ -914,6 +935,7 @@ h1 {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
position: relative;
} }
@media(prefers-color-scheme: light) { @media(prefers-color-scheme: light) {
@@ -929,20 +951,18 @@ h1 {
.mystery-button.selected { .mystery-button.selected {
border-color: var(--nord10); border-color: var(--nord10);
background: var(--nord2); transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
} }
@media(prefers-color-scheme: light) { .mystery-button:nth-child(1):hover,
.mystery-button.selected { .mystery-button:nth-child(1).selected { background: var(--nord15); }
border-color: var(--nord10); .mystery-button:nth-child(2):hover,
background: var(--nord5); .mystery-button:nth-child(2).selected { background: var(--nord13); }
} .mystery-button:nth-child(3):hover,
} .mystery-button:nth-child(3).selected { background: var(--nord14); }
.mystery-button:nth-child(4):hover,
.mystery-button:nth-child(1):hover { background: var(--nord15); } .mystery-button:nth-child(4).selected { background: var(--nord12); }
.mystery-button:nth-child(2):hover { background: var(--nord13); }
.mystery-button:nth-child(3):hover { background: var(--nord14); }
.mystery-button:nth-child(4):hover { background: var(--nord12); }
.mystery-button svg { .mystery-button svg {
width: 80px; width: 80px;
@@ -978,44 +998,126 @@ h1 {
font-weight: 700; 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);
z-index: 10;
}
/* Highlighted bead (orange for counting) */ /* Highlighted bead (orange for counting) */
.rosary-visualization :global(.counted-bead) { .rosary-visualization :global(.counted-bead) {
fill: var(--nord13) !important; fill: var(--nord13) !important;
filter: drop-shadow(0 0 8px var(--nord13)); filter: drop-shadow(0 0 8px var(--nord13));
} }
/* Mystery description styling */
.mystery-description {
margin: 1.5rem 0 1.5rem 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: center;
}
.mystery-title {
font-weight: 700;
color: var(--nord10);
font-size: 1.1rem;
text-align: center;
}
.decade-buttons {
display: flex;
flex-direction: row;
gap: 1rem;
justify-content: flex-end;
align-items: center;
margin-top: 1.5rem;
}
.bible-reference-text {
color: var(--nord8);
font-size: 0.9rem;
font-weight: 600;
}
@media(prefers-color-scheme: light) {
.bible-reference-text {
color: var(--nord10);
}
}
.bible-reference-button {
background: var(--nord3);
border: 2px solid var(--nord2);
color: var(--nord6);
font-size: 1.2rem;
cursor: pointer;
padding: 0;
width: 3rem;
height: 3rem;
border-radius: 50%;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.bible-reference-button:hover {
background: var(--nord8);
border-color: var(--nord9);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.bible-reference-button:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@media(prefers-color-scheme: light) {
.bible-reference-button {
background: var(--nord5);
border-color: var(--nord4);
color: var(--nord0);
}
.bible-reference-button:hover {
background: var(--nord4);
border-color: var(--nord3);
}
}
</style> </style>
<svelte:head> <svelte:head>
<title>Rosenkranz - Interaktiv</title> <title>Interaktiver Rosenkranz</title>
<meta name="description" content="Interaktive digitale Version des Rosenkranzes zum Mitbeten. Scrolle durch die Gebete und folge der Visualisierung."> <meta name="description" content="Interaktive digitale Version des Rosenkranzes zum Mitbeten. Scrolle durch die Gebete und folge der Visualisierung.">
</svelte:head> </svelte:head>
<div class="page-container"> <div class="page-container">
<h1>Interaktiver Rosenkranz</h1> <h1>Interaktiver Rosenkranz</h1>
<!-- Luminous Mysteries Toggle -->
<div class="luminous-toggle">
<label>
<input type="checkbox" bind:checked={includeLuminous} on:change={handleToggleChange} />
<span>Lichtreiche Geheimnisse einbeziehen</span>
</label>
<p class="toggle-description">
Die Geheimnisse werden automatisch nach dem Wochenplan ausgewählt.
{#if includeLuminous}
Mit lichtreichen Geheimnissen: Do=Lichtreich, andere Tage folgen dem traditionellen Plan.
{:else}
Traditioneller Plan ohne lichtreiche Geheimnisse.
{/if}
Sie können jederzeit manuell ein anderes Geheimnis wählen.
</p>
</div>
<h2 style="text-align:center;">Geheimnisse</h2>
<!-- Mystery Selector --> <!-- Mystery Selector -->
<div class="mystery-selector"> <div class="mystery-selector" class:four-mysteries={includeLuminous}>
<button <button
class="mystery-button" class="mystery-button"
class:selected={selectedMystery === 'freudenreich'} class:selected={selectedMystery === 'freudenreich'}
on:click={() => selectMystery('freudenreich')} on:click={() => selectMystery('freudenreich')}
> >
{#if todaysMystery === 'freudenreich'}
<span class="today-badge">Heutige</span>
{/if}
<svg viewBox="-10 0 2058 2048"> <svg viewBox="-10 0 2058 2048">
<path d="M1935 90q0 32 -38 91q-21 29 -56 90q-20 55 -63 164q-35 86 -95 143q-22 -21 -43 -45q51 -49 85 -139q49 -130 61 -152q-126 48 -152 63q-76 46 -95 128q-27 -18 -58 -25q28 -104 97 -149q31 -20 138 -52q90 -28 137 -74l29 -39q22 -30 32 -30q21 0 21 26zM1714 653 q-90 30 -113 43q-65 36 -65 90q0 19 20 119q23 116 23 247q0 169 -103 299q-111 141 -275 141q-254 0 -283 87q-16 104 -31 207q-27 162 -76 162q-21 0 -41 -20q-16 -19 -32 -37q-10 3 -33 22q-18 15 -39 15q-28 0 -50 -44.5t-30 -44.5q-10 0 -35.5 11.5t-41.5 11.5 q-47 0 -58.5 -45.5t-21.5 -45.5t-29.5 2.5t-29.5 2.5q-46 0 -46 -30q0 -16 14 -44.5t14 -44.5q0 -8 -46.5 -25.5t-46.5 -48.5q0 -34 35.5 -52t99.5 -31q91 -19 103 -22q113 -32 171 -93q37 -39 105 -165q34 -64 43 -82q26 -53 31 -85q-129 -67 -224 -76q-33 0 -96 -11 q-36 -13 -36 -41q0 -7 2 -19.5t2 -19.5q0 -20 -67.5 -42t-67.5 -64q0 -11 8.5 -30t8.5 -30q0 -15 -79 -39t-79 -63q0 -16 9 -45t9 -45q0 -20 -29 -43q-23 -17 -46 -33q-49 -44 -49 -215q0 -8 1 -15q91 53 194 68l282 16q202 12 304 59q143 65 143 210q0 15 -2 44t-2 44 q0 122 78 122q73 0 108 -133q16 -70 32 -139q21 -81 57 -119q46 -51 130 -51q71 0 122 61q90 107 154 149zM1597 636q-25 -22 -77 -91q-30 -40 -75 -40q-91 0 -131 115q-30 106 -59 213q-44 115 -144 115q-146 0 -146 -180q0 -16 2.5 -46.5t2.5 -46.5q0 -62 -19 -87 q-70 -92 -303 -115q-173 -9 -347 -18q-55 -6 -116 -30v34q0 27 57.5 73.5t57.5 91.5q0 16 -10.5 45t-10.5 44q1 1 7 1q3 0 7 1q146 36 146 105q0 13 -8.5 32.5t-8.5 27.5h10q5 0 9 1q61 15 86 36q32 28 28 85q173 15 372 107q-7 77 -80 215q-67 128 -127 195 q-67 74 -169 104q-96 24 -193 47q-10 3 -29 13q86 18 86 70q0 19 -19 62q15 -5 33 -5q42 0 59 26q8 11 22 61l-1 3q10 0 34.5 -11.5t42.5 -11.5q55 0 88 84q38 -32 64 -32q37 0 66 41q25 -53 33 -151q10 -112 23 -154q43 -136 337 -136q116 0 215 -108q105 -114 105 -277 q0 -23 -12 -112l-28 -207q-4 -30 -4 -42q0 -97 124 -147zM1506 605q0 38 -38 38q-39 0 -39 -38t39 -38q38 0 38 38z" /> <path d="M1935 90q0 32 -38 91q-21 29 -56 90q-20 55 -63 164q-35 86 -95 143q-22 -21 -43 -45q51 -49 85 -139q49 -130 61 -152q-126 48 -152 63q-76 46 -95 128q-27 -18 -58 -25q28 -104 97 -149q31 -20 138 -52q90 -28 137 -74l29 -39q22 -30 32 -30q21 0 21 26zM1714 653 q-90 30 -113 43q-65 36 -65 90q0 19 20 119q23 116 23 247q0 169 -103 299q-111 141 -275 141q-254 0 -283 87q-16 104 -31 207q-27 162 -76 162q-21 0 -41 -20q-16 -19 -32 -37q-10 3 -33 22q-18 15 -39 15q-28 0 -50 -44.5t-30 -44.5q-10 0 -35.5 11.5t-41.5 11.5 q-47 0 -58.5 -45.5t-21.5 -45.5t-29.5 2.5t-29.5 2.5q-46 0 -46 -30q0 -16 14 -44.5t14 -44.5q0 -8 -46.5 -25.5t-46.5 -48.5q0 -34 35.5 -52t99.5 -31q91 -19 103 -22q113 -32 171 -93q37 -39 105 -165q34 -64 43 -82q26 -53 31 -85q-129 -67 -224 -76q-33 0 -96 -11 q-36 -13 -36 -41q0 -7 2 -19.5t2 -19.5q0 -20 -67.5 -42t-67.5 -64q0 -11 8.5 -30t8.5 -30q0 -15 -79 -39t-79 -63q0 -16 9 -45t9 -45q0 -20 -29 -43q-23 -17 -46 -33q-49 -44 -49 -215q0 -8 1 -15q91 53 194 68l282 16q202 12 304 59q143 65 143 210q0 15 -2 44t-2 44 q0 122 78 122q73 0 108 -133q16 -70 32 -139q21 -81 57 -119q46 -51 130 -51q71 0 122 61q90 107 154 149zM1597 636q-25 -22 -77 -91q-30 -40 -75 -40q-91 0 -131 115q-30 106 -59 213q-44 115 -144 115q-146 0 -146 -180q0 -16 2.5 -46.5t2.5 -46.5q0 -62 -19 -87 q-70 -92 -303 -115q-173 -9 -347 -18q-55 -6 -116 -30v34q0 27 57.5 73.5t57.5 91.5q0 16 -10.5 45t-10.5 44q1 1 7 1q3 0 7 1q146 36 146 105q0 13 -8.5 32.5t-8.5 27.5h10q5 0 9 1q61 15 86 36q32 28 28 85q173 15 372 107q-7 77 -80 215q-67 128 -127 195 q-67 74 -169 104q-96 24 -193 47q-10 3 -29 13q86 18 86 70q0 19 -19 62q15 -5 33 -5q42 0 59 26q8 11 22 61l-1 3q10 0 34.5 -11.5t42.5 -11.5q55 0 88 84q38 -32 64 -32q37 0 66 41q25 -53 33 -151q10 -112 23 -154q43 -136 337 -136q116 0 215 -108q105 -114 105 -277 q0 -23 -12 -112l-28 -207q-4 -30 -4 -42q0 -97 124 -147zM1506 605q0 38 -38 38q-39 0 -39 -38t39 -38q38 0 38 38z" />
<path d="m 1724.44,1054.6641 c -31.1769,-18 -37.7653,-42.5884 -19.7653,-73.76528 5.3333,-9.2376 12.354,-16.7312 21.0621,-22.4808 6.2201,-4.1068 44.7886,-7.2427 115.7055,-9.4077 70.9168,-2.1649 110.128,-1.0807 117.6336,3.2526 30.0222,17.3334 35.5333,42.45448 16.5333,75.36348 -7.3333,12.7017 -16.1754,20.6833 -26.5263,23.9448 -24.5645,1.2137 -56.7805,3.0135 -96.648,5.3994 -72.6282,5.7957 -115.2931,5.0269 -127.9949,-2.3065 z" /> <path d="m 1724.44,1054.6641 c -31.1769,-18 -37.7653,-42.5884 -19.7653,-73.76528 5.3333,-9.2376 12.354,-16.7312 21.0621,-22.4808 6.2201,-4.1068 44.7886,-7.2427 115.7055,-9.4077 70.9168,-2.1649 110.128,-1.0807 117.6336,3.2526 30.0222,17.3334 35.5333,42.45448 16.5333,75.36348 -7.3333,12.7017 -16.1754,20.6833 -26.5263,23.9448 -24.5645,1.2137 -56.7805,3.0135 -96.648,5.3994 -72.6282,5.7957 -115.2931,5.0269 -127.9949,-2.3065 z" />
@@ -1032,6 +1134,9 @@ h1 {
class:selected={selectedMystery === 'schmerzhaften'} class:selected={selectedMystery === 'schmerzhaften'}
on:click={() => selectMystery('schmerzhaften')} on:click={() => selectMystery('schmerzhaften')}
> >
{#if todaysMystery === 'schmerzhaften'}
<span class="today-badge">Heutige</span>
{/if}
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 512 512" ><path d="M255.094 24.875c-16.73 9.388-34.47 42.043-41.688 59.47-14.608-2.407-28.87-3.664-42.562-3.75-11.446-.074-22.49.68-33.03 2.218-16.34-8.284-34.766-29.065-42.626-50-9.324 15.704-9.558 42.313-5.782 64.593-19.443 9.72-35.107 23.633-45.53 41.688-7.262 12.577-11.5 26.34-12.97 40.875 13.294-25.904 35-46.957 65.656-54.345-34.99 31.783-59.85 87.186-51.5 129.406-1.2 22.87-9.48 37.647-24.75 44.595 16.335 4.59 35.497 3.343 49.438-1.28 24.94 34.82 60.818 67.882 105.063 94.342-6.952 17.613-16.677 49.21-16.47 66.032 10.846-13.178 37.433-40.585 61.72-42.783 23.656 10.27 47.35 17.698 70.312 22.313 12.423 17.25 12.895 38.867 7.375 53.594 16.402-9.2 33.82-33.187 39.938-48 47.1 1.423 88.046-10.534 114.718-35.563 17.536 5.52 30.744 15.707 39.813 30.5.243-19.578-8.05-44.353-18-60.31 13.42-28.268 12.786-61.81.5-96.158l.405.47c9.976-11.804 18.304-33.19 18.063-52.907-8.535 10.373-20.727 15.14-36.75 14.188-13.56-22.597-31.81-44.812-54.032-65.375 10.56-19.27 30.402-36.43 44.156-47.97-18.985-5.337-67.794 5.2-80.78 17.782l5.906 8.5c5.637 11.99 9.503 24.423 11.093 37.063-26.323-37.275-70.72-74.72-114.905-95.625-15.894-25.424-19.322-56.118-12.78-73.563zm-82.875 97.063c1.13-.015 2.258-.008 3.405 0 31.56.2 68.888 8.842 107 25.656-8.8 20.095-14.74 44.482-10 61.344 13.33-18.637 37.313-34.22 55.406-37.5 55.904 34.315 96.215 78.718 111.658 118.718l.093.22c16.088 37.88 13.36 85.186-26.56 117.312 4.79-11.41 7.986-23.828 9.5-36.438-14.078 10.012-33.524 15.304-56.314 15.97-1.954-17.242-9.117-52.874-22.28-65.72 1.565 16.122-8.11 46.272-26.22 61.063-31.916-6.495-66.794-19.67-101.03-39.438-9.538-5.506-18.65-11.307-27.314-17.344-3.444-23.614 7.842-53.562 20.563-64.03-18.967-.234-46.71 22.156-59.313 32.75-40.974-38.47-64.14-81.11-61.25-115 16.275-1.708 36.144.927 51.72 8-3.92-15.382-18.553-31.733-34.407-44.344 14.757-13.826 37.7-20.852 65.344-21.22z"/></svg> <svg viewBox="0 0 512 512" ><path d="M255.094 24.875c-16.73 9.388-34.47 42.043-41.688 59.47-14.608-2.407-28.87-3.664-42.562-3.75-11.446-.074-22.49.68-33.03 2.218-16.34-8.284-34.766-29.065-42.626-50-9.324 15.704-9.558 42.313-5.782 64.593-19.443 9.72-35.107 23.633-45.53 41.688-7.262 12.577-11.5 26.34-12.97 40.875 13.294-25.904 35-46.957 65.656-54.345-34.99 31.783-59.85 87.186-51.5 129.406-1.2 22.87-9.48 37.647-24.75 44.595 16.335 4.59 35.497 3.343 49.438-1.28 24.94 34.82 60.818 67.882 105.063 94.342-6.952 17.613-16.677 49.21-16.47 66.032 10.846-13.178 37.433-40.585 61.72-42.783 23.656 10.27 47.35 17.698 70.312 22.313 12.423 17.25 12.895 38.867 7.375 53.594 16.402-9.2 33.82-33.187 39.938-48 47.1 1.423 88.046-10.534 114.718-35.563 17.536 5.52 30.744 15.707 39.813 30.5.243-19.578-8.05-44.353-18-60.31 13.42-28.268 12.786-61.81.5-96.158l.405.47c9.976-11.804 18.304-33.19 18.063-52.907-8.535 10.373-20.727 15.14-36.75 14.188-13.56-22.597-31.81-44.812-54.032-65.375 10.56-19.27 30.402-36.43 44.156-47.97-18.985-5.337-67.794 5.2-80.78 17.782l5.906 8.5c5.637 11.99 9.503 24.423 11.093 37.063-26.323-37.275-70.72-74.72-114.905-95.625-15.894-25.424-19.322-56.118-12.78-73.563zm-82.875 97.063c1.13-.015 2.258-.008 3.405 0 31.56.2 68.888 8.842 107 25.656-8.8 20.095-14.74 44.482-10 61.344 13.33-18.637 37.313-34.22 55.406-37.5 55.904 34.315 96.215 78.718 111.658 118.718l.093.22c16.088 37.88 13.36 85.186-26.56 117.312 4.79-11.41 7.986-23.828 9.5-36.438-14.078 10.012-33.524 15.304-56.314 15.97-1.954-17.242-9.117-52.874-22.28-65.72 1.565 16.122-8.11 46.272-26.22 61.063-31.916-6.495-66.794-19.67-101.03-39.438-9.538-5.506-18.65-11.307-27.314-17.344-3.444-23.614 7.842-53.562 20.563-64.03-18.967-.234-46.71 22.156-59.313 32.75-40.974-38.47-64.14-81.11-61.25-115 16.275-1.708 36.144.927 51.72 8-3.92-15.382-18.553-31.733-34.407-44.344 14.757-13.826 37.7-20.852 65.344-21.22z"/></svg>
</svg> </svg>
@@ -1043,6 +1148,9 @@ h1 {
class:selected={selectedMystery === 'glorreichen'} class:selected={selectedMystery === 'glorreichen'}
on:click={() => selectMystery('glorreichen')} on:click={() => selectMystery('glorreichen')}
> >
{#if todaysMystery === 'glorreichen'}
<span class="today-badge">Heutige</span>
{/if}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-10 0 2060 2048"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-10 0 2060 2048">
<path <path
d="M1968 505l-119 632q101 61 101 163q0 149 -228 212q-171 47 -356 47h-682q-47 0 -111 -8q-210 -26 -293 -55q-180 -62 -180 -196q0 -124 101 -163l-119 -632h37q87 0 170 43q-18 85 -18 103q0 116 75 130q31 -47 77 -129l40 147q49 -37 95 -37t100 37q9 -38 31 -113 d="M1968 505l-119 632q101 61 101 163q0 149 -228 212q-171 47 -356 47h-682q-47 0 -111 -8q-210 -26 -293 -55q-180 -62 -180 -196q0 -124 101 -163l-119 -632h37q87 0 170 43q-18 85 -18 103q0 116 75 130q31 -47 77 -129l40 147q49 -37 95 -37t100 37q9 -38 31 -113
@@ -1063,6 +1171,9 @@ q0 -31 22 -54.5t52 -23.5q31 0 52.5 23.5t21.5 54.5zM596 888q0 34 -34 34q-30 0 -30
class:selected={selectedMystery === 'lichtreichen'} class:selected={selectedMystery === 'lichtreichen'}
on:click={() => selectMystery('lichtreichen')} on:click={() => selectMystery('lichtreichen')}
> >
{#if todaysMystery === 'lichtreichen'}
<span class="today-badge">Heutige</span>
{/if}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-10 0 2156 2048"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-10 0 2156 2048">
<path <path
d="M1668 383q0 14 -48.5 92.5t-64.5 96t-41 17.5q-53 0 -53 -54q0 -16 46 -92q41 -68 60 -92q16 -20 43 -20q58 0 58 52zM688 535q0 54 -54 54q-16 0 -30 -7q-10 -5 -66 -95.5t-56 -103.5q0 -52 57 -52q22 0 34 11q20 31 53 81q62 90 62 112zM2064 842q0 59 -56 100 d="M1668 383q0 14 -48.5 92.5t-64.5 96t-41 17.5q-53 0 -53 -54q0 -16 46 -92q41 -68 60 -92q16 -20 43 -20q58 0 58 52zM688 535q0 54 -54 54q-16 0 -30 -7q-10 -5 -66 -95.5t-56 -103.5q0 -52 57 -52q22 0 34 11q20 31 53 81q62 90 62 112zM2064 842q0 59 -56 100
@@ -1076,6 +1187,14 @@ l536 389l-209 -629zM1671 934l-370 267l150 436l-378 -271l-371 271q8 -34 15 -68q10
{/if} {/if}
</div> </div>
<!-- Luminous Mysteries Toggle -->
<div class="luminous-toggle">
<label>
<input type="checkbox" bind:checked={includeLuminous} on:change={handleToggleChange} />
<span>Lichtreiche Geheimnisse einbeziehen</span>
</label>
</div>
<div class="rosary-layout"> <div class="rosary-layout">
<!-- Sidebar: Rosary Visualization --> <!-- Sidebar: Rosary Visualization -->
<div class="rosary-sidebar"> <div class="rosary-sidebar">
@@ -1242,14 +1361,29 @@ l536 389l-209 -629zM1671 934l-370 267l150 436l-378 -271l-371 271q8 -34 15 -68q10
data-section="secret{decadeNum}" data-section="secret{decadeNum}"
> >
<h2>{decadeNum}. Gesätz: {currentMysteryTitles[decadeNum - 1]}</h2> <h2>{decadeNum}. Gesätz: {currentMysteryTitles[decadeNum - 1]}</h2>
<!-- Mystery description with Bible reference button -->
<h3>Ave Maria <span class="repeat-count">(10×)</span></h3> <h3>Ave Maria <span class="repeat-count">(10×)</span></h3>
<AveMaria <AveMaria
mysteryLatin={currentMysteriesLatin[decadeNum - 1]} mysteryLatin={currentMysteriesLatin[decadeNum - 1]}
mystery={currentMysteries[decadeNum - 1]} mystery={currentMysteries[decadeNum - 1]}
/> />
<!-- Counter button --> <!-- Bible reference and counter buttons -->
<CounterButton onClick={() => advanceDecade(decadeNum)} /> <div class="decade-buttons">
{#if currentMysteryDescriptions[decadeNum - 1]}
{@const description = currentMysteryDescriptions[decadeNum - 1]}
<span class="bible-reference-text">{description.reference}</span>
<button
class="bible-reference-button"
on:click={() => handleCitationClick(description.reference, description.title)}
aria-label="Bibelstelle anzeigen"
>
📖
</button>
{/if}
<CounterButton onClick={() => advanceDecade(decadeNum)} />
</div>
</div> </div>
<!-- Transition prayers (Gloria, Fatima, Paternoster) --> <!-- Transition prayers (Gloria, Fatima, Paternoster) -->
@@ -1437,3 +1571,8 @@ Anders als die Geheimnisse in Deutsch ist es üblich beim beten des Rosenkranzes
</ol> </ol>
</div> </div>
</div> </div>
<!-- Bible citation modal -->
{#if showModal}
<BibleModal reference={selectedReference} title={selectedTitle} onClose={() => showModal = false} />
{/if}