Files
parentcalendar/frontend/src/routes/EventEditor.svelte
T

151 lines
6.0 KiB
Svelte

<script lang="ts">
import Check from '@lucide/svelte/icons/check';
import Quote from '@lucide/svelte/icons/quote';
import Trash2 from '@lucide/svelte/icons/trash-2';
import { untrack } from 'svelte';
import { KINDS, type CalEvent, type EventValues as Values } from '../lib/api';
import { app } from '../lib/state.svelte';
let {
event = null,
defaults = {},
onsave,
oncancel,
ondelete = null,
}: {
event?: CalEvent | null;
defaults?: Partial<Values>;
onsave: (v: Values) => void | Promise<void>;
oncancel: () => void;
ondelete?: (() => void) | null;
} = $props();
// The form edits a copy taken when it opens; later changes to the props don't reset it.
const initial = untrack(() => ({ ...defaults, ...(event ?? {}) }));
let f = $state<Values>({
title: initial.title ?? '',
date: initial.date ?? '',
end_date: initial.end_date ?? null,
all_day: initial.all_day ?? true,
start_time: initial.start_time ?? null,
end_time: initial.end_time ?? null,
kind: initial.kind ?? 'event',
notes: initial.notes ?? null,
location: initial.location ?? null,
child_ids: [...(initial.child_ids ?? [])],
});
let saving = $state(false);
const kinds = $derived(Object.entries(KINDS).filter(([k]) => k !== 'pack_lunch' || f.kind === 'pack_lunch'));
const uid = untrack(() => event?.id ?? 'new');
async function submit(e: SubmitEvent) {
e.preventDefault();
saving = true;
try {
await onsave({
...f,
title: f.title.trim(),
end_date: f.end_date || null,
start_time: f.all_day ? null : f.start_time || null,
end_time: f.all_day ? null : f.end_time || null,
notes: f.notes?.trim() || null,
location: f.location?.trim() || null,
});
} finally {
saving = false;
}
}
</script>
<form class="editor" onsubmit={submit}>
{#if event?.source_text}
<p class="source"><Quote size={14} aria-hidden="true" /><span>On the page: “{event.source_text}”</span></p>
{/if}
<div class="field">
<label class="label" for="title-{uid}">Title</label>
<input id="title-{uid}" bind:value={f.title} required maxlength="80" />
</div>
<div class="fields">
<div class="field">
<label class="label" for="date-{uid}">Date</label>
<input id="date-{uid}" type="date" bind:value={f.date} required />
</div>
<div class="field">
<label class="label" for="end-{uid}">Last day <span class="hint">(multi-day)</span></label>
<input id="end-{uid}" type="date" bind:value={f.end_date} min={f.date} />
</div>
<div class="field">
<label class="label" for="kind-{uid}">Kind</label>
<select id="kind-{uid}" bind:value={f.kind}>
{#each kinds as [value, label]}<option {value}>{label}</option>{/each}
</select>
</div>
</div>
<label class="switch">
<input type="checkbox" role="switch" bind:checked={f.all_day} />
<span class="track" aria-hidden="true"><span class="thumb"></span></span>
<span>All day</span>
</label>
{#if !f.all_day}
<div class="fields">
<div class="field">
<label class="label" for="start-{uid}">Starts</label>
<input id="start-{uid}" type="time" bind:value={f.start_time} required />
</div>
<div class="field">
<label class="label" for="stop-{uid}">Ends <span class="hint">(optional)</span></label>
<input id="stop-{uid}" type="time" bind:value={f.end_time} />
</div>
</div>
{/if}
<fieldset class="field">
<legend class="label">For</legend>
<div class="row">
{#each app.household!.children as c (c.id)}
<label class="chip-check">
<input type="checkbox" bind:group={f.child_ids} value={c.id} />
<span class="dot" style:background={c.color}></span>{c.display_name}
</label>
{/each}
</div>
</fieldset>
<div class="field">
<label class="label" for="loc-{uid}">Location</label>
<input id="loc-{uid}" bind:value={f.location} maxlength="200" />
</div>
<div class="field">
<label class="label" for="notes-{uid}">Notes</label>
<textarea id="notes-{uid}" bind:value={f.notes} maxlength="2000"></textarea>
</div>
<div class="footer">
<div class="row">
<button class="primary" type="submit" disabled={saving}><Check size={18} aria-hidden="true" />Save</button>
<button type="button" class="ghost" onclick={oncancel}>Cancel</button>
</div>
{#if ondelete}
<button type="button" class="danger" onclick={ondelete}><Trash2 size={18} aria-hidden="true" />Delete</button>
{/if}
</div>
</form>
<style>
.editor { display: flex; flex-direction: column; gap: var(--s4); padding: var(--s4) var(--s2) var(--s2); margin-top: var(--s2); border-top: 1px solid var(--border); }
.source { display: flex; gap: var(--s2); align-items: flex-start; padding: var(--s2) var(--s3); border-radius: var(--r-md); background: var(--surface-2); color: var(--text-2); font-size: var(--fs-sm); }
.source :global(svg) { flex: none; margin-top: 3px; }
.footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s3); padding-top: var(--s2); }
.switch { display: inline-flex; align-items: center; gap: var(--s3); min-height: 44px; cursor: pointer; font-weight: 600; align-self: flex-start; }
.switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.track { position: relative; width: 44px; height: 26px; border-radius: var(--r-full); background: var(--surface-3); border: 1px solid var(--border-strong); transition: background-color var(--dur) var(--ease); }
.thumb { position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%; background: var(--surface); box-shadow: var(--shadow-1); transition: transform var(--dur) var(--ease); }
.switch input:checked + .track { background: var(--primary); border-color: var(--primary); }
.switch input:checked + .track .thumb { transform: translateX(18px); }
.switch input:focus-visible + .track { outline: 3px solid var(--primary); outline-offset: 2px; }
</style>