fix: eliminate all 167 svelte-check warnings

Refactor page components to use $derived + invalidateAll() where data
is read-only or re-fetched after mutations. Suppress state_referenced_locally
for intentional patterns (form state, optimistic updates, pagination).
Fix a11y issues with role="presentation", add standard line-clamp properties,
remove unused CSS selectors and empty rulesets.
This commit is contained in:
2026-04-08 14:05:56 +02:00
parent 5093db8281
commit 39ffe1732f
38 changed files with 152 additions and 112 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.11.1", "version": "1.11.2",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+1
View File
@@ -10,6 +10,7 @@
children children
} = $props(); } = $props();
// svelte-ignore state_referenced_locally
let isVisible = $state(eager); // If eager=true, render immediately let isVisible = $state(eager); // If eager=true, render immediately
/** @type {HTMLDivElement | null} */ /** @type {HTMLDivElement | null} */
let containerRef = $state(null); let containerRef = $state(null);
+1
View File
@@ -11,6 +11,7 @@
...restProps ...restProps
} = $props(); } = $props();
// svelte-ignore state_referenced_locally
let shouldLoad = $state(eager); let shouldLoad = $state(eager);
/** @type {HTMLImageElement | null} */ /** @type {HTMLImageElement | null} */
let imgElement = $state(null); let imgElement = $state(null);
@@ -5,16 +5,19 @@
let { initialBalance = null, initialDebtData = null } = $props<{ initialBalance?: any, initialDebtData?: any }>(); let { initialBalance = null, initialDebtData = null } = $props<{ initialBalance?: any, initialDebtData?: any }>();
// svelte-ignore state_referenced_locally
let balance = $state(initialBalance || { let balance = $state(initialBalance || {
netBalance: 0, netBalance: 0,
recentSplits: [] recentSplits: []
}); });
// svelte-ignore state_referenced_locally
let debtData = $state(initialDebtData || { let debtData = $state(initialDebtData || {
whoOwesMe: [], whoOwesMe: [],
whoIOwe: [], whoIOwe: [],
totalOwedToMe: 0, totalOwedToMe: 0,
totalIOwe: 0 totalIOwe: 0
}); });
// svelte-ignore state_referenced_locally
let loading = $state(!initialBalance || !initialDebtData); let loading = $state(!initialBalance || !initialDebtData);
let error = $state<string | null>(null); let error = $state<string | null>(null);
@@ -17,10 +17,14 @@
const isEnglish = $derived(lang === 'en'); const isEnglish = $derived(lang === 'en');
// svelte-ignore state_referenced_locally
let book: string = $state(verseData?.book || ''); let book: string = $state(verseData?.book || '');
// svelte-ignore state_referenced_locally
let chapter: number = $state(verseData?.chapter || 0); let chapter: number = $state(verseData?.chapter || 0);
// svelte-ignore state_referenced_locally
let verses: Array<{ verse: number; text: string }> = $state(verseData?.verses || []); let verses: Array<{ verse: number; text: string }> = $state(verseData?.verses || []);
let loading = $state(false); let loading = $state(false);
// svelte-ignore state_referenced_locally
let error = $state(verseData ? '' : (lang === 'en' ? 'No verse data available' : 'Keine Versdaten verfügbar')); let error = $state(verseData ? '' : (lang === 'en' ? 'No verse data available' : 'Keine Versdaten verfügbar'));
function handleBackdropClick(event: MouseEvent) { function handleBackdropClick(event: MouseEvent) {
@@ -9,6 +9,7 @@
*/ */
let { periods: initialPeriods = [], lang = 'en', sharedWith: initialSharedWith = [], readOnly = false, ownerName = '' } = $props(); let { periods: initialPeriods = [], lang = 'en', sharedWith: initialSharedWith = [], readOnly = false, ownerName = '' } = $props();
// svelte-ignore state_referenced_locally
let periods = $state([...initialPeriods]); let periods = $state([...initialPeriods]);
let loading = $state(false); let loading = $state(false);
let showAddForm = $state(false); let showAddForm = $state(false);
@@ -24,6 +25,7 @@
let showHistory = $state(false); let showHistory = $state(false);
// Sharing state // Sharing state
// svelte-ignore state_referenced_locally
let shareList = $state([...initialSharedWith]); let shareList = $state([...initialSharedWith]);
let showShare = $state(false); let showShare = $state(false);
let shareInput = $state(''); let shareInput = $state('');
@@ -855,10 +857,10 @@
<!-- Share modal --> <!-- Share modal -->
{#if showShare} {#if showShare}
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="share-overlay" onclick={() => showShare = false} onkeydown={(e) => e.key === 'Escape' && (showShare = false)}> <div class="share-overlay" onclick={() => showShare = false} onkeydown={(e) => e.key === 'Escape' && (showShare = false)}>
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="share-modal" onclick={(e) => e.stopPropagation()}> <div class="share-modal" role="presentation" onclick={(e) => e.stopPropagation()}>
<div class="share-modal-header"> <div class="share-modal-header">
<h3>{t('share', lang)}</h3> <h3>{t('share', lang)}</h3>
<button class="share-modal-close" onclick={() => showShare = false}> <button class="share-modal-close" onclick={() => showShare = false}>
@@ -14,8 +14,8 @@
<svelte:window onkeydown={handleKeydown} /> <svelte:window onkeydown={handleKeydown} />
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="video-overlay" onclick={handleBackdrop}> <div class="video-overlay" role="presentation" onclick={handleBackdrop}>
<button class="close-btn" onclick={onClose} aria-label="Close video"> <button class="close-btn" onclick={onClose} aria-label="Close video">
<X size={24} /> <X size={24} />
</button> </button>
@@ -15,6 +15,7 @@ let {
} = $props(); } = $props();
// Unique dialog ID based on type to prevent conflicts when both are on the same page // Unique dialog ID based on type to prevent conflicts when both are on the same page
// svelte-ignore state_referenced_locally
const dialogId = `base-recipe-selector-modal-${type}`; const dialogId = `base-recipe-selector-modal-${type}`;
let baseRecipes: any[] = $state([]); let baseRecipes: any[] = $state([]);
@@ -11,6 +11,7 @@
const isEnglish = $derived(lang === 'en'); const isEnglish = $derived(lang === 'en');
const label = $derived(isEnglish ? 'Favorites' : 'Favoriten'); const label = $derived(isEnglish ? 'Favorites' : 'Favoriten');
// svelte-ignore state_referenced_locally
let checked = $state(enabled); let checked = $state(enabled);
// Watch for changes to checked and call onToggle // Watch for changes to checked and call onToggle
@@ -120,6 +120,7 @@ const flattenedIngredients = $derived.by(() => {
const lang = data.lang || 'de'; const lang = data.lang || 'de';
return flattenIngredientReferences(data.ingredients, lang); return flattenIngredientReferences(data.ingredients, lang);
}); });
// svelte-ignore state_referenced_locally
let multiplier = $state(data.multiplier || 1); let multiplier = $state(data.multiplier || 1);
const isEnglish = $derived(data.lang === 'en'); const isEnglish = $derived(data.lang === 'en');
@@ -138,10 +139,15 @@ const labels = $derived({
// Cake form scaling // Cake form scaling
const hasDefaultForm = $derived(!!data.defaultForm?.shape); const hasDefaultForm = $derived(!!data.defaultForm?.shape);
// svelte-ignore state_referenced_locally
let userFormShape = $state(data.defaultForm?.shape || 'round'); let userFormShape = $state(data.defaultForm?.shape || 'round');
// svelte-ignore state_referenced_locally
let userFormDiameter = $state(data.defaultForm?.diameter || 26); let userFormDiameter = $state(data.defaultForm?.diameter || 26);
// svelte-ignore state_referenced_locally
let userFormWidth = $state(data.defaultForm?.width || 20); let userFormWidth = $state(data.defaultForm?.width || 20);
// svelte-ignore state_referenced_locally
let userFormLength = $state(data.defaultForm?.length || 30); let userFormLength = $state(data.defaultForm?.length || 30);
// svelte-ignore state_referenced_locally
let userFormInnerDiameter = $state(data.defaultForm?.innerDiameter || 8); let userFormInnerDiameter = $state(data.defaultForm?.innerDiameter || 8);
/** @param {string} shape @param {number} diameter @param {number} width @param {number} length @param {number} innerDiameter */ /** @param {string} shape @param {number} diameter @param {number} width @param {number} length @param {number} innerDiameter */
@@ -1,6 +1,7 @@
<script> <script>
let { data } = $props(); let { data } = $props();
// svelte-ignore state_referenced_locally
let multiplier = $state(data.multiplier || 1); let multiplier = $state(data.multiplier || 1);
// Recursively flatten nested instruction references // Recursively flatten nested instruction references
@@ -11,6 +11,7 @@
const andLabel = $derived(isEnglish ? 'AND' : 'UND'); const andLabel = $derived(isEnglish ? 'AND' : 'UND');
const orLabel = $derived(isEnglish ? 'OR' : 'ODER'); const orLabel = $derived(isEnglish ? 'OR' : 'ODER');
// svelte-ignore state_referenced_locally
let checked = $state(useAndLogic); let checked = $state(useAndLogic);
// Watch for changes to checked and call onToggle // Watch for changes to checked and call onToggle
@@ -28,6 +28,7 @@
}: Props = $props(); }: Props = $props();
type TranslationState = 'idle' | 'translating' | 'preview' | 'approved' | 'error'; type TranslationState = 'idle' | 'translating' | 'preview' | 'approved' | 'error';
// svelte-ignore state_referenced_locally
let translationState = $state<TranslationState>(englishData ? 'preview' : 'idle'); let translationState = $state<TranslationState>(englishData ? 'preview' : 'idle');
let errorMessage = $state(''); let errorMessage = $state('');
let validationErrors = $state<string[]>([]); let validationErrors = $state<string[]>([]);
@@ -43,6 +44,7 @@
} }
// Eagerly initialize editableEnglish from germanData if no English translation exists // Eagerly initialize editableEnglish from germanData if no English translation exists
// svelte-ignore state_referenced_locally
let editableEnglish = $state<any>( let editableEnglish = $state<any>(
englishData ? { englishData ? {
...englishData, ...englishData,
+10
View File
@@ -31,17 +31,27 @@
{ tag: 'müll', icon: Trash2 }, { tag: 'müll', icon: Trash2 },
]; ];
// svelte-ignore state_referenced_locally
let title = $state(task?.title || ''); let title = $state(task?.title || '');
// svelte-ignore state_referenced_locally
let description = $state(task?.description || ''); let description = $state(task?.description || '');
/** @type {string[]} */ /** @type {string[]} */
// svelte-ignore state_referenced_locally
let selectedAssignees = $state(task?.assignees ? [...task.assignees] : []); let selectedAssignees = $state(task?.assignees ? [...task.assignees] : []);
/** @type {string[]} */ /** @type {string[]} */
// svelte-ignore state_referenced_locally
let selectedTags = $state(task?.tags ? [...task.tags] : []); let selectedTags = $state(task?.tags ? [...task.tags] : []);
// svelte-ignore state_referenced_locally
let difficulty = $state(task?.difficulty || ''); let difficulty = $state(task?.difficulty || '');
// svelte-ignore state_referenced_locally
let refreshMode = $state(task?.refreshMode || 'completion'); let refreshMode = $state(task?.refreshMode || 'completion');
// svelte-ignore state_referenced_locally
let isRecurring = $state(task?.isRecurring || false); let isRecurring = $state(task?.isRecurring || false);
// svelte-ignore state_referenced_locally
let frequencyType = $state(task?.frequency?.type || 'weekly'); let frequencyType = $state(task?.frequency?.type || 'weekly');
// svelte-ignore state_referenced_locally
let customDays = $state(task?.frequency?.customDays || 7); let customDays = $state(task?.frequency?.customDays || 7);
// svelte-ignore state_referenced_locally
let nextDueDate = $state( let nextDueDate = $state(
task?.nextDueDate task?.nextDueDate
? new Date(task.nextDueDate).toISOString().split('T')[0] ? new Date(task.nextDueDate).toISOString().split('T')[0]
@@ -32,6 +32,7 @@
let { data } = $props(); let { data } = $props();
// Create language context for prayer components // Create language context for prayer components
// svelte-ignore state_referenced_locally
const langContext = createLanguageContext({ urlLang: /** @type {'de' | 'en'} */(data.lang), initialLatin: data.lang === 'la' ? true : 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) // Update lang store when data.lang changes (e.g., after navigation)
@@ -113,6 +114,7 @@
postcommunio: ['eucharistic'], postcommunio: ['eucharistic'],
}; };
// svelte-ignore state_referenced_locally
let selectedCategory = $state(data.initialCategory); let selectedCategory = $state(data.initialCategory);
// JS-only search (hidden without JS) // JS-only search (hidden without JS)
@@ -27,6 +27,7 @@
let { data } = $props(); let { data } = $props();
// svelte-ignore state_referenced_locally
const langContext = createLanguageContext({ urlLang: /** @type {'de' | 'en'} */(data.lang), initialLatin: data.lang === 'la' ? true : data.initialLatin }); const langContext = createLanguageContext({ urlLang: /** @type {'de' | 'en'} */(data.lang), initialLatin: data.lang === 'la' ? true : data.initialLatin });
$effect(() => { $effect(() => {
@@ -30,9 +30,11 @@ import { setupScrollSync } from "./rosaryScrollSync.js";
let { data } = $props(); let { data } = $props();
// Toggle for including Luminous mysteries (initialized from URL param or default) // Toggle for including Luminous mysteries (initialized from URL param or default)
// svelte-ignore state_referenced_locally
let includeLuminous = $state(data.initialLuminous); let includeLuminous = $state(data.initialLuminous);
// Toggle for showing mystery images (initialized from URL param or default) // Toggle for showing mystery images (initialized from URL param or default)
// svelte-ignore state_referenced_locally
let showImages = $state(data.initialShowImages); let showImages = $state(data.initialShowImages);
// Flag to prevent saving before we've loaded from localStorage // Flag to prevent saving before we've loaded from localStorage
@@ -40,6 +42,7 @@ let hasLoadedFromStorage = $state(false);
// Create language context for prayer components (LanguageToggle will use this) // Create language context for prayer components (LanguageToggle will use this)
// For Latin route, force showLatin on so only Latin prayers render // For Latin route, force showLatin on so only Latin prayers render
// svelte-ignore state_referenced_locally
const langContext = createLanguageContext({ urlLang: /** @type {'en'|'de'} */ (data.lang), initialLatin: data.lang === 'la' ? true : data.initialLatin }); 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) // Update lang store when data.lang changes (e.g., after navigation)
@@ -71,8 +74,9 @@ $effect(() => {
}); });
// Use server-computed initial values (supports no-JS via URL params) // Use server-computed initial values (supports no-JS via URL params)
// svelte-ignore state_referenced_locally
let selectedMystery = $state(/** @type {MysteryType} */ (data.initialMystery)); let selectedMystery = $state(/** @type {MysteryType} */ (data.initialMystery));
let todaysMystery = $state(/** @type {MysteryType} */ (data.todaysMystery)); let todaysMystery = $derived(/** @type {MysteryType} */ (data.todaysMystery));
// Derive these values from selectedMystery so they update automatically // Derive these values from selectedMystery so they update automatically
let currentMysteries = $derived(mysteries[selectedMystery]); let currentMysteries = $derived(mysteries[selectedMystery]);
@@ -27,6 +27,7 @@
const hasHashedImage = (r: any) => r.images?.length > 0 && /\.\w+\.\w+$/.test(r.images[0].mediapath); const hasHashedImage = (r: any) => r.images?.length > 0 && /\.\w+\.\w+$/.test(r.images[0].mediapath);
// Server-generated random index ensures SSR and client pick the same hero // Server-generated random index ensures SSR and client pick the same hero
// svelte-ignore state_referenced_locally
const heroIndex = data.heroIndex; const heroIndex = data.heroIndex;
const heroRecipe = $derived.by(() => { const heroRecipe = $derived.by(() => {
const seasonPool = data.season.filter(hasHashedImage); const seasonPool = data.season.filter(hasHashedImage);
@@ -1,6 +1,8 @@
<script> <script>
let { data } = $props(); let { data } = $props();
// svelte-ignore state_referenced_locally
const isEnglish = data.lang === 'en'; const isEnglish = data.lang === 'en';
// svelte-ignore state_referenced_locally
const recipeLang = data.recipeLang; const recipeLang = data.recipeLang;
let processing = $state(false); let processing = $state(false);
@@ -3,12 +3,14 @@
let { data } = $props<{ data: PageData }>(); let { data } = $props<{ data: PageData }>();
// svelte-ignore state_referenced_locally
const isEnglish = data.lang === 'en'; const isEnglish = data.lang === 'en';
const pageTitle = isEnglish ? 'Administration' : 'Administration'; const pageTitle = isEnglish ? 'Administration' : 'Administration';
const pageDescription = isEnglish const pageDescription = isEnglish
? 'Manage recipes and content' ? 'Manage recipes and content'
: 'Rezepte und Inhalte verwalten'; : 'Rezepte und Inhalte verwalten';
// svelte-ignore state_referenced_locally
const links = [ const links = [
{ {
title: isEnglish ? 'Untranslated Recipes' : 'Unübersetzte Rezepte', title: isEnglish ? 'Untranslated Recipes' : 'Unübersetzte Rezepte',
@@ -19,10 +19,14 @@
let { data, form }: { data: PageData; form: ActionData } = $props(); let { data, form }: { data: PageData; form: ActionData } = $props();
// Recipe data state // Recipe data state — all form fields initialized from server data (intentionally local mutable state)
// svelte-ignore state_referenced_locally
let preamble = $state(data.recipe.preamble || ""); let preamble = $state(data.recipe.preamble || "");
// svelte-ignore state_referenced_locally
let addendum = $state(data.recipe.addendum || ""); let addendum = $state(data.recipe.addendum || "");
// svelte-ignore state_referenced_locally
let note = $state(data.recipe.note || ""); let note = $state(data.recipe.note || "");
// svelte-ignore state_referenced_locally
let image_preview_url = $state( let image_preview_url = $state(
"https://bocken.org/static/rezepte/thumb/" + "https://bocken.org/static/rezepte/thumb/" +
(data.recipe.images?.[0]?.mediapath || `${data.recipe.short_name}.webp`) (data.recipe.images?.[0]?.mediapath || `${data.recipe.short_name}.webp`)
@@ -31,15 +35,20 @@
// Translation workflow state // Translation workflow state
let showTranslationWorkflow = $state(false); let showTranslationWorkflow = $state(false);
// svelte-ignore state_referenced_locally
let translationData = $state<any>(data.recipe.translations?.en || null); let translationData = $state<any>(data.recipe.translations?.en || null);
let changedFields = $state<string[]>([]); let changedFields = $state<string[]>([]);
// Store original recipe data for change detection // Store original recipe data for change detection
// svelte-ignore state_referenced_locally
const originalRecipe = JSON.parse(JSON.stringify(data.recipe)); const originalRecipe = JSON.parse(JSON.stringify(data.recipe));
// svelte-ignore state_referenced_locally
const old_short_name = data.recipe.short_name; const old_short_name = data.recipe.short_name;
// Season and portions stores // Season and portions stores
// svelte-ignore state_referenced_locally
portions.update(() => data.recipe.portions || ""); portions.update(() => data.recipe.portions || "");
// svelte-ignore state_referenced_locally
let portions_local = $state<string>(data.recipe.portions || ""); let portions_local = $state<string>(data.recipe.portions || "");
$effect(() => { $effect(() => {
portions.subscribe((p) => { portions.subscribe((p) => {
@@ -47,7 +56,9 @@
}); });
}); });
// svelte-ignore state_referenced_locally
season.update(() => data.recipe.season || []); season.update(() => data.recipe.season || []);
// svelte-ignore state_referenced_locally
let season_local = $state<number[]>(data.recipe.season || []); let season_local = $state<number[]>(data.recipe.season || []);
$effect(() => { $effect(() => {
season.subscribe((s) => { season.subscribe((s) => {
@@ -55,6 +66,7 @@
}); });
}); });
// svelte-ignore state_referenced_locally
let card_data = $state({ let card_data = $state({
icon: data.recipe.icon || "", icon: data.recipe.icon || "",
category: data.recipe.category || "", category: data.recipe.category || "",
@@ -63,6 +75,7 @@
tags: data.recipe.tags || [], tags: data.recipe.tags || [],
}); });
// svelte-ignore state_referenced_locally
let add_info = $state({ let add_info = $state({
preparation: data.recipe.preparation || "", preparation: data.recipe.preparation || "",
fermentation: { fermentation: {
@@ -78,13 +91,20 @@
cooking: data.recipe.cooking || "", cooking: data.recipe.cooking || "",
}); });
// svelte-ignore state_referenced_locally
let images = $state(data.recipe.images || []); let images = $state(data.recipe.images || []);
// svelte-ignore state_referenced_locally
let short_name = $state(data.recipe.short_name || ""); let short_name = $state(data.recipe.short_name || "");
// svelte-ignore state_referenced_locally
let datecreated = $state(data.recipe.datecreated); let datecreated = $state(data.recipe.datecreated);
let datemodified = $state(new Date()); let datemodified = $state(new Date());
// svelte-ignore state_referenced_locally
let isBaseRecipe = $state(data.recipe.isBaseRecipe || false); let isBaseRecipe = $state(data.recipe.isBaseRecipe || false);
// svelte-ignore state_referenced_locally
let defaultForm = $state(data.recipe.defaultForm ? { ...data.recipe.defaultForm } : null); let defaultForm = $state(data.recipe.defaultForm ? { ...data.recipe.defaultForm } : null);
// svelte-ignore state_referenced_locally
let ingredients = $state(data.recipe.ingredients || []); let ingredients = $state(data.recipe.ingredients || []);
// svelte-ignore state_referenced_locally
let instructions = $state(data.recipe.instructions || []); let instructions = $state(data.recipe.instructions || []);
// Form submission state // Form submission state
@@ -245,6 +265,7 @@
} }
// Nutrition state — all edits are local until form save // Nutrition state — all edits are local until form save
// svelte-ignore state_referenced_locally
let nutritionMappings = $state<any[]>(data.recipe.nutritionMappings || []); let nutritionMappings = $state<any[]>(data.recipe.nutritionMappings || []);
let generatingNutrition = $state(false); let generatingNutrition = $state(false);
let searchQueries = $state<Record<string, string>>({}); let searchQueries = $state<Record<string, string>>({});
@@ -3,6 +3,7 @@
let { data } = $props(); let { data } = $props();
// svelte-ignore state_referenced_locally
let items = $state(data.items ?? []); let items = $state(data.items ?? []);
const isEnglish = $derived(data.lang === 'en'); const isEnglish = $derived(data.lang === 'en');
+1 -1
View File
@@ -14,7 +14,7 @@
let showModal = $state(false); let showModal = $state(false);
/** @type {string | null} */ /** @type {string | null} */
let paymentId = $state(null); let paymentId = $state(null);
let user = $state(data.session?.user); let user = $derived(data.session?.user);
let isGuest = $derived(!data.session?.user); let isGuest = $derived(!data.session?.user);
$effect(() => { $effect(() => {
+16 -55
View File
@@ -1,6 +1,7 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { invalidateAll } from '$app/navigation';
import { pushState } from '$app/navigation'; import { pushState } from '$app/navigation';
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte'; import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
import EnhancedBalance from '$lib/components/cospend/EnhancedBalance.svelte'; import EnhancedBalance from '$lib/components/cospend/EnhancedBalance.svelte';
@@ -16,14 +17,14 @@
let { data } = $props(); // Contains session data and balance from server let { data } = $props(); // Contains session data and balance from server
// Use server-side data, with fallback for progressive enhancement // Use server-side data, with fallback for progressive enhancement
let balance = $state(data.balance || { let balance = $derived(data.balance || {
netBalance: 0, netBalance: 0,
recentSplits: [] recentSplits: []
}); });
let loading = $state(false); // Start as false since we have server data let loading = $state(false);
/** @type {string | null} */ /** @type {string | null} */
let error = $state(null); let error = $state(null);
let monthlyExpensesData = $state(/** @type {any} */ (data).monthlyExpensesData || { labels: [], datasets: [] }); let monthlyExpensesData = $state(/** @type {any} */ ({ labels: [], datasets: [] }));
let expensesLoading = $state(false); let expensesLoading = $state(false);
/** @type {string[] | null} */ /** @type {string[] | null} */
let categoryFilter = $state(null); let categoryFilter = $state(null);
@@ -40,71 +41,31 @@
/** @type {any} */ /** @type {any} */
let debtBreakdownComponent; let debtBreakdownComponent;
// Progressive enhancement: refresh data if JavaScript is available
onMount(() => {
// Mark that JavaScript is loaded for progressive enhancement
document.body.classList.add('js-loaded');
// Only fetch if we don't have server-side data
if (!balance.recentSplits || balance.recentSplits.length === 0) {
fetchBalance();
}
if (!monthlyExpensesData.datasets || monthlyExpensesData.datasets.length === 0) {
fetchMonthlyExpenses();
}
// Listen for dashboard refresh events from the layout
const handleDashboardRefresh = () => {
refreshAllComponents();
};
window.addEventListener('dashboardRefresh', handleDashboardRefresh);
// Cleanup
return () => {
window.removeEventListener('dashboardRefresh', handleDashboardRefresh);
};
});
async function fetchBalance() {
try {
loading = true;
const response = await fetch('/api/cospend/balance');
if (!response.ok) {
throw new Error('Failed to fetch balance');
}
balance = await response.json();
} catch (err) {
error = err instanceof Error ? err.message : String(err);
} finally {
loading = false;
}
}
async function fetchMonthlyExpenses() { async function fetchMonthlyExpenses() {
try { try {
expensesLoading = true; expensesLoading = true;
const response = await fetch('/api/cospend/monthly-expenses'); const response = await fetch('/api/cospend/monthly-expenses');
if (!response.ok) { if (response.ok) monthlyExpensesData = await response.json();
throw new Error('Failed to fetch monthly expenses');
}
monthlyExpensesData = await response.json();
} catch (err) { } catch (err) {
console.error('Error fetching monthly expenses:', err); console.error('Error fetching monthly expenses:', err);
// Don't show this error in the main error state
} finally { } finally {
expensesLoading = false; expensesLoading = false;
} }
} }
// Progressive enhancement: refresh data if JavaScript is available
onMount(() => {
document.body.classList.add('js-loaded');
fetchMonthlyExpenses();
const handleDashboardRefresh = () => { refreshAllComponents(); };
window.addEventListener('dashboardRefresh', handleDashboardRefresh);
return () => { window.removeEventListener('dashboardRefresh', handleDashboardRefresh); };
});
// Function to refresh all dashboard components after payment deletion // Function to refresh all dashboard components after payment deletion
async function refreshAllComponents() { async function refreshAllComponents() {
// Refresh the main balance and recent activity await Promise.all([invalidateAll(), fetchMonthlyExpenses()]);
await Promise.all([
fetchBalance(),
fetchMonthlyExpenses()
]);
// Refresh the enhanced balance component if it exists and has a refresh method // Refresh the enhanced balance component if it exists and has a refresh method
if (enhancedBalanceComponent && enhancedBalanceComponent.refresh) { if (enhancedBalanceComponent && enhancedBalanceComponent.refresh) {
+3
View File
@@ -475,6 +475,7 @@
<div class="edit-modal" onclick={(e) => e.stopPropagation()}> <div class="edit-modal" onclick={(e) => e.stopPropagation()}>
<h3>{parseQuantity(editingItem.name).name}</h3> <h3>{parseQuantity(editingItem.name).name}</h3>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="edit-label">Kategorie</label> <label class="edit-label">Kategorie</label>
<div class="category-picker"> <div class="category-picker">
{#each SHOPPING_CATEGORIES as cat} {#each SHOPPING_CATEGORIES as cat}
@@ -492,6 +493,7 @@
{/each} {/each}
</div> </div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="edit-label">Icon</label> <label class="edit-label">Icon</label>
<div class="icon-search"> <div class="icon-search">
<Search size={14} /> <Search size={14} />
@@ -838,6 +840,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
word-break: break-word; word-break: break-word;
} }
+4
View File
@@ -13,12 +13,16 @@
let { data } = $props(); let { data } = $props();
// Use server-side data with progressive enhancement // Use server-side data with progressive enhancement
// svelte-ignore state_referenced_locally
let payments = $state(data.payments || []); let payments = $state(data.payments || []);
let loading = $state(false); // Start as false since we have server data let loading = $state(false); // Start as false since we have server data
/** @type {string | null} */ /** @type {string | null} */
let error = $state(null); let error = $state(null);
// svelte-ignore state_referenced_locally
let currentPage = $state(Math.floor(data.currentOffset / data.limit)); let currentPage = $state(Math.floor(data.currentOffset / data.limit));
// svelte-ignore state_referenced_locally
let limit = $state(data.limit || 20); let limit = $state(data.limit || 20);
// svelte-ignore state_referenced_locally
let hasMore = $state(data.hasMore || false); let hasMore = $state(data.hasMore || false);
// Re-sync local state when server data changes (e.g. URL param navigation) // Re-sync local state when server data changes (e.g. URL param navigation)
@@ -16,7 +16,9 @@
// Initialize form data with server values if available (for error handling) // Initialize form data with server values if available (for error handling)
/** @type {Record<string, any>} */ /** @type {Record<string, any>} */
// svelte-ignore state_referenced_locally
const formValues = form?.values || {}; const formValues = form?.values || {};
// svelte-ignore state_referenced_locally
let formData = $state({ let formData = $state({
title: /** @type {string} */ (formValues.title || ''), title: /** @type {string} */ (formValues.title || ''),
description: /** @type {string} */ (formValues.description || ''), description: /** @type {string} */ (formValues.description || ''),
@@ -31,6 +33,7 @@
}); });
// Recurring payment settings // Recurring payment settings
// svelte-ignore state_referenced_locally
let recurringData = $state({ let recurringData = $state({
frequency: /** @type {string} */ (formValues.recurringFrequency || 'monthly'), frequency: /** @type {string} */ (formValues.recurringFrequency || 'monthly'),
cronExpression: /** @type {string} */ (formValues.recurringCronExpression || ''), cronExpression: /** @type {string} */ (formValues.recurringCronExpression || ''),
@@ -49,7 +52,9 @@
let personalAmounts = $state({}); let personalAmounts = $state({});
let loading = $state(false); let loading = $state(false);
/** @type {string | null} */ /** @type {string | null} */
// svelte-ignore state_referenced_locally
let error = $state(form?.error || null); let error = $state(form?.error || null);
// svelte-ignore state_referenced_locally
let predefinedMode = $state(data.predefinedUsers.length > 0); let predefinedMode = $state(data.predefinedUsers.length > 0);
let jsEnhanced = $state(false); let jsEnhanced = $state(false);
let cronError = $state(false); let cronError = $state(false);
@@ -67,6 +72,7 @@
let exchangeRateTimeout = $state(); let exchangeRateTimeout = $state();
// Initialize users from server data for no-JS support (use data directly to avoid reactivity warning) // Initialize users from server data for no-JS support (use data directly to avoid reactivity warning)
// svelte-ignore state_referenced_locally
const initialUsers = data.predefinedUsers.length > 0 ? [...data.predefinedUsers] : (data.currentUser ? [data.currentUser] : []); const initialUsers = data.predefinedUsers.length > 0 ? [...data.predefinedUsers] : (data.currentUser ? [data.currentUser] : []);
let users = $state(initialUsers); let users = $state(initialUsers);
@@ -1,6 +1,6 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto, invalidateAll } from '$app/navigation';
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte'; import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories'; import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
import EditButton from '$lib/components/EditButton.svelte'; import EditButton from '$lib/components/EditButton.svelte';
@@ -12,8 +12,8 @@
// Use server-side data with progressive enhancement // Use server-side data with progressive enhancement
/** @type {any | null} */ /** @type {any | null} */
let payment = $state(data.payment || null); let payment = $derived(data.payment || null);
let loading = $state(false); // Start as false since we have server data let loading = $state(false);
/** @type {string | null} */ /** @type {string | null} */
let error = $state(null); let error = $state(null);
@@ -24,26 +24,10 @@
// Only refresh if we don't have server data // Only refresh if we don't have server data
if (!payment) { if (!payment) {
await loadPayment(); await invalidateAll();
} }
}); });
async function loadPayment() {
try {
loading = true;
const response = await fetch(`/api/cospend/payments/${data.paymentId}`);
if (!response.ok) {
throw new Error('Failed to load payment');
}
const result = await response.json();
payment = result.payment;
} catch (err) {
error = err instanceof Error ? err.message : String(err);
} finally {
loading = false;
}
}
function formatAmountWithCurrency(/** @type {any} */ payment) { function formatAmountWithCurrency(/** @type {any} */ payment) {
if (payment.currency === 'CHF' || !payment.originalAmount) { if (payment.currency === 'CHF' || !payment.originalAmount) {
return formatCurrency(payment.amount, 'CHF', 'de-CH'); return formatCurrency(payment.amount, 'CHF', 'de-CH');
@@ -11,6 +11,7 @@
let { data } = $props(); let { data } = $props();
// svelte-ignore state_referenced_locally
let formData = $state({ let formData = $state({
title: '', title: '',
description: '', description: '',
+3 -1
View File
@@ -10,7 +10,7 @@
let { data, form } = $props(); let { data, form } = $props();
// Use server-side data with progressive enhancement // Use server-side data with progressive enhancement
let debtData = $state(data.debtData || { let debtData = $derived(data.debtData || {
whoOwesMe: [], whoOwesMe: [],
whoIOwe: [], whoIOwe: [],
totalOwedToMe: 0, totalOwedToMe: 0,
@@ -18,9 +18,11 @@
}); });
let loading = $state(false); // Start as false since we have server data let loading = $state(false); // Start as false since we have server data
/** @type {string | null} */ /** @type {string | null} */
// svelte-ignore state_referenced_locally
let error = $state(data.error || form?.error || null); let error = $state(data.error || form?.error || null);
/** @type {{ type: string; from: string; to: string; amount: number; description: string } | null} */ /** @type {{ type: string; from: string; to: string; amount: number; description: string } | null} */
let selectedSettlement = $state(null); let selectedSettlement = $state(null);
// svelte-ignore state_referenced_locally
let settlementAmount = $state(form?.values?.amount || ''); let settlementAmount = $state(form?.values?.amount || '');
let submitting = $state(false); let submitting = $state(false);
let predefinedMode = isPredefinedUsersMode(); let predefinedMode = isPredefinedUsersMode();
@@ -8,7 +8,9 @@
let { data } = $props(); let { data } = $props();
// svelte-ignore state_referenced_locally
let sessions = $state(data.sessions?.sessions ? [...data.sessions.sessions] : []); let sessions = $state(data.sessions?.sessions ? [...data.sessions.sessions] : []);
// svelte-ignore state_referenced_locally
let total = $state(data.sessions?.total ? data.sessions.total : 0); let total = $state(data.sessions?.total ? data.sessions.total : 0);
let loading = $state(false); let loading = $state(false);
let page = $state(1); let page = $state(1);
@@ -13,14 +13,20 @@
let { data } = $props(); let { data } = $props();
const workout = getWorkout(); const workout = getWorkout();
// svelte-ignore state_referenced_locally
let latest = $state(data.latest ? { ...data.latest } : {}); let latest = $state(data.latest ? { ...data.latest } : {});
// svelte-ignore state_referenced_locally
let measurements = $state(data.measurements?.measurements ? [...data.measurements.measurements] : []); let measurements = $state(data.measurements?.measurements ? [...data.measurements.measurements] : []);
let showWeightHistory = $state(false); let showWeightHistory = $state(false);
// Profile fields (sex, height, birth year) — stored in FitnessGoal // Profile fields (sex, height, birth year) — stored in FitnessGoal
// svelte-ignore state_referenced_locally
let savedSex = $state(data.profile?.sex ?? 'male'); let savedSex = $state(data.profile?.sex ?? 'male');
// svelte-ignore state_referenced_locally
let profileSex = $state(data.profile?.sex ?? 'male'); let profileSex = $state(data.profile?.sex ?? 'male');
// svelte-ignore state_referenced_locally
let profileHeight = $state(data.profile?.heightCm != null ? String(data.profile.heightCm) : ''); let profileHeight = $state(data.profile?.heightCm != null ? String(data.profile.heightCm) : '');
// svelte-ignore state_referenced_locally
let profileBirthYear = $state(data.profile?.birthYear != null ? String(data.profile.birthYear) : ''); let profileBirthYear = $state(data.profile?.birthYear != null ? String(data.profile.birthYear) : '');
let profileSaving = $state(false); let profileSaving = $state(false);
let profileEditing = $state(false); let profileEditing = $state(false);
@@ -146,7 +152,8 @@
{#if profileEditing} {#if profileEditing}
<div class="profile-fields"> <div class="profile-fields">
<div class="form-group"> <div class="form-group">
<label>{t('sex', lang)}</label> <!-- svelte-ignore a11y_label_has_associated_control -->
<label>{t('sex', lang)}</label>
<div class="sex-pills"> <div class="sex-pills">
<button class="sex-pill" class:active={profileSex === 'male'} onclick={() => profileSex = 'male'}> <button class="sex-pill" class:active={profileSex === 'male'} onclick={() => profileSex = 'male'}>
<Mars size={14} /> {t('male', lang)} <Mars size={14} /> {t('male', lang)}
@@ -357,7 +364,7 @@
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
.form-group input, .form-group select { .form-group input {
padding: 0.4rem 0.5rem; padding: 0.4rem 0.5rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 6px; border-radius: 6px;
@@ -10,6 +10,7 @@
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen'); const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
let { data } = $props(); let { data } = $props();
// svelte-ignore state_referenced_locally
const m = data.measurement?.measurement; const m = data.measurement?.measurement;
let saving = $state(false); let saving = $state(false);
@@ -15,6 +15,7 @@
let { data } = $props(); let { data } = $props();
// --- Date navigation --- // --- Date navigation ---
// svelte-ignore state_referenced_locally
let currentDate = $state(data.date); let currentDate = $state(data.date);
const todayStr = new Date().toISOString().slice(0, 10); const todayStr = new Date().toISOString().slice(0, 10);
const isToday = $derived(currentDate === todayStr); const isToday = $derived(currentDate === todayStr);
@@ -37,7 +38,9 @@
} }
// --- Entries --- // --- Entries ---
// svelte-ignore state_referenced_locally
let entries = $state(data.foodLog?.entries ?? []); let entries = $state(data.foodLog?.entries ?? []);
// svelte-ignore state_referenced_locally
let recipeImages = $state(data.recipeImages ?? {}); let recipeImages = $state(data.recipeImages ?? {});
async function loadEntries() { async function loadEntries() {
@@ -53,13 +56,21 @@
}); });
// --- Goals --- // --- Goals ---
// svelte-ignore state_referenced_locally
let goalCalories = $state(data.goal?.dailyCalories ?? null); let goalCalories = $state(data.goal?.dailyCalories ?? null);
// svelte-ignore state_referenced_locally
let goalProteinMode = $state(data.goal?.proteinMode ?? 'fixed'); let goalProteinMode = $state(data.goal?.proteinMode ?? 'fixed');
// svelte-ignore state_referenced_locally
let goalProteinTarget = $state(data.goal?.proteinTarget ?? null); let goalProteinTarget = $state(data.goal?.proteinTarget ?? null);
// svelte-ignore state_referenced_locally
let goalFatPercent = $state(data.goal?.fatPercent ?? null); let goalFatPercent = $state(data.goal?.fatPercent ?? null);
// svelte-ignore state_referenced_locally
let goalCarbPercent = $state(data.goal?.carbPercent ?? null); let goalCarbPercent = $state(data.goal?.carbPercent ?? null);
// svelte-ignore state_referenced_locally
let goalSex = $state(data.goal?.sex ?? 'male'); let goalSex = $state(data.goal?.sex ?? 'male');
// svelte-ignore state_referenced_locally
let activityLevel = $state(data.goal?.activityLevel ?? 'light'); let activityLevel = $state(data.goal?.activityLevel ?? 'light');
// svelte-ignore state_referenced_locally
let latestWeight = $state(data.latestWeight?.weight?.value ?? null); let latestWeight = $state(data.latestWeight?.weight?.value ?? null);
let showGoalEditor = $state(false); let showGoalEditor = $state(false);
@@ -395,6 +406,7 @@
const carbGoalGrams = $derived(goalCalories && goalCarbPercent ? (goalCalories * goalCarbPercent / 100) / 4 : null); const carbGoalGrams = $derived(goalCalories && goalCarbPercent ? (goalCalories * goalCarbPercent / 100) / 4 : null);
// --- Burned kcal --- // --- Burned kcal ---
// svelte-ignore state_referenced_locally
let exerciseKcal = $state(Number(data.exerciseKcal) || 0); let exerciseKcal = $state(Number(data.exerciseKcal) || 0);
// BMR via Mifflin-St Jeor (doi:10.1093/ajcn/51.2.241) // BMR via Mifflin-St Jeor (doi:10.1093/ajcn/51.2.241)
@@ -2417,9 +2429,6 @@
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.meal-section {
}
.meal-header { .meal-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -2505,6 +2514,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.food-card-link { .food-card-link {
@@ -1,4 +1,5 @@
<script> <script>
import { invalidateAll } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte'; import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
import MuscleHeatmap from '$lib/components/fitness/MuscleHeatmap.svelte'; import MuscleHeatmap from '$lib/components/fitness/MuscleHeatmap.svelte';
@@ -35,8 +36,8 @@
const stats = $derived(data.stats ?? {}); const stats = $derived(data.stats ?? {});
let goalStreak = $state(data.goal?.streak ?? 0); let goalStreak = $derived(data.goal?.streak ?? 0);
let goalWeekly = $state(data.goal?.weeklyWorkouts ?? null); let goalWeekly = $derived(data.goal?.weeklyWorkouts ?? null);
let goalEditing = $state(false); let goalEditing = $state(false);
let goalInput = $state(4); let goalInput = $state(4);
let goalSaving = $state(false); let goalSaving = $state(false);
@@ -57,9 +58,7 @@
body: JSON.stringify({ weeklyWorkouts: goalInput }) body: JSON.stringify({ weeklyWorkouts: goalInput })
}); });
if (res.ok) { if (res.ok) {
const d = await res.json(); await invalidateAll();
goalWeekly = d.weeklyWorkouts;
goalStreak = d.streak;
goalEditing = false; goalEditing = false;
} else { } else {
const err = await res.json().catch(() => null); const err = await res.json().catch(() => null);
@@ -18,13 +18,16 @@
const workout = getWorkout(); const workout = getWorkout();
const sync = getWorkoutSync(); const sync = getWorkoutSync();
// svelte-ignore state_referenced_locally
let templates = $state(data.templates?.templates ? [...data.templates.templates] : []); let templates = $state(data.templates?.templates ? [...data.templates.templates] : []);
let seeded = $state(false); let seeded = $state(false);
// Schedule state // Schedule state
/** @type {string[]} */ /** @type {string[]} */
// svelte-ignore state_referenced_locally
let scheduleOrder = $state(data.schedule?.schedule?.templateOrder ?? []); let scheduleOrder = $state(data.schedule?.schedule?.templateOrder ?? []);
/** @type {string | null} */ /** @type {string | null} */
// svelte-ignore state_referenced_locally
let nextTemplateId = $state(data.schedule?.nextTemplateId ?? null); let nextTemplateId = $state(data.schedule?.nextTemplateId ?? null);
let showScheduleEditor = $state(false); let showScheduleEditor = $state(false);
/** @type {string[]} */ /** @type {string[]} */
+3 -8
View File
@@ -13,8 +13,8 @@
let { data } = $props(); let { data } = $props();
let tasks = $state(data.tasks || []); let tasks = $derived(data.tasks || []);
let stats = $state(data.stats || { userStats: [], userStickers: [], recentCompletions: [] }); let stats = $derived(data.stats || { userStats: [], userStickers: [], recentCompletions: [] });
let currentUser = $derived(data.session?.user?.nickname || ''); let currentUser = $derived(data.session?.user?.nickname || '');
let myStat = $derived(stats.userStats.find((/** @type {any} */ s) => s._id === currentUser)); let myStat = $derived(stats.userStats.find((/** @type {any} */ s) => s._id === currentUser));
let showForm = $state(false); let showForm = $state(false);
@@ -130,12 +130,7 @@
} }
async function refreshTasks() { async function refreshTasks() {
const [tasksRes, statsRes] = await Promise.all([ await invalidateAll();
fetch('/api/tasks'),
fetch('/api/tasks/stats')
]);
if (tasksRes.ok) tasks = (await tasksRes.json()).tasks;
if (statsRes.ok) stats = await statsRes.json();
} }
async function handleTaskSaved() { async function handleTaskSaved() {
+4 -9
View File
@@ -1,4 +1,5 @@
<script> <script>
import { invalidateAll } from '$app/navigation';
import { STICKERS, getStickerById, getRarityColor } from '$lib/utils/stickers'; import { STICKERS, getStickerById, getRarityColor } from '$lib/utils/stickers';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { de } from 'date-fns/locale'; import { de } from 'date-fns/locale';
@@ -13,7 +14,7 @@
/** @type {import('$lib/utils/stickers').Sticker | null} */ /** @type {import('$lib/utils/stickers').Sticker | null} */
let selectedSticker = $state(null); let selectedSticker = $state(null);
let stats = $state(data.stats || { userStats: [], userStickers: [], recentCompletions: [] }); let stats = $derived(data.stats || { userStats: [], userStickers: [], recentCompletions: [] });
let currentUser = $derived(data.session?.user?.nickname || ''); let currentUser = $derived(data.session?.user?.nickname || '');
const rarityLabels = /** @type {Record<string, string>} */ ({ const rarityLabels = /** @type {Record<string, string>} */ ({
@@ -69,19 +70,13 @@
/** @param {string} id */ /** @param {string} id */
async function deleteCompletion(id) { async function deleteCompletion(id) {
const res = await fetch(`/api/tasks/completions/${id}`, { method: 'DELETE' }); const res = await fetch(`/api/tasks/completions/${id}`, { method: 'DELETE' });
if (res.ok) { if (res.ok) await invalidateAll();
const statsRes = await fetch('/api/tasks/stats');
if (statsRes.ok) stats = await statsRes.json();
}
} }
async function clearHistory() { async function clearHistory() {
if (!confirm('Deinen gesamten Verlauf und alle Sticker wirklich löschen? Das kann nicht rückgängig gemacht werden.')) return; if (!confirm('Deinen gesamten Verlauf und alle Sticker wirklich löschen? Das kann nicht rückgängig gemacht werden.')) return;
const res = await fetch('/api/tasks/stats', { method: 'DELETE' }); const res = await fetch('/api/tasks/stats', { method: 'DELETE' });
if (res.ok) { if (res.ok) await invalidateAll();
const [statsRes] = await Promise.all([fetch('/api/tasks/stats')]);
if (statsRes.ok) stats = await statsRes.json();
}
} }
</script> </script>