refactor: extract language toggle into reusable components
All checks were successful
CI / update (push) Successful in 24s
All checks were successful
CI / update (push) Successful in 24s
Create Toggle and LanguageToggle components to reduce code duplication and enable shared state across pages. - Add Toggle.svelte: Generic iOS-style toggle with customizable accent color - Add LanguageToggle.svelte: Language-specific toggle with localStorage persistence - Refactor rosary page to use new toggle components - Add language toggle to gebete page - Toggle state persists across both pages via localStorage - Reduce min-height of Ave Maria decades in monolingual mode (50vh → 30vh)
This commit is contained in:
39
src/lib/components/LanguageToggle.svelte
Normal file
39
src/lib/components/LanguageToggle.svelte
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { getLanguageContext } from '$lib/contexts/languageContext.js';
|
||||||
|
import Toggle from './Toggle.svelte';
|
||||||
|
|
||||||
|
// Get the language context (must be created by parent page)
|
||||||
|
const { showLatin } = getLanguageContext();
|
||||||
|
|
||||||
|
// Local state for the checkbox
|
||||||
|
let showBilingual = true;
|
||||||
|
|
||||||
|
// Flag to prevent saving before we've loaded from localStorage
|
||||||
|
let hasLoadedFromStorage = false;
|
||||||
|
|
||||||
|
// Sync checkbox with context
|
||||||
|
$: $showLatin = showBilingual;
|
||||||
|
|
||||||
|
// Save to localStorage whenever it changes (but only after initial load)
|
||||||
|
$: if (typeof localStorage !== 'undefined' && hasLoadedFromStorage) {
|
||||||
|
localStorage.setItem('rosary_showBilingual', showBilingual.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Load from localStorage
|
||||||
|
const saved = localStorage.getItem('rosary_showBilingual');
|
||||||
|
if (saved !== null) {
|
||||||
|
showBilingual = saved === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now allow saving
|
||||||
|
hasLoadedFromStorage = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
bind:checked={showBilingual}
|
||||||
|
label="Lateinisch und Deutsch anzeigen"
|
||||||
|
accentColor="var(--nord14)"
|
||||||
|
/>
|
||||||
85
src/lib/components/Toggle.svelte
Normal file
85
src/lib/components/Toggle.svelte
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<script>
|
||||||
|
export let checked = false;
|
||||||
|
export let label = "";
|
||||||
|
export let accentColor = "var(--nord14)"; // Default to nord14, can be overridden
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toggle-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-wrapper label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--nord4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(prefers-color-scheme: light) {
|
||||||
|
.toggle-wrapper label {
|
||||||
|
color: var(--nord2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-wrapper span {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* iOS-style toggle switch */
|
||||||
|
.toggle-wrapper input[type="checkbox"] {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 51px;
|
||||||
|
height: 31px;
|
||||||
|
background: var(--nord2);
|
||||||
|
border-radius: 31px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(prefers-color-scheme: light) {
|
||||||
|
.toggle-wrapper input[type="checkbox"] {
|
||||||
|
background: var(--nord4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-wrapper input[type="checkbox"]:checked {
|
||||||
|
background: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-wrapper input[type="checkbox"]::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
background: white;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-wrapper input[type="checkbox"]:checked::before {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="toggle-wrapper" style="--accent-color: {accentColor}">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" bind:checked on:change />
|
||||||
|
<span>{label}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { createLanguageContext } from "$lib/contexts/languageContext.js";
|
||||||
import "$lib/css/christ.css";
|
import "$lib/css/christ.css";
|
||||||
import "$lib/css/nordtheme.css";
|
import "$lib/css/nordtheme.css";
|
||||||
import Gebet from "./Gebet.svelte";
|
import Gebet from "./Gebet.svelte";
|
||||||
|
import LanguageToggle from "$lib/components/LanguageToggle.svelte";
|
||||||
import Kreuzzeichen from "$lib/components/prayers/Kreuzzeichen.svelte";
|
import Kreuzzeichen from "$lib/components/prayers/Kreuzzeichen.svelte";
|
||||||
import GloriaPatri from "$lib/components/prayers/GloriaPatri.svelte";
|
import GloriaPatri from "$lib/components/prayers/GloriaPatri.svelte";
|
||||||
import Paternoster from "$lib/components/prayers/Paternoster.svelte";
|
import Paternoster from "$lib/components/prayers/Paternoster.svelte";
|
||||||
@@ -13,6 +15,9 @@
|
|||||||
import MichaelGebet from "$lib/components/prayers/MichaelGebet.svelte";
|
import MichaelGebet from "$lib/components/prayers/MichaelGebet.svelte";
|
||||||
import BruderKlausGebet from "$lib/components/prayers/BruderKlausGebet.svelte";
|
import BruderKlausGebet from "$lib/components/prayers/BruderKlausGebet.svelte";
|
||||||
import JosephGebet from "$lib/components/prayers/JosephGebet.svelte";
|
import JosephGebet from "$lib/components/prayers/JosephGebet.svelte";
|
||||||
|
|
||||||
|
// Create language context for prayer components
|
||||||
|
createLanguageContext();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.ccontainer{
|
.ccontainer{
|
||||||
@@ -41,6 +46,7 @@ h1{
|
|||||||
</style>
|
</style>
|
||||||
<h1>Gebete</h1>
|
<h1>Gebete</h1>
|
||||||
|
|
||||||
|
<LanguageToggle />
|
||||||
|
|
||||||
<div class="ccontainer">
|
<div class="ccontainer">
|
||||||
<div class=container>
|
<div class=container>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ 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";
|
import BibleModal from "$lib/components/BibleModal.svelte";
|
||||||
|
import Toggle from "$lib/components/Toggle.svelte";
|
||||||
|
import LanguageToggle from "$lib/components/LanguageToggle.svelte";
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
@@ -115,25 +117,17 @@ const mysteryTitles = {
|
|||||||
// Toggle for including Luminous mysteries
|
// Toggle for including Luminous mysteries
|
||||||
let includeLuminous = true;
|
let includeLuminous = true;
|
||||||
|
|
||||||
// Create language context for prayer components
|
|
||||||
const { showLatin } = createLanguageContext();
|
|
||||||
let showBilingual = true;
|
|
||||||
|
|
||||||
// Flag to prevent saving before we've loaded from localStorage
|
// Flag to prevent saving before we've loaded from localStorage
|
||||||
let hasLoadedFromStorage = false;
|
let hasLoadedFromStorage = false;
|
||||||
|
|
||||||
// Sync checkbox with context
|
// Create language context for prayer components (LanguageToggle will use this)
|
||||||
$: $showLatin = showBilingual;
|
createLanguageContext();
|
||||||
|
|
||||||
// Save toggle states to localStorage whenever they change (but only after initial load)
|
// Save luminous toggle state to localStorage whenever it changes (but only after initial load)
|
||||||
$: if (typeof localStorage !== 'undefined' && hasLoadedFromStorage) {
|
$: if (typeof localStorage !== 'undefined' && hasLoadedFromStorage) {
|
||||||
localStorage.setItem('rosary_includeLuminous', includeLuminous.toString());
|
localStorage.setItem('rosary_includeLuminous', includeLuminous.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (typeof localStorage !== 'undefined' && hasLoadedFromStorage) {
|
|
||||||
localStorage.setItem('rosary_showBilingual', showBilingual.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to get the appropriate mystery for a given weekday
|
// Function to get the appropriate mystery for a given weekday
|
||||||
function getMysteryForWeekday(date, includeLuminous) {
|
function getMysteryForWeekday(date, includeLuminous) {
|
||||||
const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
||||||
@@ -274,18 +268,13 @@ const sectionPositions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Load toggle states from localStorage
|
// Load toggle state from localStorage
|
||||||
const savedIncludeLuminous = localStorage.getItem('rosary_includeLuminous');
|
const savedIncludeLuminous = localStorage.getItem('rosary_includeLuminous');
|
||||||
const savedShowBilingual = localStorage.getItem('rosary_showBilingual');
|
|
||||||
|
|
||||||
if (savedIncludeLuminous !== null) {
|
if (savedIncludeLuminous !== null) {
|
||||||
includeLuminous = savedIncludeLuminous === 'true';
|
includeLuminous = savedIncludeLuminous === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedShowBilingual !== null) {
|
|
||||||
showBilingual = savedShowBilingual === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recalculate mystery based on loaded includeLuminous value
|
// Recalculate mystery based on loaded includeLuminous value
|
||||||
todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
|
todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
|
||||||
selectMystery(todaysMystery);
|
selectMystery(todaysMystery);
|
||||||
@@ -912,78 +901,6 @@ h1 {
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Luminous mysteries toggle */
|
|
||||||
.luminous-toggle {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.luminous-toggle label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--nord4);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(prefers-color-scheme: light) {
|
|
||||||
.luminous-toggle label {
|
|
||||||
color: var(--nord2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.luminous-toggle span {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* iOS-style toggle switch */
|
|
||||||
.luminous-toggle input[type="checkbox"] {
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 51px;
|
|
||||||
height: 31px;
|
|
||||||
background: var(--nord2);
|
|
||||||
border-radius: 31px;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s ease;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(prefers-color-scheme: light) {
|
|
||||||
.luminous-toggle input[type="checkbox"] {
|
|
||||||
background: var(--nord4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.luminous-toggle input[type="checkbox"]:checked {
|
|
||||||
background: var(--nord14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.luminous-toggle input[type="checkbox"]::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 27px;
|
|
||||||
height: 27px;
|
|
||||||
border-radius: 50%;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
background: white;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.luminous-toggle input[type="checkbox"]:checked::before {
|
|
||||||
transform: translateX(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mystery selector grid */
|
/* Mystery selector grid */
|
||||||
.mystery-selector {
|
.mystery-selector {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1310,20 +1227,14 @@ l536 389l-209 -629zM1671 934l-370 267l150 436l-378 -271l-371 271q8 -34 15 -68q10
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Luminous Mysteries Toggle -->
|
<!-- Luminous Mysteries Toggle -->
|
||||||
<div class="luminous-toggle">
|
<Toggle
|
||||||
<label>
|
bind:checked={includeLuminous}
|
||||||
<input type="checkbox" bind:checked={includeLuminous} on:change={handleToggleChange} />
|
label="Lichtreiche Geheimnisse einbeziehen"
|
||||||
<span>Lichtreiche Geheimnisse einbeziehen</span>
|
on:change={handleToggleChange}
|
||||||
</label>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Language Toggle -->
|
<!-- Language Toggle -->
|
||||||
<div class="luminous-toggle">
|
<LanguageToggle />
|
||||||
<label>
|
|
||||||
<input type="checkbox" bind:checked={showBilingual} />
|
|
||||||
<span>Lateinisch und Deutsch anzeigen</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rosary-layout">
|
<div class="rosary-layout">
|
||||||
<!-- Sidebar: Rosary Visualization -->
|
<!-- Sidebar: Rosary Visualization -->
|
||||||
|
|||||||
Reference in New Issue
Block a user