From 3aa03b412b30f6fff71b6e4236fbb656d8064fe7 Mon Sep 17 00:00:00 2001 From: Piotr Oleszczyk Date: Sun, 1 Mar 2026 18:22:51 +0100 Subject: [PATCH] feat(frontend): group product selector by category in routine step forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/routes/routines/[id]/+page.svelte | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/frontend/src/routes/routines/[id]/+page.svelte b/frontend/src/routes/routines/[id]/+page.svelte index d7ac90a..b61124f 100644 --- a/frontend/src/routes/routines/[id]/+page.svelte +++ b/frontend/src/routes/routines/[id]/+page.svelte @@ -10,7 +10,14 @@ import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card'; import { Input } from '$lib/components/ui/input'; 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'; let { data, form }: { data: PageData; form: ActionData } = $props(); @@ -103,6 +110,34 @@ let selectedProductId = $state(''); 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(); + 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); + }); Routine {routine.routine_date} {routine.part_of_day.toUpperCase()} — innercontext @@ -150,8 +185,13 @@ {/if} - {#each products as p (p.id)} - {p.name} ({p.brand}) + {#each groupedProducts as [cat, items]} + + {formatCategory(cat)} + {#each items as p (p.id)} + {p.name} · {p.brand} + {/each} + {/each} @@ -202,8 +242,13 @@ {/if} - {#each products as p (p.id)} - {p.name} ({p.brand}) + {#each groupedProducts as [cat, items]} + + {formatCategory(cat)} + {#each items as p (p.id)} + {p.name} · {p.brand} + {/each} + {/each}