feat(frontend): unify editorial UI and DRY form architecture

This commit is contained in:
Piotr Oleszczyk 2026-03-04 21:43:37 +01:00
parent d4fbc1faf5
commit 693c6a9626
35 changed files with 2600 additions and 1180 deletions

View file

@ -5,10 +5,11 @@
import type { ActionData, PageData } from './$types';
import { m } from '$lib/paraglide/messages.js';
import { Button } from '$lib/components/ui/button';
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 { baseSelectClass } from '$lib/components/forms/form-classes';
import FormSectionCard from '$lib/components/forms/FormSectionCard.svelte';
import SimpleSelect from '$lib/components/forms/SimpleSelect.svelte';
import {
Table,
TableBody,
@ -21,68 +22,67 @@
let { data, form }: { data: PageData; form: ActionData } = $props();
const flags = ['N', 'ABN', 'POS', 'NEG', 'L', 'H'];
const flagColors: Record<string, string> = {
N: 'bg-green-100 text-green-800',
ABN: 'bg-red-100 text-red-800',
POS: 'bg-orange-100 text-orange-800',
NEG: 'bg-blue-100 text-blue-800',
L: 'bg-yellow-100 text-yellow-800',
H: 'bg-red-100 text-red-800'
const flagPills: Record<string, string> = {
N: 'health-flag-pill health-flag-pill--normal',
ABN: 'health-flag-pill health-flag-pill--abnormal',
POS: 'health-flag-pill health-flag-pill--positive',
NEG: 'health-flag-pill health-flag-pill--negative',
L: 'health-flag-pill health-flag-pill--low',
H: 'health-flag-pill health-flag-pill--high'
};
let showForm = $state(false);
let selectedFlag = $state('');
let filterFlag = $derived(data.flag ?? '');
const flagOptions = flags.map((f) => ({ value: f, label: f }));
function onFlagChange(v: string) {
const base = resolve('/health/lab-results');
const url = v ? base + '?flag=' + v : base;
goto(url, { replaceState: true });
const target = v ? `${base}?flag=${encodeURIComponent(v)}` : base;
goto(target, { replaceState: true });
}
</script>
<svelte:head><title>{m["labResults_title"]()} — innercontext</title></svelte:head>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">{m["labResults_title"]()}</h2>
<p class="text-muted-foreground">{m["labResults_count"]({ count: data.results.length })}</p>
<div class="editorial-page space-y-4">
<section class="editorial-hero reveal-1">
<p class="editorial-kicker">{m["nav_appSubtitle"]()}</p>
<h2 class="editorial-title">{m["labResults_title"]()}</h2>
<p class="editorial-subtitle">{m["labResults_count"]({ count: data.results.length })}</p>
<div class="editorial-toolbar">
<Button href={resolve('/health/medications')} variant="outline">{m.medications_title()}</Button>
<Button variant="outline" onclick={() => (showForm = !showForm)}>
{showForm ? m.common_cancel() : m["labResults_addNew"]()}
</Button>
</div>
<Button variant="outline" onclick={() => (showForm = !showForm)}>
{showForm ? m.common_cancel() : m["labResults_addNew"]()}
</Button>
</div>
</section>
{#if form?.error}
<div class="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">{form.error}</div>
<div class="editorial-alert editorial-alert--error">{form.error}</div>
{/if}
{#if form?.created}
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["labResults_added"]()}</div>
<div class="editorial-alert editorial-alert--success">{m["labResults_added"]()}</div>
{/if}
<!-- Filter -->
<div class="flex items-center gap-3">
<div class="editorial-panel reveal-2 flex items-center gap-3">
<span class="text-sm text-muted-foreground">{m["labResults_flagFilter"]()}</span>
<Select
type="single"
<select
class={`${baseSelectClass} w-32`}
value={filterFlag}
onValueChange={onFlagChange}
onchange={(e) => onFlagChange(e.currentTarget.value)}
>
<SelectTrigger class="w-32">{filterFlag || m["labResults_flagAll"]()}</SelectTrigger>
<SelectContent>
<SelectItem value="">{m["labResults_flagAll"]()}</SelectItem>
{#each flags as f (f)}
<SelectItem value={f}>{f}</SelectItem>
{/each}
</SelectContent>
</Select>
<option value="">{m["labResults_flagAll"]()}</option>
{#each flags as f (f)}
<option value={f}>{f}</option>
{/each}
</select>
</div>
{#if showForm}
<Card>
<CardHeader><CardTitle>{m["labResults_newTitle"]()}</CardTitle></CardHeader>
<CardContent>
<FormSectionCard title={m["labResults_newTitle"]()} className="reveal-2">
<form method="POST" action="?/create" use:enhance class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1">
<Label for="collected_at">{m["labResults_date"]()}</Label>
@ -108,29 +108,23 @@
<Label for="unit_original">{m["labResults_unit"]()}</Label>
<Input id="unit_original" name="unit_original" placeholder={m["labResults_unitPlaceholder"]()} />
</div>
<div class="space-y-1">
<Label>{m["labResults_flag"]()}</Label>
<input type="hidden" name="flag" value={selectedFlag} />
<Select type="single" value={selectedFlag} onValueChange={(v) => (selectedFlag = v)}>
<SelectTrigger>{selectedFlag || m["labResults_flagNone"]()}</SelectTrigger>
<SelectContent>
<SelectItem value="">{m["labResults_flagNone"]()}</SelectItem>
{#each flags as f (f)}
<SelectItem value={f}>{f}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<SimpleSelect
id="flag"
name="flag"
label={m["labResults_flag"]()}
options={flagOptions}
placeholder={m["labResults_flagNone"]()}
bind:value={selectedFlag}
/>
<div class="flex items-end">
<Button type="submit">{m.common_add()}</Button>
</div>
</form>
</CardContent>
</Card>
</FormSectionCard>
{/if}
<!-- Desktop: table -->
<div class="hidden rounded-md border border-border md:block">
<div class="products-table-shell hidden md:block reveal-2">
<Table>
<TableHeader>
<TableRow>
@ -159,7 +153,7 @@
</TableCell>
<TableCell>
{#if r.flag}
<span class="rounded-full px-2 py-0.5 text-xs font-medium {flagColors[r.flag] ?? ''}">
<span class={flagPills[r.flag] ?? 'health-flag-pill'}>
{r.flag}
</span>
{:else}
@ -180,13 +174,13 @@
</div>
<!-- Mobile: cards -->
<div class="flex flex-col gap-3 md:hidden">
<div class="flex flex-col gap-3 md:hidden reveal-3">
{#each data.results as r (r.record_id)}
<div class="rounded-lg border border-border p-4 flex flex-col gap-1">
<div class="products-mobile-card flex flex-col gap-1">
<div class="flex items-start justify-between gap-2">
<span class="font-medium">{r.test_name_original ?? r.test_code}</span>
{#if r.flag}
<span class="shrink-0 rounded-full px-2 py-0.5 text-xs font-medium {flagColors[r.flag] ?? ''}">
<span class={flagPills[r.flag] ?? 'health-flag-pill'}>
{r.flag}
</span>
{/if}