splitwise / src /lib /components /ExpenseForm.svelte
assafvayner's picture
assafvayner HF Staff
fix(ui): split editor usable without JS, untrack prop snapshots, clean orphan photo on conflict
3b74a95
Raw
History Blame Contribute Delete
3.54 kB
<script lang="ts">
import { enhance } from '$app/forms';
import { downscaleImage } from '$lib/client/downscale';
import type { ExpensePhoto, PublicUser } from '$lib/domain/types';
import type { ExpenseFormValues, FieldErrors } from '$lib/forms/types';
import { untrack } from 'svelte';
import FormError from './FormError.svelte';
import SplitEditor from './SplitEditor.svelte';
let {
members,
currency,
values,
errors = {},
existingPhoto,
version,
submitLabel
}: {
members: PublicUser[];
currency: string;
values: ExpenseFormValues;
errors?: FieldErrors;
existingPhoto?: ExpensePhoto;
version?: number;
submitLabel: string;
} = $props();
// one-time snapshot of props at mount; not meant to react to later prop changes
let amount = $state(untrack(() => values.amount));
let submitting = $state(false);
</script>
<form
method="POST"
enctype="multipart/form-data"
class="space-y-4"
use:enhance={async ({ formData }) => {
submitting = true;
let reachedReturn = false;
try {
const photo = formData.get('photo');
if (photo instanceof File && photo.size > 0 && photo.type.startsWith('image/')) {
try {
const small = await downscaleImage(photo);
if (small) formData.set('photo', small, 'photo.jpg');
} catch {
/* keep original file */
}
}
reachedReturn = true;
} finally {
if (!reachedReturn) submitting = false;
}
return async ({ update }) => {
submitting = false;
await update();
};
}}
>
{#if version !== undefined}
<input type="hidden" name="version" value={version} />
{/if}
<div>
<label for="description">Description</label>
<input
id="description"
name="description"
required
maxlength="120"
value={values.description}
placeholder="Dinner, groceries, tickets…"
/>
<FormError message={errors.description} />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="amount">Amount ({currency})</label>
<input id="amount" name="amount" inputmode="decimal" placeholder="0.00" required bind:value={amount} />
<FormError message={errors.amount} />
</div>
<div>
<label for="date">Date</label>
<input id="date" name="date" type="date" required value={values.date} />
<FormError message={errors.date} />
</div>
</div>
<div>
<label for="paidBy">Paid by</label>
<select id="paidBy" name="paidBy" value={values.paidBy}>
<option value="">Choose…</option>
{#each members as m (m.id)}
<option value={m.id}>{m.displayName}</option>
{/each}
</select>
<FormError message={errors.paidBy} />
</div>
<SplitEditor {members} {currency} {values} {amount} error={errors.participants ?? errors.shares} />
<div>
<label for="notes">Notes (optional)</label>
<textarea id="notes" name="notes" rows="2" maxlength="2000" value={values.notes}></textarea>
</div>
<div>
<label for="photo">Receipt photo (optional)</label>
{#if existingPhoto}
<img src="/{existingPhoto.key}" alt="Current receipt" class="mb-2 max-h-40 rounded-lg" />
<label class="mb-2 flex items-center gap-2 font-normal"
><input type="checkbox" name="removePhoto" class="h-5 w-5" /> Remove current photo</label
>
{/if}
<input id="photo" name="photo" type="file" accept="image/*" />
<FormError message={errors.photo} />
</div>
<FormError message={errors.form} />
<button
type="submit"
disabled={submitting}
class="w-full rounded-lg bg-green-700 py-3 font-semibold text-white disabled:opacity-60"
>
{submitting ? 'Saving…' : submitLabel}
</button>
</form>