- 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>
496 lines
20 KiB
Svelte
496 lines
20 KiB
Svelte
<script lang="ts">
|
||
import { enhance } from '$app/forms';
|
||
import type { ActionData, PageData } from './$types';
|
||
import { m } from '$lib/paraglide/messages.js';
|
||
import { analyzeSkinPhotos } from '$lib/api';
|
||
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';
|
||
|
||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||
|
||
const states = ['excellent', 'good', 'fair', 'poor'];
|
||
const skinTextures = ['smooth', 'rough', 'flaky', 'bumpy'];
|
||
const barrierStates = ['intact', 'mildly_compromised', 'compromised'];
|
||
const skinTypes = ['dry', 'oily', 'combination', 'sensitive', 'normal', 'acne_prone'];
|
||
|
||
const stateColors: Record<string, string> = {
|
||
excellent: 'bg-green-100 text-green-800',
|
||
good: 'bg-blue-100 text-blue-800',
|
||
fair: 'bg-yellow-100 text-yellow-800',
|
||
poor: 'bg-red-100 text-red-800'
|
||
};
|
||
|
||
const stateLabels: Record<string, () => string> = {
|
||
excellent: m["skin_stateExcellent"],
|
||
good: m["skin_stateGood"],
|
||
fair: m["skin_stateFair"],
|
||
poor: m["skin_statePoor"]
|
||
};
|
||
|
||
const textureLabels: Record<string, () => string> = {
|
||
smooth: m["skin_textureSmooth"],
|
||
rough: m["skin_textureRough"],
|
||
flaky: m["skin_textureFlaky"],
|
||
bumpy: m["skin_textureBumpy"]
|
||
};
|
||
|
||
const barrierLabels: Record<string, () => string> = {
|
||
intact: m["skin_barrierIntact"],
|
||
mildly_compromised: m["skin_barrierMildly"],
|
||
compromised: m["skin_barrierCompromised"]
|
||
};
|
||
|
||
const skinTypeLabels: Record<string, () => string> = {
|
||
dry: m["skin_typeDry"],
|
||
oily: m["skin_typeOily"],
|
||
combination: m["skin_typeCombination"],
|
||
sensitive: m["skin_typeSensitive"],
|
||
normal: m["skin_typeNormal"],
|
||
acne_prone: m["skin_typeAcneProne"]
|
||
};
|
||
|
||
let showForm = $state(false);
|
||
|
||
// Create form state (bound to inputs so AI can pre-fill)
|
||
let snapshotDate = $state(new Date().toISOString().slice(0, 10));
|
||
let overallState = $state('');
|
||
let texture = $state('');
|
||
let barrierState = $state('');
|
||
let skinType = $state('');
|
||
let hydrationLevel = $state('');
|
||
let sensitivityLevel = $state('');
|
||
let sebumTzone = $state('');
|
||
let sebumCheeks = $state('');
|
||
let activeConcernsRaw = $state('');
|
||
let prioritiesRaw = $state('');
|
||
let notes = $state('');
|
||
|
||
// Edit state
|
||
let editingId = $state<string | null>(null);
|
||
let editSnapshotDate = $state('');
|
||
let editOverallState = $state('');
|
||
let editTexture = $state('');
|
||
let editBarrierState = $state('');
|
||
let editSkinType = $state('');
|
||
let editHydrationLevel = $state('');
|
||
let editSensitivityLevel = $state('');
|
||
let editSebumTzone = $state('');
|
||
let editSebumCheeks = $state('');
|
||
let editActiveConcernsRaw = $state('');
|
||
let editPrioritiesRaw = $state('');
|
||
let editNotes = $state('');
|
||
|
||
function startEdit(snap: (typeof data.snapshots)[number]) {
|
||
editingId = snap.id;
|
||
editSnapshotDate = snap.snapshot_date;
|
||
editOverallState = snap.overall_state ?? '';
|
||
editTexture = snap.texture ?? '';
|
||
editBarrierState = snap.barrier_state ?? '';
|
||
editSkinType = snap.skin_type ?? '';
|
||
editHydrationLevel = snap.hydration_level != null ? String(snap.hydration_level) : '';
|
||
editSensitivityLevel = snap.sensitivity_level != null ? String(snap.sensitivity_level) : '';
|
||
editSebumTzone = snap.sebum_tzone != null ? String(snap.sebum_tzone) : '';
|
||
editSebumCheeks = snap.sebum_cheeks != null ? String(snap.sebum_cheeks) : '';
|
||
editActiveConcernsRaw = snap.active_concerns?.join(', ') ?? '';
|
||
editPrioritiesRaw = snap.priorities?.join(', ') ?? '';
|
||
editNotes = snap.notes ?? '';
|
||
showForm = false;
|
||
}
|
||
|
||
// AI photo analysis state
|
||
let aiPanelOpen = $state(false);
|
||
let selectedFiles = $state<File[]>([]);
|
||
let previewUrls = $state<string[]>([]);
|
||
let aiLoading = $state(false);
|
||
let aiError = $state('');
|
||
|
||
const sortedSnapshots = $derived(
|
||
[...data.snapshots].sort((a, b) => b.snapshot_date.localeCompare(a.snapshot_date))
|
||
);
|
||
|
||
function handleFileSelect(e: Event) {
|
||
const input = e.target as HTMLInputElement;
|
||
const files = Array.from(input.files ?? []).slice(0, 3);
|
||
selectedFiles = files;
|
||
previewUrls.forEach(URL.revokeObjectURL);
|
||
previewUrls = files.map((f) => URL.createObjectURL(f));
|
||
}
|
||
|
||
async function analyzePhotos() {
|
||
if (!selectedFiles.length) return;
|
||
aiLoading = true;
|
||
aiError = '';
|
||
try {
|
||
const r = await analyzeSkinPhotos(selectedFiles);
|
||
if (r.overall_state) overallState = r.overall_state;
|
||
if (r.texture) texture = r.texture;
|
||
if (r.skin_type) skinType = r.skin_type;
|
||
if (r.barrier_state) barrierState = r.barrier_state;
|
||
if (r.hydration_level != null) hydrationLevel = String(r.hydration_level);
|
||
if (r.sensitivity_level != null) sensitivityLevel = String(r.sensitivity_level);
|
||
if (r.sebum_tzone != null) sebumTzone = String(r.sebum_tzone);
|
||
if (r.sebum_cheeks != null) sebumCheeks = String(r.sebum_cheeks);
|
||
if (r.active_concerns?.length) activeConcernsRaw = r.active_concerns.join(', ');
|
||
if (r.priorities?.length) prioritiesRaw = r.priorities.join(', ');
|
||
if (r.notes) notes = r.notes;
|
||
aiPanelOpen = false;
|
||
} catch (e) {
|
||
aiError = (e as Error).message;
|
||
} finally {
|
||
aiLoading = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<svelte:head><title>{m.skin_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.skin_title()}</h2>
|
||
<p class="text-muted-foreground">{m.skin_count({ count: data.snapshots.length })}</p>
|
||
</div>
|
||
<Button variant="outline" onclick={() => (showForm = !showForm)}>
|
||
{showForm ? m.common_cancel() : m["skin_addNew"]()}
|
||
</Button>
|
||
</div>
|
||
|
||
{#if form?.error}
|
||
<div class="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">{form.error}</div>
|
||
{/if}
|
||
{#if form?.created}
|
||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["skin_snapshotAdded"]()}</div>
|
||
{/if}
|
||
{#if form?.updated}
|
||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["skin_snapshotUpdated"]()}</div>
|
||
{/if}
|
||
{#if form?.deleted}
|
||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["skin_snapshotDeleted"]()}</div>
|
||
{/if}
|
||
|
||
{#if showForm}
|
||
<!-- AI photo analysis card -->
|
||
<Card>
|
||
<CardHeader>
|
||
<button
|
||
type="button"
|
||
class="flex w-full items-center justify-between text-left"
|
||
onclick={() => (aiPanelOpen = !aiPanelOpen)}
|
||
>
|
||
<CardTitle>{m["skin_aiAnalysisTitle"]()}</CardTitle>
|
||
<span class="text-sm text-muted-foreground">{aiPanelOpen ? '▲' : '▼'}</span>
|
||
</button>
|
||
</CardHeader>
|
||
{#if aiPanelOpen}
|
||
<CardContent class="space-y-3">
|
||
<p class="text-sm text-muted-foreground">
|
||
{m["skin_aiUploadText"]()}
|
||
</p>
|
||
<input
|
||
type="file"
|
||
accept="image/heic,image/heif,image/jpeg,image/png,image/webp"
|
||
multiple
|
||
onchange={handleFileSelect}
|
||
class="block w-full text-sm text-muted-foreground
|
||
file:mr-4 file:rounded-md file:border-0 file:bg-primary
|
||
file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-primary-foreground"
|
||
/>
|
||
{#if previewUrls.length}
|
||
<div class="flex flex-wrap gap-2">
|
||
{#each previewUrls as url (url)}
|
||
<img src={url} alt="skin preview" class="h-24 w-24 rounded-md object-cover border" />
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
{#if aiError}
|
||
<p class="text-sm text-destructive">{aiError}</p>
|
||
{/if}
|
||
<Button
|
||
type="button"
|
||
onclick={analyzePhotos}
|
||
disabled={aiLoading || !selectedFiles.length}
|
||
>
|
||
{aiLoading ? m.skin_analyzing() : m["skin_analyzePhotos"]()}
|
||
</Button>
|
||
</CardContent>
|
||
{/if}
|
||
</Card>
|
||
|
||
<!-- New snapshot form -->
|
||
<Card>
|
||
<CardHeader><CardTitle>{m["skin_newSnapshotTitle"]()}</CardTitle></CardHeader>
|
||
<CardContent>
|
||
<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="snapshot_date">{m.skin_date()}</Label>
|
||
<Input
|
||
id="snapshot_date"
|
||
name="snapshot_date"
|
||
type="date"
|
||
bind:value={snapshotDate}
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label>{m["skin_overallState"]()}</Label>
|
||
<input type="hidden" name="overall_state" value={overallState} />
|
||
<Select type="single" value={overallState} onValueChange={(v) => (overallState = v)}>
|
||
<SelectTrigger>{overallState ? stateLabels[overallState]?.() ?? overallState : m.common_select()}</SelectTrigger>
|
||
<SelectContent>
|
||
{#each states as s (s)}
|
||
<SelectItem value={s}>{stateLabels[s]?.() ?? s}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label>{m.skin_texture()}</Label>
|
||
<input type="hidden" name="texture" value={texture} />
|
||
<Select type="single" value={texture} onValueChange={(v) => (texture = v)}>
|
||
<SelectTrigger>{texture ? textureLabels[texture]?.() ?? texture : m.common_select()}</SelectTrigger>
|
||
<SelectContent>
|
||
{#each skinTextures as t (t)}
|
||
<SelectItem value={t}>{textureLabels[t]?.() ?? t}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label>{m["skin_skinType"]()}</Label>
|
||
<input type="hidden" name="skin_type" value={skinType} />
|
||
<Select type="single" value={skinType} onValueChange={(v) => (skinType = v)}>
|
||
<SelectTrigger>{skinType ? skinTypeLabels[skinType]?.() ?? skinType : m.common_select()}</SelectTrigger>
|
||
<SelectContent>
|
||
{#each skinTypes as st (st)}
|
||
<SelectItem value={st}>{skinTypeLabels[st]?.() ?? st}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label>{m["skin_barrierState"]()}</Label>
|
||
<input type="hidden" name="barrier_state" value={barrierState} />
|
||
<Select type="single" value={barrierState} onValueChange={(v) => (barrierState = v)}>
|
||
<SelectTrigger>{barrierState ? barrierLabels[barrierState]?.() ?? barrierState : m.common_select()}</SelectTrigger>
|
||
<SelectContent>
|
||
{#each barrierStates as b (b)}
|
||
<SelectItem value={b}>{barrierLabels[b]?.() ?? b}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label for="hydration_level">{m.skin_hydration()}</Label>
|
||
<Input id="hydration_level" name="hydration_level" type="number" min="1" max="5" bind:value={hydrationLevel} />
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label for="sensitivity_level">{m.skin_sensitivity()}</Label>
|
||
<Input id="sensitivity_level" name="sensitivity_level" type="number" min="1" max="5" bind:value={sensitivityLevel} />
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label for="sebum_tzone">{m["skin_sebumTzone"]()}</Label>
|
||
<Input id="sebum_tzone" name="sebum_tzone" type="number" min="1" max="5" bind:value={sebumTzone} />
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label for="sebum_cheeks">{m["skin_sebumCheeks"]()}</Label>
|
||
<Input id="sebum_cheeks" name="sebum_cheeks" type="number" min="1" max="5" bind:value={sebumCheeks} />
|
||
</div>
|
||
<div class="space-y-1 col-span-2">
|
||
<Label for="active_concerns">{m["skin_activeConcerns"]()}</Label>
|
||
<Input id="active_concerns" name="active_concerns" placeholder={m["skin_activeConcernsPlaceholder"]()} bind:value={activeConcernsRaw} />
|
||
</div>
|
||
<div class="space-y-1 col-span-2">
|
||
<Label for="priorities">{m["skin_priorities"]()}</Label>
|
||
<Input id="priorities" name="priorities" placeholder={m["skin_prioritiesPlaceholder"]()} bind:value={prioritiesRaw} />
|
||
</div>
|
||
<div class="space-y-1 col-span-2">
|
||
<Label for="notes">{m.skin_notes()}</Label>
|
||
<Input id="notes" name="notes" bind:value={notes} />
|
||
</div>
|
||
<div class="col-span-2">
|
||
<Button type="submit">{m["skin_addSnapshot"]()}</Button>
|
||
</div>
|
||
</form>
|
||
</CardContent>
|
||
</Card>
|
||
{/if}
|
||
|
||
<div class="space-y-4">
|
||
{#each sortedSnapshots as snap (snap.id)}
|
||
<Card>
|
||
<CardContent class="pt-4">
|
||
{#if editingId === snap.id}
|
||
<!-- Inline edit form -->
|
||
<form
|
||
method="POST"
|
||
action="?/update"
|
||
use:enhance={() => async ({ result, update }) => {
|
||
await update();
|
||
if (result.type === 'success') editingId = null;
|
||
}}
|
||
class="grid grid-cols-1 sm:grid-cols-2 gap-4"
|
||
>
|
||
<input type="hidden" name="id" value={snap.id} />
|
||
<div class="space-y-1">
|
||
<Label for="edit_snapshot_date">{m.skin_date()}</Label>
|
||
<Input id="edit_snapshot_date" name="snapshot_date" type="date" bind:value={editSnapshotDate} required />
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label>{m["skin_overallState"]()}</Label>
|
||
<input type="hidden" name="overall_state" value={editOverallState} />
|
||
<Select type="single" value={editOverallState} onValueChange={(v) => (editOverallState = v)}>
|
||
<SelectTrigger>{editOverallState ? stateLabels[editOverallState]?.() ?? editOverallState : m.common_select()}</SelectTrigger>
|
||
<SelectContent>
|
||
{#each states as s (s)}
|
||
<SelectItem value={s}>{stateLabels[s]?.() ?? s}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label>{m.skin_texture()}</Label>
|
||
<input type="hidden" name="texture" value={editTexture} />
|
||
<Select type="single" value={editTexture} onValueChange={(v) => (editTexture = v)}>
|
||
<SelectTrigger>{editTexture ? textureLabels[editTexture]?.() ?? editTexture : m.common_select()}</SelectTrigger>
|
||
<SelectContent>
|
||
{#each skinTextures as t (t)}
|
||
<SelectItem value={t}>{textureLabels[t]?.() ?? t}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label>{m["skin_skinType"]()}</Label>
|
||
<input type="hidden" name="skin_type" value={editSkinType} />
|
||
<Select type="single" value={editSkinType} onValueChange={(v) => (editSkinType = v)}>
|
||
<SelectTrigger>{editSkinType ? skinTypeLabels[editSkinType]?.() ?? editSkinType : m.common_select()}</SelectTrigger>
|
||
<SelectContent>
|
||
{#each skinTypes as st (st)}
|
||
<SelectItem value={st}>{skinTypeLabels[st]?.() ?? st}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label>{m["skin_barrierState"]()}</Label>
|
||
<input type="hidden" name="barrier_state" value={editBarrierState} />
|
||
<Select type="single" value={editBarrierState} onValueChange={(v) => (editBarrierState = v)}>
|
||
<SelectTrigger>{editBarrierState ? barrierLabels[editBarrierState]?.() ?? editBarrierState : m.common_select()}</SelectTrigger>
|
||
<SelectContent>
|
||
{#each barrierStates as b (b)}
|
||
<SelectItem value={b}>{barrierLabels[b]?.() ?? b}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label for="edit_hydration_level">{m.skin_hydration()}</Label>
|
||
<Input id="edit_hydration_level" name="hydration_level" type="number" min="1" max="5" bind:value={editHydrationLevel} />
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label for="edit_sensitivity_level">{m.skin_sensitivity()}</Label>
|
||
<Input id="edit_sensitivity_level" name="sensitivity_level" type="number" min="1" max="5" bind:value={editSensitivityLevel} />
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label for="edit_sebum_tzone">{m["skin_sebumTzone"]()}</Label>
|
||
<Input id="edit_sebum_tzone" name="sebum_tzone" type="number" min="1" max="5" bind:value={editSebumTzone} />
|
||
</div>
|
||
<div class="space-y-1">
|
||
<Label for="edit_sebum_cheeks">{m["skin_sebumCheeks"]()}</Label>
|
||
<Input id="edit_sebum_cheeks" name="sebum_cheeks" type="number" min="1" max="5" bind:value={editSebumCheeks} />
|
||
</div>
|
||
<div class="space-y-1 col-span-2">
|
||
<Label for="edit_active_concerns">{m["skin_activeConcerns"]()}</Label>
|
||
<Input id="edit_active_concerns" name="active_concerns" placeholder={m["skin_activeConcernsPlaceholder"]()} bind:value={editActiveConcernsRaw} />
|
||
</div>
|
||
<div class="space-y-1 col-span-2">
|
||
<Label for="edit_priorities">{m["skin_priorities"]()}</Label>
|
||
<Input id="edit_priorities" name="priorities" placeholder={m["skin_prioritiesPlaceholder"]()} bind:value={editPrioritiesRaw} />
|
||
</div>
|
||
<div class="space-y-1 col-span-2">
|
||
<Label for="edit_notes">{m.skin_notes()}</Label>
|
||
<Input id="edit_notes" name="notes" bind:value={editNotes} />
|
||
</div>
|
||
<div class="col-span-2 flex gap-2">
|
||
<Button type="submit">{m.common_save()}</Button>
|
||
<Button type="button" variant="outline" onclick={() => (editingId = null)}>{m.common_cancel()}</Button>
|
||
</div>
|
||
</form>
|
||
{:else}
|
||
<!-- Read view -->
|
||
<div class="mb-3 space-y-1.5">
|
||
<div class="flex items-center justify-between">
|
||
<span class="font-medium">{snap.snapshot_date}</span>
|
||
<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}
|
||
<span class="rounded-full px-2 py-0.5 text-xs font-medium {stateColors[snap.overall_state] ?? ''}">
|
||
{stateLabels[snap.overall_state]?.() ?? snap.overall_state}
|
||
</span>
|
||
{/if}
|
||
{#if snap.texture}
|
||
<Badge variant="secondary">{textureLabels[snap.texture]?.() ?? snap.texture}</Badge>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
<div class="grid grid-cols-3 gap-3 text-sm mb-3">
|
||
{#if snap.hydration_level != null}
|
||
<div>
|
||
<p class="text-xs text-muted-foreground">{m["skin_hydrationLabel"]()}</p>
|
||
<p class="font-medium">{snap.hydration_level}/5</p>
|
||
</div>
|
||
{/if}
|
||
{#if snap.sensitivity_level != null}
|
||
<div>
|
||
<p class="text-xs text-muted-foreground">{m["skin_sensitivityLabel"]()}</p>
|
||
<p class="font-medium">{snap.sensitivity_level}/5</p>
|
||
</div>
|
||
{/if}
|
||
{#if snap.barrier_state}
|
||
<div>
|
||
<p class="text-xs text-muted-foreground">{m["skin_barrierLabel"]()}</p>
|
||
<p class="font-medium">{barrierLabels[snap.barrier_state]?.() ?? snap.barrier_state.replace(/_/g, ' ')}</p>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{#if snap.active_concerns.length}
|
||
<div class="flex flex-wrap gap-1">
|
||
{#each snap.active_concerns as c (c)}
|
||
<Badge variant="secondary" class="text-xs">{c.replace(/_/g, ' ')}</Badge>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
{#if snap.priorities?.length}
|
||
<div class="mt-2">
|
||
<p class="text-xs text-muted-foreground mb-1">{m["skin_prioritiesLabel"]()}</p>
|
||
<div class="flex flex-wrap gap-1">
|
||
{#each snap.priorities as p (p)}
|
||
<Badge variant="outline" class="text-xs">{p}</Badge>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
{#if snap.notes}
|
||
<p class="mt-2 text-sm text-muted-foreground">{snap.notes}</p>
|
||
{/if}
|
||
{/if}
|
||
</CardContent>
|
||
</Card>
|
||
{:else}
|
||
<p class="text-sm text-muted-foreground">{m["skin_noSnapshots"]()}</p>
|
||
{/each}
|
||
</div>
|
||
</div>
|