feat(frontend): localize skin active concerns with enum multi-select

This commit is contained in:
Piotr Oleszczyk 2026-03-04 23:37:14 +01:00
parent 30315fdf56
commit 9df241a6a9
4 changed files with 97 additions and 33 deletions

View file

@ -314,7 +314,7 @@
"skin_sensitivity": "Sensitivity (15)", "skin_sensitivity": "Sensitivity (15)",
"skin_sebumTzone": "Sebum T-zone (15)", "skin_sebumTzone": "Sebum T-zone (15)",
"skin_sebumCheeks": "Sebum cheeks (15)", "skin_sebumCheeks": "Sebum cheeks (15)",
"skin_activeConcerns": "Active concerns (comma-separated)", "skin_activeConcerns": "Active concerns",
"skin_activeConcernsPlaceholder": "acne, redness, dehydration", "skin_activeConcernsPlaceholder": "acne, redness, dehydration",
"skin_priorities": "Priorities (comma-separated)", "skin_priorities": "Priorities (comma-separated)",
"skin_prioritiesPlaceholder": "strengthen barrier, reduce redness", "skin_prioritiesPlaceholder": "strengthen barrier, reduce redness",

View file

@ -328,7 +328,7 @@
"skin_sensitivity": "Wrażliwość (15)", "skin_sensitivity": "Wrażliwość (15)",
"skin_sebumTzone": "Sebum T-zone (15)", "skin_sebumTzone": "Sebum T-zone (15)",
"skin_sebumCheeks": "Sebum policzki (15)", "skin_sebumCheeks": "Sebum policzki (15)",
"skin_activeConcerns": "Aktywne problemy (przecinek)", "skin_activeConcerns": "Aktywne problemy",
"skin_activeConcernsPlaceholder": "trądzik, zaczerwienienie, odwodnienie", "skin_activeConcernsPlaceholder": "trądzik, zaczerwienienie, odwodnienie",
"skin_priorities": "Priorytety (przecinek)", "skin_priorities": "Priorytety (przecinek)",
"skin_prioritiesPlaceholder": "wzmocnić barierę, redukować zaczerwienienie", "skin_prioritiesPlaceholder": "wzmocnić barierę, redukować zaczerwienienie",

View file

@ -18,17 +18,17 @@ export const actions: Actions = {
const hydration_level = form.get('hydration_level') as string; const hydration_level = form.get('hydration_level') as string;
const sensitivity_level = form.get('sensitivity_level') as string; const sensitivity_level = form.get('sensitivity_level') as string;
const barrier_state = form.get('barrier_state') 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 priorities_raw = form.get('priorities') as string;
if (!snapshot_date) { if (!snapshot_date) {
return fail(400, { error: 'Date is required' }); return fail(400, { error: 'Date is required' });
} }
const active_concerns = active_concerns_raw const active_concerns = active_concerns_values;
?.split(',')
.map((c) => c.trim())
.filter(Boolean) ?? [];
const priorities = priorities_raw const priorities = priorities_raw
?.split(',') ?.split(',')
@ -68,7 +68,10 @@ export const actions: Actions = {
const hydration_level = form.get('hydration_level') as string; const hydration_level = form.get('hydration_level') as string;
const sensitivity_level = form.get('sensitivity_level') as string; const sensitivity_level = form.get('sensitivity_level') as string;
const barrier_state = form.get('barrier_state') 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 priorities_raw = form.get('priorities') as string;
const skin_type = form.get('skin_type') as string; const skin_type = form.get('skin_type') as string;
const sebum_tzone = form.get('sebum_tzone') 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' }); if (!id) return fail(400, { error: 'Missing id' });
const active_concerns = active_concerns_raw const active_concerns = active_concerns_values;
?.split(',')
.map((c) => c.trim())
.filter(Boolean) ?? [];
const priorities = priorities_raw const priorities = priorities_raw
?.split(',') ?.split(',')

View file

@ -16,6 +16,18 @@
const skinTextures = ['smooth', 'rough', 'flaky', 'bumpy']; const skinTextures = ['smooth', 'rough', 'flaky', 'bumpy'];
const barrierStates = ['intact', 'mildly_compromised', 'compromised']; const barrierStates = ['intact', 'mildly_compromised', 'compromised'];
const skinTypes = ['dry', 'oily', 'combination', 'sensitive', 'normal', 'acne_prone']; 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<string, string> = { const statePills: Record<string, string> = {
excellent: 'state-pill state-pill--excellent', excellent: 'state-pill state-pill--excellent',
@ -53,6 +65,20 @@
acne_prone: m["skin_typeAcneProne"] acne_prone: m["skin_typeAcneProne"]
}; };
const concernLabels: Record<string, () => 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); let showForm = $state(false);
// Create form state (bound to inputs so AI can pre-fill) // Create form state (bound to inputs so AI can pre-fill)
@ -65,7 +91,7 @@
let sensitivityLevel = $state(''); let sensitivityLevel = $state('');
let sebumTzone = $state(''); let sebumTzone = $state('');
let sebumCheeks = $state(''); let sebumCheeks = $state('');
let activeConcernsRaw = $state(''); let activeConcerns = $state<string[]>([]);
let prioritiesRaw = $state(''); let prioritiesRaw = $state('');
let notes = $state(''); let notes = $state('');
@ -80,7 +106,7 @@
let editSensitivityLevel = $state(''); let editSensitivityLevel = $state('');
let editSebumTzone = $state(''); let editSebumTzone = $state('');
let editSebumCheeks = $state(''); let editSebumCheeks = $state('');
let editActiveConcernsRaw = $state(''); let editActiveConcerns = $state<string[]>([]);
let editPrioritiesRaw = $state(''); let editPrioritiesRaw = $state('');
let editNotes = $state(''); let editNotes = $state('');
@ -95,7 +121,7 @@
editSensitivityLevel = snap.sensitivity_level != null ? String(snap.sensitivity_level) : ''; editSensitivityLevel = snap.sensitivity_level != null ? String(snap.sensitivity_level) : '';
editSebumTzone = snap.sebum_tzone != null ? String(snap.sebum_tzone) : ''; editSebumTzone = snap.sebum_tzone != null ? String(snap.sebum_tzone) : '';
editSebumCheeks = snap.sebum_cheeks != null ? String(snap.sebum_cheeks) : ''; editSebumCheeks = snap.sebum_cheeks != null ? String(snap.sebum_cheeks) : '';
editActiveConcernsRaw = snap.active_concerns?.join(', ') ?? ''; editActiveConcerns = [...(snap.active_concerns ?? [])];
editPrioritiesRaw = snap.priorities?.join(', ') ?? ''; editPrioritiesRaw = snap.priorities?.join(', ') ?? '';
editNotes = snap.notes ?? ''; editNotes = snap.notes ?? '';
showForm = false; showForm = false;
@ -112,6 +138,12 @@
const textureOptions = $derived(skinTextures.map((t) => ({ value: t, label: textureLabels[t]?.() ?? t }))); 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 skinTypeOptions = $derived(skinTypes.map((st) => ({ value: st, label: skinTypeLabels[st]?.() ?? st })));
const barrierOptions = $derived(barrierStates.map((b) => ({ value: b, label: barrierLabels[b]?.() ?? b }))); 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( const sortedSnapshots = $derived(
[...data.snapshots].sort((a, b) => b.snapshot_date.localeCompare(a.snapshot_date)) [...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.sensitivity_level != null) sensitivityLevel = String(r.sensitivity_level);
if (r.sebum_tzone != null) sebumTzone = String(r.sebum_tzone); if (r.sebum_tzone != null) sebumTzone = String(r.sebum_tzone);
if (r.sebum_cheeks != null) sebumCheeks = String(r.sebum_cheeks); 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.priorities?.length) prioritiesRaw = r.priorities.join(', ');
if (r.notes) notes = r.notes; if (r.notes) notes = r.notes;
aiModalOpen = false; aiModalOpen = false;
@ -332,14 +364,30 @@
max="5" max="5"
bind:value={sebumCheeks} bind:value={sebumCheeks}
/> />
<LabeledInputField <div class="space-y-2 col-span-2">
id="active_concerns" <p class="text-sm font-medium">{m["skin_activeConcerns"]()}</p>
name="active_concerns" <div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
label={m["skin_activeConcerns"]()} {#each activeConcernOptions as option (option.value)}
placeholder={m["skin_activeConcernsPlaceholder"]()} <label class="flex cursor-pointer items-center gap-2 text-sm">
className="space-y-1 col-span-2" <input
bind:value={activeConcernsRaw} type="checkbox"
/> name="active_concerns"
value={option.value}
checked={activeConcerns.includes(option.value)}
onchange={() => {
if (activeConcerns.includes(option.value)) {
activeConcerns = activeConcerns.filter((c) => c !== option.value);
} else {
activeConcerns = [...activeConcerns, option.value];
}
}}
class="rounded border-input"
/>
{option.label}
</label>
{/each}
</div>
</div>
<LabeledInputField <LabeledInputField
id="priorities" id="priorities"
name="priorities" name="priorities"
@ -455,14 +503,30 @@
max="5" max="5"
bind:value={editSebumCheeks} bind:value={editSebumCheeks}
/> />
<LabeledInputField <div class="space-y-2 col-span-2">
id="edit_active_concerns" <p class="text-sm font-medium">{m["skin_activeConcerns"]()}</p>
name="active_concerns" <div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
label={m["skin_activeConcerns"]()} {#each activeConcernOptions as option (option.value)}
placeholder={m["skin_activeConcernsPlaceholder"]()} <label class="flex cursor-pointer items-center gap-2 text-sm">
className="space-y-1 col-span-2" <input
bind:value={editActiveConcernsRaw} type="checkbox"
/> name="active_concerns"
value={option.value}
checked={editActiveConcerns.includes(option.value)}
onchange={() => {
if (editActiveConcerns.includes(option.value)) {
editActiveConcerns = editActiveConcerns.filter((c) => c !== option.value);
} else {
editActiveConcerns = [...editActiveConcerns, option.value];
}
}}
class="rounded border-input"
/>
{option.label}
</label>
{/each}
</div>
</div>
<LabeledInputField <LabeledInputField
id="edit_priorities" id="edit_priorities"
name="priorities" name="priorities"
@ -532,7 +596,7 @@
{#if snap.active_concerns.length} {#if snap.active_concerns.length}
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
{#each snap.active_concerns as c (c)} {#each snap.active_concerns as c (c)}
<Badge variant="secondary" class="text-xs">{c.replace(/_/g, ' ')}</Badge> <Badge variant="secondary" class="text-xs">{concernLabels[c]?.() ?? c.replace(/_/g, ' ')}</Badge>
{/each} {/each}
</div> </div>
{/if} {/if}