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:
parent
d62812274b
commit
d938c9999b
2 changed files with 309 additions and 41 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { createSkinSnapshot, getSkinSnapshots } from '$lib/api';
|
||||
import { createSkinSnapshot, deleteSkinSnapshot, getSkinSnapshots, updateSkinSnapshot } from '$lib/api';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
|
|
@ -50,5 +50,59 @@ export const actions: Actions = {
|
|||
} catch (e) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
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 overallState = $state('');
|
||||
let texture = $state('');
|
||||
|
|
@ -38,6 +38,36 @@
|
|||
let activeConcernsRaw = $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
|
||||
let aiPanelOpen = $state(false);
|
||||
let selectedFiles = $state<File[]>([]);
|
||||
|
|
@ -101,6 +131,12 @@
|
|||
{#if form?.created}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Snapshot added.</div>
|
||||
{/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}
|
||||
<!-- AI photo analysis card -->
|
||||
|
|
@ -284,9 +320,167 @@
|
|||
{#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-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 (1–5)</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 (1–5)</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 (1–5)</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 (1–5)</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">
|
||||
<span class="font-medium">{snap.snapshot_date}</span>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
{#if snap.overall_state}
|
||||
<span
|
||||
class="rounded-full px-2 py-0.5 text-xs font-medium {stateColors[
|
||||
|
|
@ -299,6 +493,25 @@
|
|||
{#if snap.texture}
|
||||
<Badge variant="secondary">{snap.texture}</Badge>
|
||||
{/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 class="grid grid-cols-3 gap-3 text-sm mb-3">
|
||||
|
|
@ -331,6 +544,7 @@
|
|||
{#if snap.notes}
|
||||
<p class="mt-2 text-sm text-muted-foreground">{snap.notes}</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue