From 9df241a6a919bc7ee668bcd787921780af969c5f Mon Sep 17 00:00:00 2001 From: Piotr Oleszczyk Date: Wed, 4 Mar 2026 23:37:14 +0100 Subject: [PATCH] feat(frontend): localize skin active concerns with enum multi-select --- frontend/messages/en.json | 2 +- frontend/messages/pl.json | 2 +- frontend/src/routes/skin/+page.server.ts | 20 ++--- frontend/src/routes/skin/+page.svelte | 106 ++++++++++++++++++----- 4 files changed, 97 insertions(+), 33 deletions(-) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 442d6dc..61873e5 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -314,7 +314,7 @@ "skin_sensitivity": "Sensitivity (1–5)", "skin_sebumTzone": "Sebum T-zone (1–5)", "skin_sebumCheeks": "Sebum cheeks (1–5)", - "skin_activeConcerns": "Active concerns (comma-separated)", + "skin_activeConcerns": "Active concerns", "skin_activeConcernsPlaceholder": "acne, redness, dehydration", "skin_priorities": "Priorities (comma-separated)", "skin_prioritiesPlaceholder": "strengthen barrier, reduce redness", diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index 46edcc8..1e50b13 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -328,7 +328,7 @@ "skin_sensitivity": "Wrażliwość (1–5)", "skin_sebumTzone": "Sebum T-zone (1–5)", "skin_sebumCheeks": "Sebum policzki (1–5)", - "skin_activeConcerns": "Aktywne problemy (przecinek)", + "skin_activeConcerns": "Aktywne problemy", "skin_activeConcernsPlaceholder": "trądzik, zaczerwienienie, odwodnienie", "skin_priorities": "Priorytety (przecinek)", "skin_prioritiesPlaceholder": "wzmocnić barierę, redukować zaczerwienienie", diff --git a/frontend/src/routes/skin/+page.server.ts b/frontend/src/routes/skin/+page.server.ts index 90a1401..504e76f 100644 --- a/frontend/src/routes/skin/+page.server.ts +++ b/frontend/src/routes/skin/+page.server.ts @@ -18,17 +18,17 @@ export const actions: Actions = { const hydration_level = form.get('hydration_level') as string; const sensitivity_level = form.get('sensitivity_level') as string; const barrier_state = form.get('barrier_state') as string; - const active_concerns_raw = form.get('active_concerns') as string; + const active_concerns_values = form + .getAll('active_concerns') + .map((value) => String(value).trim()) + .filter(Boolean); const priorities_raw = form.get('priorities') as string; if (!snapshot_date) { return fail(400, { error: 'Date is required' }); } - const active_concerns = active_concerns_raw - ?.split(',') - .map((c) => c.trim()) - .filter(Boolean) ?? []; + const active_concerns = active_concerns_values; const priorities = priorities_raw ?.split(',') @@ -68,7 +68,10 @@ export const actions: Actions = { const hydration_level = form.get('hydration_level') as string; const sensitivity_level = form.get('sensitivity_level') as string; const barrier_state = form.get('barrier_state') as string; - const active_concerns_raw = form.get('active_concerns') as string; + const active_concerns_values = form + .getAll('active_concerns') + .map((value) => String(value).trim()) + .filter(Boolean); const priorities_raw = form.get('priorities') as string; const skin_type = form.get('skin_type') as string; const sebum_tzone = form.get('sebum_tzone') as string; @@ -76,10 +79,7 @@ export const actions: Actions = { if (!id) return fail(400, { error: 'Missing id' }); - const active_concerns = active_concerns_raw - ?.split(',') - .map((c) => c.trim()) - .filter(Boolean) ?? []; + const active_concerns = active_concerns_values; const priorities = priorities_raw ?.split(',') diff --git a/frontend/src/routes/skin/+page.svelte b/frontend/src/routes/skin/+page.svelte index 5a386bb..2922c56 100644 --- a/frontend/src/routes/skin/+page.svelte +++ b/frontend/src/routes/skin/+page.svelte @@ -16,6 +16,18 @@ const skinTextures = ['smooth', 'rough', 'flaky', 'bumpy']; const barrierStates = ['intact', 'mildly_compromised', 'compromised']; const skinTypes = ['dry', 'oily', 'combination', 'sensitive', 'normal', 'acne_prone']; + const activeConcernValues = [ + 'acne', + 'rosacea', + 'hyperpigmentation', + 'aging', + 'dehydration', + 'redness', + 'damaged_barrier', + 'pore_visibility', + 'uneven_texture', + 'sebum_excess' + ]; const statePills: Record = { excellent: 'state-pill state-pill--excellent', @@ -53,6 +65,20 @@ acne_prone: m["skin_typeAcneProne"] }; + const concernLabels: Record string> = { + acne: m["productForm_concernAcne"], + rosacea: m["productForm_concernRosacea"], + hyperpigmentation: m["productForm_concernHyperpigmentation"], + aging: m["productForm_concernAging"], + dehydration: m["productForm_concernDehydration"], + redness: m["productForm_concernRedness"], + damaged_barrier: m["productForm_concernDamagedBarrier"], + pore_visibility: m["productForm_concernPoreVisibility"], + uneven_texture: m["productForm_concernUnevenTexture"], + hair_growth: m["productForm_concernHairGrowth"], + sebum_excess: m["productForm_concernSebumExcess"] + }; + let showForm = $state(false); // Create form state (bound to inputs so AI can pre-fill) @@ -65,7 +91,7 @@ let sensitivityLevel = $state(''); let sebumTzone = $state(''); let sebumCheeks = $state(''); - let activeConcernsRaw = $state(''); + let activeConcerns = $state([]); let prioritiesRaw = $state(''); let notes = $state(''); @@ -80,7 +106,7 @@ let editSensitivityLevel = $state(''); let editSebumTzone = $state(''); let editSebumCheeks = $state(''); - let editActiveConcernsRaw = $state(''); + let editActiveConcerns = $state([]); let editPrioritiesRaw = $state(''); let editNotes = $state(''); @@ -95,7 +121,7 @@ editSensitivityLevel = snap.sensitivity_level != null ? String(snap.sensitivity_level) : ''; editSebumTzone = snap.sebum_tzone != null ? String(snap.sebum_tzone) : ''; editSebumCheeks = snap.sebum_cheeks != null ? String(snap.sebum_cheeks) : ''; - editActiveConcernsRaw = snap.active_concerns?.join(', ') ?? ''; + editActiveConcerns = [...(snap.active_concerns ?? [])]; editPrioritiesRaw = snap.priorities?.join(', ') ?? ''; editNotes = snap.notes ?? ''; showForm = false; @@ -112,6 +138,12 @@ const textureOptions = $derived(skinTextures.map((t) => ({ value: t, label: textureLabels[t]?.() ?? t }))); const skinTypeOptions = $derived(skinTypes.map((st) => ({ value: st, label: skinTypeLabels[st]?.() ?? st }))); const barrierOptions = $derived(barrierStates.map((b) => ({ value: b, label: barrierLabels[b]?.() ?? b }))); + const activeConcernOptions = $derived( + activeConcernValues.map((value) => ({ + value, + label: concernLabels[value]?.() ?? value.replace(/_/g, ' ') + })) + ); const sortedSnapshots = $derived( [...data.snapshots].sort((a, b) => b.snapshot_date.localeCompare(a.snapshot_date)) @@ -139,7 +171,7 @@ if (r.sensitivity_level != null) sensitivityLevel = String(r.sensitivity_level); if (r.sebum_tzone != null) sebumTzone = String(r.sebum_tzone); if (r.sebum_cheeks != null) sebumCheeks = String(r.sebum_cheeks); - if (r.active_concerns?.length) activeConcernsRaw = r.active_concerns.join(', '); + if (r.active_concerns?.length) activeConcerns = [...r.active_concerns]; if (r.priorities?.length) prioritiesRaw = r.priorities.join(', '); if (r.notes) notes = r.notes; aiModalOpen = false; @@ -332,14 +364,30 @@ max="5" bind:value={sebumCheeks} /> - +
+

{m["skin_activeConcerns"]()}

+
+ {#each activeConcernOptions as option (option.value)} + + {/each} +
+
- +
+

{m["skin_activeConcerns"]()}

+
+ {#each activeConcernOptions as option (option.value)} + + {/each} +
+
{#each snap.active_concerns as c (c)} - {c.replace(/_/g, ' ')} + {concernLabels[c]?.() ?? c.replace(/_/g, ' ')} {/each} {/if}