6275b526d8
New shared StreakInfoButton component — small (i) pip in the corner
of the rosary, Angelus, and Regina Cæli streak counters that opens a
modal with a short reflection on what the counter is for.
The text frames the streak as a tool for forming the *habit* of
regular prayer, not as a metric of piety; warns against mechanical
repetition with Mt 6:7 ("do not heap up empty phrases"); and grounds
the rest in CCC 2698 (rhythms of prayer), 2700 (heart present to him
to whom we are speaking), 2702 (body+spirit, habit forms us), and
2728 (the wounded pride that comes from treating prayer as personal
accomplishment).
Available in DE/EN/LA. Modal dismissable via X, click-outside, or
Escape; honours prefers-color-scheme.
Refactoring:
- StreakCounter and AngelusStreakCounter both render
<StreakInfoButton {lang} /> instead of duplicating the pip+modal.
Parents just declare position:relative as the anchor.
- AngelusStreakCounter is also used for Regina Cæli, so eastertide
visitors get the same explanation there for free.
Bump 1.67.0 -> 1.67.1.
152 lines
3.6 KiB
Svelte
152 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
import { browser } from '$app/environment';
|
|
import { getRosaryStreak } from '$lib/stores/rosaryStreak.svelte';
|
|
import StreakAura from '$lib/components/faith/StreakAura.svelte';
|
|
import StreakInfoButton from '$lib/components/faith/StreakInfoButton.svelte';
|
|
import { m, type FaithLang } from '$lib/js/faithI18n';
|
|
import { tick, onMount } from 'svelte';
|
|
|
|
let burst = $state(false);
|
|
let streak = $state<ReturnType<typeof getRosaryStreak> | null>(null);
|
|
|
|
interface Props {
|
|
streakData?: { length: number; lastPrayed: string | null } | null;
|
|
lang?: FaithLang;
|
|
isLoggedIn?: boolean;
|
|
}
|
|
|
|
let { streakData = null, lang = 'de', isLoggedIn = false }: Props = $props();
|
|
|
|
const t = $derived(m[lang]);
|
|
|
|
// Derive display values: use store when available, fall back to server data for SSR
|
|
let displayLength = $derived(streak?.length ?? streakData?.length ?? 0);
|
|
let prayedToday = $derived(streak?.prayedToday ?? (streakData?.lastPrayed === new Date().toISOString().split('T')[0]));
|
|
|
|
const dayLabel = $derived(displayLength === 1 ? t.day_singular : t.day_plural);
|
|
|
|
// Initialize store on mount (client-side only)
|
|
// Init with server data BEFORE assigning to streak, so displayLength
|
|
// never sees stale localStorage data from the singleton
|
|
onMount(() => {
|
|
const s = getRosaryStreak();
|
|
s.initWithServerData(streakData, isLoggedIn);
|
|
streak = s;
|
|
});
|
|
|
|
async function pray() {
|
|
burst = true;
|
|
await tick();
|
|
setTimeout(() => burst = false, 700);
|
|
streak?.recordPrayer();
|
|
}
|
|
</script>
|
|
|
|
<div class="streak-container" class:no-js-hidden={!isLoggedIn}>
|
|
<StreakInfoButton {lang} />
|
|
<div class="streak-display">
|
|
<StreakAura value={displayLength} {burst} />
|
|
<span class="streak-label">{dayLabel}</span>
|
|
</div>
|
|
<form method="POST" action="?/pray" onsubmit={(e) => { e.preventDefault(); pray(); }
|
|
}>
|
|
<button
|
|
class="streak-button"
|
|
type="submit"
|
|
disabled={prayedToday}
|
|
aria-label={t.mark_prayer}
|
|
>
|
|
{#if prayedToday}
|
|
{t.prayed_today}
|
|
{:else}
|
|
{t.prayed}
|
|
{/if}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<style>
|
|
.streak-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.5rem;
|
|
padding: 1rem 1.5rem;
|
|
background: var(--nord1);
|
|
border-radius: 12px;
|
|
width: fit-content;
|
|
/* Anchor for the absolute-positioned StreakInfoButton pip */
|
|
position: relative;
|
|
}
|
|
|
|
@media (prefers-color-scheme: light) {
|
|
:global(:root:not([data-theme="dark"])) .streak-container {
|
|
background: var(--nord5);
|
|
}
|
|
}
|
|
:global(:root[data-theme="light"]) .streak-container {
|
|
background: var(--nord5);
|
|
}
|
|
|
|
.streak-display {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
.streak-label {
|
|
font-size: 0.85rem;
|
|
color: var(--nord4);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
@media (prefers-color-scheme: light) {
|
|
:global(:root:not([data-theme="dark"])) .streak-label {
|
|
color: var(--nord3);
|
|
}
|
|
}
|
|
:global(:root[data-theme="light"]) .streak-label {
|
|
color: var(--nord3);
|
|
}
|
|
|
|
.streak-button {
|
|
padding: 0.75rem 1.5rem;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: var(--nord10);
|
|
color: white;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.streak-button:hover:not(:disabled) {
|
|
background: var(--nord9);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.streak-button:disabled {
|
|
background: var(--nord3);
|
|
cursor: default;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* Hide for non-logged-in users without JS (no form action available) */
|
|
.no-js-hidden {
|
|
display: none;
|
|
}
|
|
|
|
:global(html.js-enabled) .no-js-hidden {
|
|
display: flex;
|
|
}
|
|
|
|
@media (prefers-color-scheme: light) {
|
|
:global(:root:not([data-theme="dark"])) .streak-button:disabled {
|
|
background: var(--nord4);
|
|
}
|
|
}
|
|
:global(:root[data-theme="light"]) .streak-button:disabled {
|
|
background: var(--nord4);
|
|
}
|
|
</style>
|