feat(frontend): group product selector by category in routine step forms

Products in the Add/Edit step dropdowns are now grouped by category
(Cleanser → Serum → Moisturizer → …) using SelectGroup/SelectGroupHeading,
and sorted alphabetically by brand then name within each group.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Piotr Oleszczyk 2026-03-01 18:22:51 +01:00
parent 78c67b6179
commit 3aa03b412b

View file

@ -10,7 +10,14 @@
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label'; import { Label } from '$lib/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/components/ui/select'; import {
Select,
SelectContent,
SelectGroup,
SelectGroupHeading,
SelectItem,
SelectTrigger
} from '$lib/components/ui/select';
import { Separator } from '$lib/components/ui/separator'; import { Separator } from '$lib/components/ui/separator';
let { data, form }: { data: PageData; form: ActionData } = $props(); let { data, form }: { data: PageData; form: ActionData } = $props();
@ -103,6 +110,34 @@
let selectedProductId = $state(''); let selectedProductId = $state('');
const GROOMING_ACTIONS: GroomingAction[] = ['shaving_razor', 'shaving_oneblade', 'dermarolling']; const GROOMING_ACTIONS: GroomingAction[] = ['shaving_razor', 'shaving_oneblade', 'dermarolling'];
const CATEGORY_ORDER = [
'cleanser', 'toner', 'essence', 'serum', 'moisturizer',
'spf', 'mask', 'exfoliant', 'spot_treatment', 'oil',
'hair_treatment', 'tool', 'other'
];
function formatCategory(cat: string): string {
return cat.charAt(0).toUpperCase() + cat.slice(1).replace(/_/g, ' ');
}
const groupedProducts = $derived.by(() => {
const groups = new Map<string, typeof products>();
for (const p of products) {
const key = p.category ?? 'other';
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(p);
}
for (const list of groups.values()) {
list.sort((a, b) => {
const b_cmp = (a.brand ?? '').localeCompare(b.brand ?? '');
return b_cmp !== 0 ? b_cmp : a.name.localeCompare(b.name);
});
}
return CATEGORY_ORDER
.filter((c) => groups.has(c))
.map((c) => [c, groups.get(c)!] as const);
});
</script> </script>
<svelte:head><title>Routine {routine.routine_date} {routine.part_of_day.toUpperCase()} — innercontext</title></svelte:head> <svelte:head><title>Routine {routine.routine_date} {routine.part_of_day.toUpperCase()} — innercontext</title></svelte:head>
@ -150,8 +185,13 @@
{/if} {/if}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{#each products as p (p.id)} {#each groupedProducts as [cat, items]}
<SelectItem value={p.id}>{p.name} ({p.brand})</SelectItem> <SelectGroup>
<SelectGroupHeading>{formatCategory(cat)}</SelectGroupHeading>
{#each items as p (p.id)}
<SelectItem value={p.id}>{p.name} · {p.brand}</SelectItem>
{/each}
</SelectGroup>
{/each} {/each}
</SelectContent> </SelectContent>
</Select> </Select>
@ -202,8 +242,13 @@
{/if} {/if}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{#each products as p (p.id)} {#each groupedProducts as [cat, items]}
<SelectItem value={p.id}>{p.name} ({p.brand})</SelectItem> <SelectGroup>
<SelectGroupHeading>{formatCategory(cat)}</SelectGroupHeading>
{#each items as p (p.id)}
<SelectItem value={p.id}>{p.name} · {p.brand}</SelectItem>
{/each}
</SelectGroup>
{/each} {/each}
</SelectContent> </SelectContent>
</Select> </Select>