From 9d8d1ec41fa014721fe10539419f6e7c25614bd2 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Wed, 8 Apr 2026 22:21:21 +0200 Subject: [PATCH] refactor: extract RingGraph, StatsRingGraph, MacroBreakdown components Consolidate 6 duplicated instances of the SVG arc ring pattern into a composable component hierarchy: RingGraph (base ring), StatsRingGraph (ring with target markers), and MacroBreakdown (3 rings + kcal + detail table). Removes ~400 lines of duplicated SVG/CSS from FoodSearch, nutrition page, meals page, stats page, food detail page, and recipe NutritionSummary. --- package.json | 2 +- src/lib/components/fitness/FoodSearch.svelte | 183 ++---------------- .../components/fitness/MacroBreakdown.svelte | 161 +++++++++++++++ src/lib/components/fitness/RingGraph.svelte | 95 +++++++++ .../components/fitness/StatsRingGraph.svelte | 78 ++++++++ .../recipes/NutritionSummary.svelte | 81 ++------ .../[nutrition=fitnessNutrition]/+page.svelte | 154 +-------------- .../food/[source]/[id]/+page.svelte | 87 ++------- .../meals/+page.svelte | 52 +++-- .../fitness/[stats=fitnessStats]/+page.svelte | 129 ++---------- 10 files changed, 420 insertions(+), 602 deletions(-) create mode 100644 src/lib/components/fitness/MacroBreakdown.svelte create mode 100644 src/lib/components/fitness/RingGraph.svelte create mode 100644 src/lib/components/fitness/StatsRingGraph.svelte diff --git a/package.json b/package.json index 560b46a..81525a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.17.0", + "version": "1.17.1", "private": true, "type": "module", "scripts": { diff --git a/src/lib/components/fitness/FoodSearch.svelte b/src/lib/components/fitness/FoodSearch.svelte index fcb5c51..94db09f 100644 --- a/src/lib/components/fitness/FoodSearch.svelte +++ b/src/lib/components/fitness/FoodSearch.svelte @@ -3,6 +3,7 @@ import { browser } from '$app/environment'; import { Heart, ExternalLink, ScanBarcode, X } from '@lucide/svelte'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; + import MacroBreakdown from './MacroBreakdown.svelte'; /** * @type {{ @@ -30,15 +31,6 @@ const isEn = $derived(lang === 'en'); const btnLabel = $derived(confirmLabel ?? t('log_food', lang)); - // SVG ring constants - const RADIUS = 28; - const ARC_DEGREES = 300; - const ARC_LENGTH = (ARC_DEGREES / 360) * 2 * Math.PI * RADIUS; - const ARC_ROTATE = 120; - function strokeOffset(percent) { - return ARC_LENGTH - (Math.min(percent, 100) / 100) * ARC_LENGTH; - } - // --- Search state --- let query = $state(''); let results = $state(initialResults ?? []); @@ -126,21 +118,6 @@ }; }); - const macroPercent = $derived.by(() => { - if (!selected?.per100g) return { protein: 0, fat: 0, carbs: 0 }; - const n = selected.per100g; - const proteinCal = (n.protein ?? 0) * 4; - const fatCal = (n.fat ?? 0) * 9; - const carbsCal = (n.carbs ?? 0) * 4; - const total = proteinCal + fatCal + carbsCal; - if (total === 0) return { protein: 0, fat: 0, carbs: 0 }; - return { - protein: Math.round(proteinCal / total * 100), - fat: Math.round(fatCal / total * 100), - carbs: 100 - Math.round(proteinCal / total * 100) - Math.round(fatCal / total * 100), - }; - }); - function confirm() { if (!selected) return; const grams = resolveGrams(); @@ -195,11 +172,7 @@ } } - function fmt(v) { - if (v >= 100) return Math.round(v).toString(); - if (v >= 10) return v.toFixed(1); - return v.toFixed(1); - } + function sourceLabel(source) { if (source === 'bls') return 'BLS'; @@ -518,63 +491,15 @@ {/if} {#if previewNutrients} - -
- {previewNutrients.calories} - kcal -
- - -
- {#each [ - { pct: macroPercent.protein, label: isEn ? 'Protein' : 'Eiweiß', cls: 'fs-ring-protein', grams: previewNutrients.protein }, - { pct: macroPercent.fat, label: isEn ? 'Fat' : 'Fett', cls: 'fs-ring-fat', grams: previewNutrients.fat }, - { pct: macroPercent.carbs, label: isEn ? 'Carbs' : 'Kohlenh.', cls: 'fs-ring-carbs', grams: previewNutrients.carbs }, - ] as macro (macro.cls)} -
- - - - {macro.pct}% - - {macro.label} - {fmt(macro.grams)}g -
- {/each} -
- - -
-
- {isEn ? 'Protein' : 'Eiweiß'} - {fmt(previewNutrients.protein)} g -
-
- {isEn ? 'Fat' : 'Fett'} - {fmt(previewNutrients.fat)} g -
-
- {isEn ? 'Saturated Fat' : 'Ges. Fettsäuren'} - {fmt(previewNutrients.saturatedFat)} g -
-
- {isEn ? 'Carbohydrates' : 'Kohlenhydrate'} - {fmt(previewNutrients.carbs)} g -
-
- {isEn ? 'Sugars' : 'Zucker'} - {fmt(previewNutrients.sugars)} g -
-
- {isEn ? 'Fiber' : 'Ballaststoffe'} - {fmt(previewNutrients.fiber)} g -
-
+ {/if}
@@ -923,92 +848,6 @@ font-size: 0.75rem; color: var(--color-text-tertiary); } - .fs-detail-cal { - text-align: center; - margin: 0.25rem 0 0.25rem; - } - .fs-detail-cal-num { - font-size: 2.2rem; - font-weight: 800; - color: var(--color-text-primary); - line-height: 1; - } - .fs-detail-cal-unit { - font-size: 1rem; - font-weight: 600; - color: var(--color-text-secondary); - margin-left: 0.2rem; - } - .fs-detail-macros { - display: flex; - justify-content: space-around; - margin: 0.25rem 0 0.5rem; - } - .fs-detail-macro { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.1rem; - flex: 1; - } - .fs-detail-macro-label { - font-size: 0.78rem; - font-weight: 600; - color: var(--color-text-primary); - text-align: center; - } - .fs-detail-macro-val { - font-size: 0.72rem; - color: var(--color-text-tertiary); - } - .fs-ring-bg { - fill: none; - stroke: var(--color-border); - stroke-width: 5; - stroke-linecap: round; - } - .fs-ring-fill { - fill: none; - stroke-width: 5; - stroke-linecap: round; - transition: stroke-dashoffset 0.4s ease; - } - .fs-ring-text { - font-size: 14px; - font-weight: 700; - fill: currentColor; - text-anchor: middle; - dominant-baseline: central; - } - .fs-ring-protein { stroke: var(--nord14); } - .fs-ring-fat { stroke: var(--nord12); } - .fs-ring-carbs { stroke: var(--nord9); } - .fs-detail-rows { - background: var(--color-surface); - border-radius: 10px; - padding: 0.5rem 0.75rem; - border: 1px solid var(--color-border); - } - .fs-detail-row { - display: flex; - justify-content: space-between; - padding: 0.3rem 0; - border-bottom: 1px solid var(--color-border); - font-size: 0.85rem; - color: var(--color-text-primary); - } - .fs-detail-row:last-child { - border-bottom: none; - } - .fs-detail-row.sub span:first-child { - padding-left: 0.75rem; - color: var(--color-text-tertiary); - font-size: 0.8rem; - } - .fs-detail-row span:last-child { - color: var(--color-text-secondary); - font-variant-numeric: tabular-nums; - } /* ── Buttons ── */ .fs-actions { diff --git a/src/lib/components/fitness/MacroBreakdown.svelte b/src/lib/components/fitness/MacroBreakdown.svelte new file mode 100644 index 0000000..32d88de --- /dev/null +++ b/src/lib/components/fitness/MacroBreakdown.svelte @@ -0,0 +1,161 @@ + + +
+ {#if showCalories} +
+ {Math.round(calories)} + kcal +
+ {/if} + +
+ {#each macros as macro (macro.color)} + + {/each} +
+ + {#if showDetailRows} +
+
+ {isEn ? 'Protein' : 'Eiweiß'} + {fmt(protein)} g +
+
+ {isEn ? 'Fat' : 'Fett'} + {fmt(fat)} g +
+
+ {isEn ? 'Saturated Fat' : 'Ges. Fettsäuren'} + {fmt(saturatedFat)} g +
+
+ {isEn ? 'Carbohydrates' : 'Kohlenhydrate'} + {fmt(carbs)} g +
+
+ {isEn ? 'Sugars' : 'Zucker'} + {fmt(sugars)} g +
+
+ {isEn ? 'Fiber' : 'Ballaststoffe'} + {fmt(fiber)} g +
+
+ {/if} +
+ + diff --git a/src/lib/components/fitness/RingGraph.svelte b/src/lib/components/fitness/RingGraph.svelte new file mode 100644 index 0000000..1ded563 --- /dev/null +++ b/src/lib/components/fitness/RingGraph.svelte @@ -0,0 +1,95 @@ + + + + +
+ + + + {@render extra?.()} + {percent}% + + {#if label} + {label} + {/if} + {#if sublabel} + {sublabel} + {/if} +
+ + diff --git a/src/lib/components/fitness/StatsRingGraph.svelte b/src/lib/components/fitness/StatsRingGraph.svelte new file mode 100644 index 0000000..c9839a5 --- /dev/null +++ b/src/lib/components/fitness/StatsRingGraph.svelte @@ -0,0 +1,78 @@ + + + + {#snippet extra()} + {#if target != null} + {@const pos = targetMarkerPos(target)} + + {target}% + {/if} + {/snippet} + + + diff --git a/src/lib/components/recipes/NutritionSummary.svelte b/src/lib/components/recipes/NutritionSummary.svelte index 264272d..bd1e85a 100644 --- a/src/lib/components/recipes/NutritionSummary.svelte +++ b/src/lib/components/recipes/NutritionSummary.svelte @@ -1,5 +1,6 @@