feat(frontend): responsive design for mobile (RWD)
- Layout: mobile hamburger + drawer nav (backdrop button + sibling nav), desktop sidebar hidden on small screens, p-4 md:p-8 main padding - Products: card list view on mobile, flex-wrap filters - Lab results: card list view on mobile - ProductForm: responsive grids (grid-cols-1 sm:grid-cols-2), skin profile checkboxes 2→3 cols, active ingredient row restructured (name+✕ in flex row, percent/strength/irritation in 3-col grid), section headers stack on mobile - Skin snapshots: date+icons on one row, badges on separate row below - Product [id] header: back link stacked above title, redundant badge removed - Routines header: flex-col on mobile, sm:flex-row Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c85ca355df
commit
679e4e81f4
8 changed files with 193 additions and 60 deletions
|
|
@ -451,7 +451,7 @@
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{m["productForm_basicInfo"]()}</CardTitle></CardHeader>
|
<CardHeader><CardTitle>{m["productForm_basicInfo"]()}</CardTitle></CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="name">{m["productForm_name"]()}</Label>
|
<Label for="name">{m["productForm_name"]()}</Label>
|
||||||
<Input id="name" name="name" required placeholder={m["productForm_namePlaceholder"]()} bind:value={name} />
|
<Input id="name" name="name" required placeholder={m["productForm_namePlaceholder"]()} bind:value={name} />
|
||||||
|
|
@ -461,7 +461,7 @@
|
||||||
<Input id="brand" name="brand" required placeholder={m["productForm_brandPlaceholder"]()} bind:value={brand} />
|
<Input id="brand" name="brand" required placeholder={m["productForm_brandPlaceholder"]()} bind:value={brand} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="line_name">{m["productForm_lineName"]()}</Label>
|
<Label for="line_name">{m["productForm_lineName"]()}</Label>
|
||||||
<Input id="line_name" name="line_name" placeholder={m["productForm_lineNamePlaceholder"]()} bind:value={lineName} />
|
<Input id="line_name" name="line_name" placeholder={m["productForm_lineNamePlaceholder"]()} bind:value={lineName} />
|
||||||
|
|
@ -471,7 +471,7 @@
|
||||||
<Input id="url" name="url" type="url" placeholder="https://…" bind:value={url} />
|
<Input id="url" name="url" type="url" placeholder="https://…" bind:value={url} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="sku">{m["productForm_sku"]()}</Label>
|
<Label for="sku">{m["productForm_sku"]()}</Label>
|
||||||
<Input id="sku" name="sku" placeholder={m["productForm_skuPlaceholder"]()} bind:value={sku} />
|
<Input id="sku" name="sku" placeholder={m["productForm_skuPlaceholder"]()} bind:value={sku} />
|
||||||
|
|
@ -488,7 +488,7 @@
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{m["productForm_classification"]()}</CardTitle></CardHeader>
|
<CardHeader><CardTitle>{m["productForm_classification"]()}</CardTitle></CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="col-span-2 space-y-2">
|
<div class="col-span-2 space-y-2">
|
||||||
<Label>{m["productForm_category"]()}</Label>
|
<Label>{m["productForm_category"]()}</Label>
|
||||||
<input type="hidden" name="category" value={category} />
|
<input type="hidden" name="category" value={category} />
|
||||||
|
|
@ -564,7 +564,7 @@
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label>{m["productForm_recommendedFor"]()}</Label>
|
<Label>{m["productForm_recommendedFor"]()}</Label>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||||
{#each skinTypes as st}
|
{#each skinTypes as st}
|
||||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
<input
|
<input
|
||||||
|
|
@ -588,7 +588,7 @@
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label>{m["productForm_targetConcerns"]()}</Label>
|
<Label>{m["productForm_targetConcerns"]()}</Label>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||||
{#each skinConcerns as sc}
|
{#each skinConcerns as sc}
|
||||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
<input
|
<input
|
||||||
|
|
@ -641,7 +641,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<Label>{m["productForm_activeIngredients"]()}</Label>
|
<Label>{m["productForm_activeIngredients"]()}</Label>
|
||||||
<Button type="button" variant="outline" size="sm" onclick={addActive}>{m["productForm_addActive"]()}</Button>
|
<Button type="button" variant="outline" size="sm" onclick={addActive}>{m["productForm_addActive"]()}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -650,14 +650,23 @@
|
||||||
|
|
||||||
{#each actives as active, i}
|
{#each actives as active, i}
|
||||||
<div class="rounded-md border border-border p-3 space-y-3">
|
<div class="rounded-md border border-border p-3 space-y-3">
|
||||||
<div class="grid grid-cols-[1fr_100px_120px_120px_auto] gap-2 items-end">
|
<div class="flex items-end gap-2">
|
||||||
<div class="space-y-1">
|
<div class="min-w-0 flex-1 space-y-1">
|
||||||
<Label class="text-xs">{m["productForm_activeName"]()}</Label>
|
<Label class="text-xs">{m["productForm_activeName"]()}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g. Niacinamide"
|
placeholder="e.g. Niacinamide"
|
||||||
bind:value={active.name}
|
bind:value={active.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => removeActive(i)}
|
||||||
|
class="h-7 w-7 shrink-0 p-0 text-destructive hover:text-destructive"
|
||||||
|
>✕</Button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Label class="text-xs">{m["productForm_activePercent"]()}</Label>
|
<Label class="text-xs">{m["productForm_activePercent"]()}</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -687,18 +696,11 @@
|
||||||
<option value="3">{m["productForm_strengthHigh"]()}</option>
|
<option value="3">{m["productForm_strengthHigh"]()}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => removeActive(i)}
|
|
||||||
class="text-destructive hover:text-destructive"
|
|
||||||
>✕</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Label class="text-xs text-muted-foreground">{m["productForm_activeFunctions"]()}</Label>
|
<Label class="text-xs text-muted-foreground">{m["productForm_activeFunctions"]()}</Label>
|
||||||
<div class="grid grid-cols-4 gap-1">
|
<div class="grid grid-cols-2 gap-1 sm:grid-cols-4">
|
||||||
{#each ingFunctions as fn}
|
{#each ingFunctions as fn}
|
||||||
<label class="flex cursor-pointer items-center gap-1.5 text-xs">
|
<label class="flex cursor-pointer items-center gap-1.5 text-xs">
|
||||||
<input
|
<input
|
||||||
|
|
@ -764,7 +766,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<Label>{m["productForm_incompatibleWith"]()}</Label>
|
<Label>{m["productForm_incompatibleWith"]()}</Label>
|
||||||
<Button type="button" variant="outline" size="sm" onclick={addIncompatible}>
|
<Button type="button" variant="outline" size="sm" onclick={addIncompatible}>
|
||||||
{m["productForm_addIncompatibility"]()}
|
{m["productForm_addIncompatibility"]()}
|
||||||
|
|
@ -774,7 +776,7 @@
|
||||||
<input type="hidden" name="incompatible_with_json" value={incompatibleJson} />
|
<input type="hidden" name="incompatible_with_json" value={incompatibleJson} />
|
||||||
|
|
||||||
{#each incompatibleWith as row, i}
|
{#each incompatibleWith as row, i}
|
||||||
<div class="grid grid-cols-[1fr_140px_1fr_auto] gap-2 items-end">
|
<div class="grid grid-cols-2 gap-2 items-end sm:grid-cols-[1fr_140px_1fr_auto]">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Label class="text-xs">{m["productForm_incompTarget"]()}</Label>
|
<Label class="text-xs">{m["productForm_incompTarget"]()}</Label>
|
||||||
<Input placeholder="e.g. Vitamin C" bind:value={row.target} />
|
<Input placeholder="e.g. Vitamin C" bind:value={row.target} />
|
||||||
|
|
@ -813,7 +815,7 @@
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{m["productForm_contextRules"]()}</CardTitle></CardHeader>
|
<CardHeader><CardTitle>{m["productForm_contextRules"]()}</CardTitle></CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label>{m["productForm_ctxAfterShaving"]()}</Label>
|
<Label>{m["productForm_ctxAfterShaving"]()}</Label>
|
||||||
<input type="hidden" name="ctx_safe_after_shaving" value={ctxAfterShaving} />
|
<input type="hidden" name="ctx_safe_after_shaving" value={ctxAfterShaving} />
|
||||||
|
|
@ -884,7 +886,7 @@
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{m["productForm_productDetails"]()}</CardTitle></CardHeader>
|
<CardHeader><CardTitle>{m["productForm_productDetails"]()}</CardTitle></CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label>{m["productForm_priceTier"]()}</Label>
|
<Label>{m["productForm_priceTier"]()}</Label>
|
||||||
<input type="hidden" name="price_tier" value={priceTier} />
|
<input type="hidden" name="price_tier" value={priceTier} />
|
||||||
|
|
@ -919,7 +921,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="ph_min">{m["productForm_phMin"]()}</Label>
|
<Label for="ph_min">{m["productForm_phMin"]()}</Label>
|
||||||
<Input id="ph_min" name="ph_min" type="number" min="0" max="14" step="0.1" placeholder="e.g. 3.5" bind:value={phMin} />
|
<Input id="ph_min" name="ph_min" type="number" min="0" max="14" step="0.1" placeholder="e.g. 3.5" bind:value={phMin} />
|
||||||
|
|
@ -948,7 +950,7 @@
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{m["productForm_safetyFlags"]()}</CardTitle></CardHeader>
|
<CardHeader><CardTitle>{m["productForm_safetyFlags"]()}</CardTitle></CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label>{m["productForm_fragranceFree"]()}</Label>
|
<Label>{m["productForm_fragranceFree"]()}</Label>
|
||||||
<input type="hidden" name="fragrance_free" value={fragranceFree} />
|
<input type="hidden" name="fragrance_free" value={fragranceFree} />
|
||||||
|
|
@ -1008,7 +1010,7 @@
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{m["productForm_usageConstraints"]()}</CardTitle></CardHeader>
|
<CardHeader><CardTitle>{m["productForm_usageConstraints"]()}</CardTitle></CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="min_interval_hours">{m["productForm_minIntervalHours"]()}</Label>
|
<Label for="min_interval_hours">{m["productForm_minIntervalHours"]()}</Label>
|
||||||
<Input id="min_interval_hours" name="min_interval_hours" type="number" min="0" placeholder="e.g. 24" bind:value={minIntervalHours} />
|
<Input id="min_interval_hours" name="min_interval_hours" type="number" min="0" placeholder="e.g. 24" bind:value={minIntervalHours} />
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
let mobileMenuOpen = $state(false);
|
||||||
|
|
||||||
const navItems = $derived([
|
const navItems = $derived([
|
||||||
{ href: '/', label: m.nav_dashboard(), icon: '🏠' },
|
{ href: '/', label: m.nav_dashboard(), icon: '🏠' },
|
||||||
{ href: '/products', label: m.nav_products(), icon: '🧴' },
|
{ href: '/products', label: m.nav_products(), icon: '🧴' },
|
||||||
|
|
@ -28,9 +30,68 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex min-h-screen bg-background">
|
<div class="flex min-h-screen flex-col bg-background md:flex-row">
|
||||||
<!-- Sidebar -->
|
<!-- Mobile header -->
|
||||||
<nav class="w-56 shrink-0 border-r border-border bg-card px-3 py-6">
|
<header class="flex items-center justify-between border-b border-border bg-card px-4 py-3 md:hidden">
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-semibold tracking-tight">{m["nav_appName"]()}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
|
||||||
|
class="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
{#if mobileMenuOpen}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
|
{:else}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Mobile drawer overlay -->
|
||||||
|
{#if mobileMenuOpen}
|
||||||
|
<!-- Backdrop: closes drawer on click -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="fixed inset-0 z-50 bg-black/50 md:hidden"
|
||||||
|
onclick={() => (mobileMenuOpen = false)}
|
||||||
|
aria-label={m.common_cancel()}
|
||||||
|
></button>
|
||||||
|
<!-- Drawer (same z-50 but later in DOM → on top of backdrop) -->
|
||||||
|
<nav
|
||||||
|
class="fixed inset-y-0 left-0 z-50 w-64 overflow-y-auto bg-card px-3 py-6 md:hidden"
|
||||||
|
>
|
||||||
|
<div class="mb-8 px-3">
|
||||||
|
<h1 class="text-lg font-semibold tracking-tight">{m["nav_appName"]()}</h1>
|
||||||
|
<p class="text-xs text-muted-foreground">{m["nav_appSubtitle"]()}</p>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
{#each navItems as item}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
onclick={() => (mobileMenuOpen = false)}
|
||||||
|
class="flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors
|
||||||
|
{isActive(item.href)
|
||||||
|
? 'bg-accent text-accent-foreground font-medium'
|
||||||
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'}"
|
||||||
|
>
|
||||||
|
<span class="text-base">{item.icon}</span>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<div class="mt-6 px-3">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Desktop Sidebar -->
|
||||||
|
<nav class="hidden w-56 shrink-0 flex-col border-r border-border bg-card px-3 py-6 md:flex">
|
||||||
<div class="mb-8 px-3">
|
<div class="mb-8 px-3">
|
||||||
<h1 class="text-lg font-semibold tracking-tight">{m["nav_appName"]()}</h1>
|
<h1 class="text-lg font-semibold tracking-tight">{m["nav_appName"]()}</h1>
|
||||||
<p class="text-xs text-muted-foreground">{m["nav_appSubtitle"]()}</p>
|
<p class="text-xs text-muted-foreground">{m["nav_appSubtitle"]()}</p>
|
||||||
|
|
@ -57,7 +118,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="flex-1 overflow-auto p-8">
|
<main class="flex-1 overflow-auto p-4 md:p-8">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{m["labResults_newTitle"]()}</CardTitle></CardHeader>
|
<CardHeader><CardTitle>{m["labResults_newTitle"]()}</CardTitle></CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form method="POST" action="?/create" use:enhance class="grid grid-cols-2 gap-4">
|
<form method="POST" action="?/create" use:enhance class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Label for="collected_at">{m["labResults_date"]()}</Label>
|
<Label for="collected_at">{m["labResults_date"]()}</Label>
|
||||||
<Input id="collected_at" name="collected_at" type="date" required />
|
<Input id="collected_at" name="collected_at" type="date" required />
|
||||||
|
|
@ -125,7 +125,8 @@
|
||||||
</Card>
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="rounded-md border border-border">
|
<!-- Desktop: table -->
|
||||||
|
<div class="hidden rounded-md border border-border md:block">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
@ -173,4 +174,34 @@
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile: cards -->
|
||||||
|
<div class="flex flex-col gap-3 md:hidden">
|
||||||
|
{#each data.results as r (r.record_id)}
|
||||||
|
<div class="rounded-lg border border-border p-4 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] ?? ''}">
|
||||||
|
{r.flag}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">{r.collected_at.slice(0, 10)}</p>
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<span class="font-mono text-xs text-muted-foreground">{r.test_code}</span>
|
||||||
|
{#if r.value_num != null}
|
||||||
|
<span>{r.value_num} {r.unit_original ?? ''}</span>
|
||||||
|
{:else if r.value_text}
|
||||||
|
<span>{r.value_text}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if r.lab}
|
||||||
|
<p class="text-xs text-muted-foreground">{r.lab}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="py-8 text-center text-sm text-muted-foreground">{m["labResults_noResults"]()}</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{m["medications_newTitle"]()}</CardTitle></CardHeader>
|
<CardHeader><CardTitle>{m["medications_newTitle"]()}</CardTitle></CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form method="POST" action="?/create" use:enhance class="grid grid-cols-2 gap-4">
|
<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">
|
<div class="space-y-1 col-span-2">
|
||||||
<Label>{m.medications_kind()}</Label>
|
<Label>{m.medications_kind()}</Label>
|
||||||
<input type="hidden" name="kind" value={kind} />
|
<input type="hidden" name="kind" value={kind} />
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
<Button href="/products/new">{m["products_addNew"]()}</Button>
|
<Button href="/products/new">{m["products_addNew"]()}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
{#each (['all', 'owned', 'unowned'] as OwnershipFilter[]) as f (f)}
|
{#each (['all', 'owned', 'unowned'] as OwnershipFilter[]) as f (f)}
|
||||||
<Button
|
<Button
|
||||||
variant={ownershipFilter === f ? 'default' : 'outline'}
|
variant={ownershipFilter === f ? 'default' : 'outline'}
|
||||||
|
|
@ -78,7 +78,8 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-md border border-border">
|
<!-- Desktop: table -->
|
||||||
|
<div class="hidden rounded-md border border-border md:block">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
@ -128,4 +129,41 @@
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile: cards -->
|
||||||
|
<div class="flex flex-col gap-3 md:hidden">
|
||||||
|
{#if totalCount === 0}
|
||||||
|
<p class="py-8 text-center text-sm text-muted-foreground">{m["products_noProducts"]()}</p>
|
||||||
|
{:else}
|
||||||
|
{#each groupedProducts as [category, products] (category)}
|
||||||
|
<div class="border-b border-border pb-1 pt-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
|
{category.replace(/_/g, ' ')}
|
||||||
|
</div>
|
||||||
|
{#each products as product (product.id)}
|
||||||
|
<a
|
||||||
|
href="/products/{product.id}"
|
||||||
|
class="block rounded-lg border border-border p-4 hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">{product.name}</p>
|
||||||
|
<p class="text-sm text-muted-foreground">{product.brand}</p>
|
||||||
|
</div>
|
||||||
|
<span class="shrink-0 text-xs uppercase text-muted-foreground">{product.recommended_time}</span>
|
||||||
|
</div>
|
||||||
|
{#if product.targets.length}
|
||||||
|
<div class="mt-2 flex flex-wrap gap-1">
|
||||||
|
{#each product.targets.slice(0, 3) as t (t)}
|
||||||
|
<Badge variant="secondary" class="text-xs">{t.replace(/_/g, ' ')}</Badge>
|
||||||
|
{/each}
|
||||||
|
{#if product.targets.length > 3}
|
||||||
|
<span class="text-xs text-muted-foreground">+{product.targets.length - 3}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,9 @@
|
||||||
<svelte:head><title>{product.name} — innercontext</title></svelte:head>
|
<svelte:head><title>{product.name} — innercontext</title></svelte:head>
|
||||||
|
|
||||||
<div class="max-w-2xl space-y-6">
|
<div class="max-w-2xl space-y-6">
|
||||||
<div class="flex items-center gap-4">
|
<div>
|
||||||
<a href="/products" class="text-sm text-muted-foreground hover:underline">{m["products_backToList"]()}</a>
|
<a href="/products" class="text-sm text-muted-foreground hover:underline">{m["products_backToList"]()}</a>
|
||||||
<h2 class="text-2xl font-bold tracking-tight">{product.name}</h2>
|
<h2 class="mt-1 text-2xl font-bold tracking-tight">{product.name}</h2>
|
||||||
<Badge variant="outline">{product.category.replace(/_/g, ' ')}</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if form?.error}
|
{#if form?.error}
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,12 @@
|
||||||
<svelte:head><title>{m.routines_title()} — innercontext</title></svelte:head>
|
<svelte:head><title>{m.routines_title()} — innercontext</title></svelte:head>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold tracking-tight">{m.routines_title()}</h2>
|
<h2 class="text-2xl font-bold tracking-tight">{m.routines_title()}</h2>
|
||||||
<p class="text-muted-foreground">{m.routines_count({ count: data.routines.length })}</p>
|
<p class="text-muted-foreground">{m.routines_count({ count: data.routines.length })}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<Button href="/routines/suggest" variant="outline">{m["routines_suggestAI"]()}</Button>
|
<Button href="/routines/suggest" variant="outline">{m["routines_suggestAI"]()}</Button>
|
||||||
<Button href="/routines/new">{m["routines_addNew"]()}</Button>
|
<Button href="/routines/new">{m["routines_addNew"]()}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{m["skin_newSnapshotTitle"]()}</CardTitle></CardHeader>
|
<CardHeader><CardTitle>{m["skin_newSnapshotTitle"]()}</CardTitle></CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form method="POST" action="?/create" use:enhance class="grid grid-cols-2 gap-4">
|
<form method="POST" action="?/create" use:enhance class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Label for="snapshot_date">{m.skin_date()}</Label>
|
<Label for="snapshot_date">{m.skin_date()}</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -332,7 +332,7 @@
|
||||||
await update();
|
await update();
|
||||||
if (result.type === 'success') editingId = null;
|
if (result.type === 'success') editingId = null;
|
||||||
}}
|
}}
|
||||||
class="grid grid-cols-2 gap-4"
|
class="grid grid-cols-1 sm:grid-cols-2 gap-4"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={snap.id} />
|
<input type="hidden" name="id" value={snap.id} />
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
|
|
@ -422,9 +422,19 @@
|
||||||
</form>
|
</form>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Read view -->
|
<!-- Read view -->
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="mb-3 space-y-1.5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-medium">{snap.snapshot_date}</span>
|
<span class="font-medium">{snap.snapshot_date}</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-1">
|
||||||
|
<Button variant="ghost" size="sm" onclick={() => startEdit(snap)} class="h-7 w-7 shrink-0 p-0 text-muted-foreground hover:text-foreground" aria-label={m.common_edit()}>✎</Button>
|
||||||
|
<form method="POST" action="?/delete" use:enhance>
|
||||||
|
<input type="hidden" name="id" value={snap.id} />
|
||||||
|
<Button type="submit" variant="ghost" size="sm" class="h-7 w-7 shrink-0 p-0 text-destructive hover:text-destructive" aria-label={m.common_delete()}>×</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if snap.overall_state || snap.texture}
|
||||||
|
<div class="flex flex-wrap items-center gap-1.5">
|
||||||
{#if snap.overall_state}
|
{#if snap.overall_state}
|
||||||
<span class="rounded-full px-2 py-0.5 text-xs font-medium {stateColors[snap.overall_state] ?? ''}">
|
<span class="rounded-full px-2 py-0.5 text-xs font-medium {stateColors[snap.overall_state] ?? ''}">
|
||||||
{stateLabels[snap.overall_state]?.() ?? snap.overall_state}
|
{stateLabels[snap.overall_state]?.() ?? snap.overall_state}
|
||||||
|
|
@ -433,16 +443,8 @@
|
||||||
{#if snap.texture}
|
{#if snap.texture}
|
||||||
<Badge variant="secondary">{textureLabels[snap.texture]?.() ?? snap.texture}</Badge>
|
<Badge variant="secondary">{textureLabels[snap.texture]?.() ?? snap.texture}</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
<Button variant="ghost" size="sm" onclick={() => startEdit(snap)} class="h-7 px-2 text-xs">
|
|
||||||
{m.common_edit()}
|
|
||||||
</Button>
|
|
||||||
<form method="POST" action="?/delete" use:enhance>
|
|
||||||
<input type="hidden" name="id" value={snap.id} />
|
|
||||||
<Button type="submit" variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive hover:text-destructive">
|
|
||||||
{m.common_delete()}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-3 text-sm mb-3">
|
<div class="grid grid-cols-3 gap-3 text-sm mb-3">
|
||||||
{#if snap.hydration_level != null}
|
{#if snap.hydration_level != null}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue