feat(frontend): unify editorial UI and DRY form architecture
This commit is contained in:
parent
d4fbc1faf5
commit
693c6a9626
35 changed files with 2600 additions and 1180 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
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 FormSectionCard from '$lib/components/forms/FormSectionCard.svelte';
|
||||
import SimpleSelect from '$lib/components/forms/SimpleSelect.svelte';
|
||||
|
||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||
|
||||
|
|
@ -15,12 +16,12 @@
|
|||
let showForm = $state(false);
|
||||
let kind = $state('supplement');
|
||||
|
||||
const kindColors: Record<string, string> = {
|
||||
prescription: 'bg-purple-100 text-purple-800',
|
||||
otc: 'bg-blue-100 text-blue-800',
|
||||
supplement: 'bg-green-100 text-green-800',
|
||||
herbal: 'bg-emerald-100 text-emerald-800',
|
||||
other: 'bg-gray-100 text-gray-700'
|
||||
const kindPills: Record<string, string> = {
|
||||
prescription: 'health-kind-pill health-kind-pill--prescription',
|
||||
otc: 'health-kind-pill health-kind-pill--otc',
|
||||
supplement: 'health-kind-pill health-kind-pill--supplement',
|
||||
herbal: 'health-kind-pill health-kind-pill--herbal',
|
||||
other: 'health-kind-pill health-kind-pill--other'
|
||||
};
|
||||
|
||||
const kindLabels: Record<string, () => string> = {
|
||||
|
|
@ -30,44 +31,43 @@
|
|||
herbal: m["medications_kindHerbal"],
|
||||
other: m["medications_kindOther"]
|
||||
};
|
||||
|
||||
const kindOptions = $derived(kinds.map((k) => ({ value: k, label: kindLabels[k]?.() ?? k })));
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{m.medications_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.medications_title()}</h2>
|
||||
<p class="text-muted-foreground">{m.medications_count({ count: data.medications.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.medications_title()}</h2>
|
||||
<p class="editorial-subtitle">{m.medications_count({ count: data.medications.length })}</p>
|
||||
<div class="editorial-toolbar">
|
||||
<Button href={resolve('/health/lab-results')} variant="outline">{m["labResults_title"]()}</Button>
|
||||
<Button variant="outline" onclick={() => (showForm = !showForm)}>
|
||||
{showForm ? m.common_cancel() : m["medications_addNew"]()}
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => (showForm = !showForm)}>
|
||||
{showForm ? m.common_cancel() : m["medications_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.medications_added()}</div>
|
||||
<div class="editorial-alert editorial-alert--success">{m.medications_added()}</div>
|
||||
{/if}
|
||||
|
||||
{#if showForm}
|
||||
<Card>
|
||||
<CardHeader><CardTitle>{m["medications_newTitle"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<FormSectionCard title={m["medications_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 col-span-2">
|
||||
<Label>{m.medications_kind()}</Label>
|
||||
<input type="hidden" name="kind" value={kind} />
|
||||
<Select type="single" value={kind} onValueChange={(v) => (kind = v)}>
|
||||
<SelectTrigger>{kindLabels[kind]?.() ?? kind}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each kinds as k (k)}
|
||||
<SelectItem value={k}>{kindLabels[k]?.() ?? k}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="col-span-2">
|
||||
<SimpleSelect
|
||||
id="kind"
|
||||
name="kind"
|
||||
label={m.medications_kind()}
|
||||
options={kindOptions}
|
||||
bind:value={kind}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="product_name">{m["medications_productName"]()}</Label>
|
||||
|
|
@ -85,16 +85,15 @@
|
|||
<Button type="submit">{m.common_add()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</FormSectionCard>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="editorial-panel reveal-2 space-y-3">
|
||||
{#each data.medications as med (med.record_id)}
|
||||
<div class="rounded-md border border-border px-4 py-3">
|
||||
<div class="health-entry-row">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="rounded-full px-2 py-0.5 text-xs font-medium {kindColors[med.kind] ?? ''}">
|
||||
<span class={kindPills[med.kind] ?? 'health-kind-pill'}>
|
||||
{kindLabels[med.kind]?.() ?? med.kind}
|
||||
</span>
|
||||
<span class="font-medium">{med.product_name}</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue