feat(frontend): localize skin active concerns with enum multi-select
This commit is contained in:
parent
30315fdf56
commit
9df241a6a9
4 changed files with 97 additions and 33 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(',')
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
excellent: 'state-pill state-pill--excellent',
|
||||
|
|
@ -53,6 +65,20 @@
|
|||
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);
|
||||
|
||||
// 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<string[]>([]);
|
||||
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<string[]>([]);
|
||||
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}
|
||||
/>
|
||||
<LabeledInputField
|
||||
id="active_concerns"
|
||||
<div class="space-y-2 col-span-2">
|
||||
<p class="text-sm font-medium">{m["skin_activeConcerns"]()}</p>
|
||||
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
{#each activeConcernOptions as option (option.value)}
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="active_concerns"
|
||||
label={m["skin_activeConcerns"]()}
|
||||
placeholder={m["skin_activeConcernsPlaceholder"]()}
|
||||
className="space-y-1 col-span-2"
|
||||
bind:value={activeConcernsRaw}
|
||||
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
|
||||
id="priorities"
|
||||
name="priorities"
|
||||
|
|
@ -455,14 +503,30 @@
|
|||
max="5"
|
||||
bind:value={editSebumCheeks}
|
||||
/>
|
||||
<LabeledInputField
|
||||
id="edit_active_concerns"
|
||||
<div class="space-y-2 col-span-2">
|
||||
<p class="text-sm font-medium">{m["skin_activeConcerns"]()}</p>
|
||||
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
{#each activeConcernOptions as option (option.value)}
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="active_concerns"
|
||||
label={m["skin_activeConcerns"]()}
|
||||
placeholder={m["skin_activeConcernsPlaceholder"]()}
|
||||
className="space-y-1 col-span-2"
|
||||
bind:value={editActiveConcernsRaw}
|
||||
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
|
||||
id="edit_priorities"
|
||||
name="priorities"
|
||||
|
|
@ -532,7 +596,7 @@
|
|||
{#if snap.active_concerns.length}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#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}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue