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_sensitivity": "Sensitivity (1–5)",
|
||||||
"skin_sebumTzone": "Sebum T-zone (1–5)",
|
"skin_sebumTzone": "Sebum T-zone (1–5)",
|
||||||
"skin_sebumCheeks": "Sebum cheeks (1–5)",
|
"skin_sebumCheeks": "Sebum cheeks (1–5)",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -328,7 +328,7 @@
|
||||||
"skin_sensitivity": "Wrażliwość (1–5)",
|
"skin_sensitivity": "Wrażliwość (1–5)",
|
||||||
"skin_sebumTzone": "Sebum T-zone (1–5)",
|
"skin_sebumTzone": "Sebum T-zone (1–5)",
|
||||||
"skin_sebumCheeks": "Sebum policzki (1–5)",
|
"skin_sebumCheeks": "Sebum policzki (1–5)",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -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(',')
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue