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