feat(frontend): add edit and delete for skin snapshots

Add inline edit form and delete button to each snapshot card on /skin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Piotr Oleszczyk 2026-02-28 21:37:15 +01:00
parent d62812274b
commit d938c9999b
2 changed files with 309 additions and 41 deletions

View file

@ -1,4 +1,4 @@
import { createSkinSnapshot, getSkinSnapshots } from '$lib/api'; import { createSkinSnapshot, deleteSkinSnapshot, getSkinSnapshots, updateSkinSnapshot } from '$lib/api';
import { fail } from '@sveltejs/kit'; import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
@ -50,5 +50,59 @@ export const actions: Actions = {
} catch (e) { } catch (e) {
return fail(500, { error: (e as Error).message }); return fail(500, { error: (e as Error).message });
} }
},
update: async ({ request }) => {
const form = await request.formData();
const id = form.get('id') as string;
const snapshot_date = form.get('snapshot_date') as string;
const overall_state = form.get('overall_state') as string;
const texture = form.get('texture') as string;
const notes = form.get('notes') as string;
const hydration_level = form.get('hydration_level') as string;
const sensitivity_level = form.get('sensitivity_level') as string;
const barrier_state = form.get('barrier_state') as string;
const active_concerns_raw = form.get('active_concerns') as string;
const skin_type = form.get('skin_type') as string;
const sebum_tzone = form.get('sebum_tzone') as string;
const sebum_cheeks = form.get('sebum_cheeks') as string;
if (!id) return fail(400, { error: 'Missing id' });
const active_concerns = active_concerns_raw
?.split(',')
.map((c) => c.trim())
.filter(Boolean) ?? [];
const body: Record<string, unknown> = { active_concerns };
if (snapshot_date) body.snapshot_date = snapshot_date;
if (overall_state) body.overall_state = overall_state;
if (texture) body.texture = texture;
if (notes) body.notes = notes;
if (hydration_level) body.hydration_level = Number(hydration_level);
if (sensitivity_level) body.sensitivity_level = Number(sensitivity_level);
if (barrier_state) body.barrier_state = barrier_state;
if (skin_type) body.skin_type = skin_type;
if (sebum_tzone) body.sebum_tzone = Number(sebum_tzone);
if (sebum_cheeks) body.sebum_cheeks = Number(sebum_cheeks);
try {
await updateSkinSnapshot(id, body);
return { updated: true };
} catch (e) {
return fail(500, { error: (e as Error).message });
}
},
delete: async ({ request }) => {
const form = await request.formData();
const id = form.get('id') as string;
if (!id) return fail(400, { error: 'Missing id' });
try {
await deleteSkinSnapshot(id);
return { deleted: true };
} catch (e) {
return fail(500, { error: (e as Error).message });
}
} }
}; };

View file

@ -25,7 +25,7 @@
let showForm = $state(false); let showForm = $state(false);
// Form state (bound to inputs so AI can pre-fill) // Create form state (bound to inputs so AI can pre-fill)
let snapshotDate = $state(new Date().toISOString().slice(0, 10)); let snapshotDate = $state(new Date().toISOString().slice(0, 10));
let overallState = $state(''); let overallState = $state('');
let texture = $state(''); let texture = $state('');
@ -38,6 +38,36 @@
let activeConcernsRaw = $state(''); let activeConcernsRaw = $state('');
let notes = $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 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(', ') ?? '';
editNotes = snap.notes ?? '';
showForm = false;
}
// AI photo analysis state // AI photo analysis state
let aiPanelOpen = $state(false); let aiPanelOpen = $state(false);
let selectedFiles = $state<File[]>([]); let selectedFiles = $state<File[]>([]);
@ -101,6 +131,12 @@
{#if form?.created} {#if form?.created}
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Snapshot added.</div> <div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Snapshot added.</div>
{/if} {/if}
{#if form?.updated}
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Snapshot updated.</div>
{/if}
{#if form?.deleted}
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Snapshot deleted.</div>
{/if}
{#if showForm} {#if showForm}
<!-- AI photo analysis card --> <!-- AI photo analysis card -->
@ -284,9 +320,167 @@
{#each sortedSnapshots as snap (snap.id)} {#each sortedSnapshots as snap (snap.id)}
<Card> <Card>
<CardContent class="pt-4"> <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-2 gap-4"
>
<input type="hidden" name="id" value={snap.id} />
<div class="space-y-1">
<Label for="edit_snapshot_date">Date *</Label>
<Input
id="edit_snapshot_date"
name="snapshot_date"
type="date"
bind:value={editSnapshotDate}
required
/>
</div>
<div class="space-y-1">
<Label>Overall state</Label>
<input type="hidden" name="overall_state" value={editOverallState} />
<Select
type="single"
value={editOverallState}
onValueChange={(v) => (editOverallState = v)}
>
<SelectTrigger>{editOverallState || 'Select'}</SelectTrigger>
<SelectContent>
{#each states as s (s)}
<SelectItem value={s}>{s}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-1">
<Label>Texture</Label>
<input type="hidden" name="texture" value={editTexture} />
<Select
type="single"
value={editTexture}
onValueChange={(v) => (editTexture = v)}
>
<SelectTrigger>{editTexture || 'Select'}</SelectTrigger>
<SelectContent>
{#each skinTextures as t (t)}
<SelectItem value={t}>{t}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-1">
<Label>Skin type</Label>
<input type="hidden" name="skin_type" value={editSkinType} />
<Select
type="single"
value={editSkinType}
onValueChange={(v) => (editSkinType = v)}
>
<SelectTrigger
>{editSkinType ? editSkinType.replace(/_/g, ' ') : 'Select'}</SelectTrigger
>
<SelectContent>
{#each skinTypes as st (st)}
<SelectItem value={st}>{st.replace(/_/g, ' ')}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-1">
<Label>Barrier state</Label>
<input type="hidden" name="barrier_state" value={editBarrierState} />
<Select
type="single"
value={editBarrierState}
onValueChange={(v) => (editBarrierState = v)}
>
<SelectTrigger
>{editBarrierState
? editBarrierState.replace(/_/g, ' ')
: 'Select'}</SelectTrigger
>
<SelectContent>
{#each barrierStates as b (b)}
<SelectItem value={b}>{b.replace(/_/g, ' ')}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-1">
<Label for="edit_hydration_level">Hydration (15)</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">Sensitivity (15)</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">Sebum T-zone (15)</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">Sebum cheeks (15)</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">Active concerns (comma-separated)</Label>
<Input
id="edit_active_concerns"
name="active_concerns"
placeholder="acne, redness, dehydration"
bind:value={editActiveConcernsRaw}
/>
</div>
<div class="space-y-1 col-span-2">
<Label for="edit_notes">Notes</Label>
<Input id="edit_notes" name="notes" bind:value={editNotes} />
</div>
<div class="col-span-2 flex gap-2">
<Button type="submit">Save</Button>
<Button type="button" variant="outline" onclick={() => (editingId = null)}
>Cancel</Button
>
</div>
</form>
{:else}
<!-- Read view -->
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<span class="font-medium">{snap.snapshot_date}</span> <span class="font-medium">{snap.snapshot_date}</span>
<div class="flex gap-2"> <div class="flex items-center gap-2">
{#if snap.overall_state} {#if snap.overall_state}
<span <span
class="rounded-full px-2 py-0.5 text-xs font-medium {stateColors[ class="rounded-full px-2 py-0.5 text-xs font-medium {stateColors[
@ -299,6 +493,25 @@
{#if snap.texture} {#if snap.texture}
<Badge variant="secondary">{snap.texture}</Badge> <Badge variant="secondary">{snap.texture}</Badge>
{/if} {/if}
<Button
variant="ghost"
size="sm"
onclick={() => startEdit(snap)}
class="h-7 px-2 text-xs"
>
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"
>
Delete
</Button>
</form>
</div> </div>
</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">
@ -331,6 +544,7 @@
{#if snap.notes} {#if snap.notes}
<p class="mt-2 text-sm text-muted-foreground">{snap.notes}</p> <p class="mt-2 text-sm text-muted-foreground">{snap.notes}</p>
{/if} {/if}
{/if}
</CardContent> </CardContent>
</Card> </Card>
{:else} {:else}