add icons to lists and solve money tracker bugs
All checks were successful
Test and deploy staging / staging (push) Successful in 2m30s
All checks were successful
Test and deploy staging / staging (push) Successful in 2m30s
This commit is contained in:
@@ -12,11 +12,28 @@
|
||||
let uploadError = $state('');
|
||||
let uploading = $state(false);
|
||||
let managing = $state(false);
|
||||
let search = $state('');
|
||||
let editingItem = $state<(typeof data.list.items)[number] | null>(null);
|
||||
let visibleItems = $derived(
|
||||
data.list.items.filter((item) =>
|
||||
`${item.name} ${item.description ?? ''}`.toLowerCase().includes(search.trim().toLowerCase())
|
||||
)
|
||||
);
|
||||
const uploadPicture: SubmitFunction = async ({ formData, cancel }) => {
|
||||
const finish = async ({
|
||||
result,
|
||||
update
|
||||
}: {
|
||||
result: { type: string };
|
||||
update: () => Promise<void>;
|
||||
}) => {
|
||||
await update();
|
||||
if (result.type === 'success') editingItem = null;
|
||||
};
|
||||
uploadError = '';
|
||||
const picture = formData.get('picture');
|
||||
formData.delete('picture');
|
||||
if (!(picture instanceof File) || picture.size === 0) return;
|
||||
if (!(picture instanceof File) || picture.size === 0) return finish;
|
||||
if (!['image/jpeg', 'image/png', 'image/webp'].includes(picture.type)) {
|
||||
uploadError = 'Choose a JPEG, PNG, or WebP image.';
|
||||
cancel();
|
||||
@@ -44,9 +61,16 @@
|
||||
} finally {
|
||||
uploading = false;
|
||||
}
|
||||
return finish;
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Escape') editingItem = null;
|
||||
}}
|
||||
/>
|
||||
|
||||
<section class="mx-auto max-w-4xl space-y-6 px-4 py-10 sm:px-6 sm:py-14">
|
||||
<a
|
||||
href={resolve('/lists')}
|
||||
@@ -54,12 +78,21 @@
|
||||
><ArrowLeft size={16} aria-hidden="true" /> lists</a
|
||||
>
|
||||
<div class="flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-4xl font-semibold tracking-tight sm:text-5xl">{data.list.name}</h1>
|
||||
<p class="mt-2 text-base text-[var(--site-muted)]">
|
||||
{data.total}
|
||||
{data.total === 1 ? 'item' : 'items'}
|
||||
</p>
|
||||
<div class="flex items-center gap-4">
|
||||
{#if data.list.pictureUrl}<img
|
||||
src={data.list.pictureUrl}
|
||||
alt=""
|
||||
width="96"
|
||||
height="96"
|
||||
class="h-20 w-20 rounded-lg bg-[var(--site-surface)] object-cover sm:h-24 sm:w-24"
|
||||
/>{/if}
|
||||
<div>
|
||||
<h1 class="text-4xl font-semibold tracking-tight sm:text-5xl">{data.list.name}</h1>
|
||||
<p class="mt-2 text-base text-[var(--site-muted)]">
|
||||
{data.total}
|
||||
{data.total === 1 ? 'item' : 'items'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if data.isAdmin}<button
|
||||
type="button"
|
||||
@@ -96,7 +129,8 @@
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateList"
|
||||
use:enhance
|
||||
enctype="multipart/form-data"
|
||||
use:enhance={uploadPicture}
|
||||
class="grid gap-3 sm:grid-cols-2 sm:items-end"
|
||||
>
|
||||
<label
|
||||
@@ -124,7 +158,15 @@
|
||||
<option value="progress" selected={data.list.isProgress}>Progress</option>
|
||||
</select></label
|
||||
>
|
||||
<button class="rounded border p-2">Save list</button>
|
||||
<div class="sm:col-span-2">
|
||||
<ListPictureDropzone label="List picture (optional)" />
|
||||
{#if data.list.pictureUrl}<label class="mt-2 block text-sm"
|
||||
><input type="checkbox" name="removePicture" /> Remove current picture</label
|
||||
>{/if}
|
||||
</div>
|
||||
<button disabled={uploading} class="rounded border p-2 disabled:opacity-50"
|
||||
>Save list</button
|
||||
>
|
||||
</form>
|
||||
<details class="border-t border-[var(--site-border)] pt-4">
|
||||
<summary class="cursor-pointer font-medium text-red-700">Delete this list</summary>
|
||||
@@ -148,8 +190,17 @@
|
||||
</div>
|
||||
</details>
|
||||
{/if}
|
||||
<label class="block">
|
||||
<span class="sr-only">Search list items</span>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={search}
|
||||
placeholder="Search list items"
|
||||
class="w-full rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
|
||||
/>
|
||||
</label>
|
||||
<ul class="divide-y divide-[var(--site-border)] border-t border-[var(--site-border)]">
|
||||
{#each data.list.items as item (item.id)}
|
||||
{#each visibleItems as item (item.id)}
|
||||
<li class="py-4">
|
||||
<div class="flex items-start gap-4">
|
||||
{#if item.pictureUrl}<img
|
||||
@@ -180,56 +231,85 @@
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
{#if data.isAdmin && managing}<button
|
||||
type="button"
|
||||
onclick={() => (editingItem = item)}
|
||||
class="shrink-0 rounded border border-[var(--site-border)] px-3 py-1 text-sm hover:bg-[var(--site-surface)]"
|
||||
aria-label={`Edit ${item.name}`}>Edit</button
|
||||
>{/if}
|
||||
</div>
|
||||
{#if data.isAdmin && managing}
|
||||
<details class="mt-3 border-t border-[var(--site-border)] pt-3">
|
||||
<summary class="cursor-pointer text-base underline">edit item</summary>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateItem"
|
||||
enctype="multipart/form-data"
|
||||
use:enhance={uploadPicture}
|
||||
class="mt-3 grid gap-3"
|
||||
>
|
||||
<input type="hidden" name="itemId" value={item.id} />
|
||||
<label
|
||||
>Name<input
|
||||
name="name"
|
||||
value={item.name}
|
||||
required
|
||||
maxlength="500"
|
||||
class="block w-full rounded border p-2"
|
||||
/></label
|
||||
>
|
||||
<ListPictureDropzone label="Replace picture (optional)" />
|
||||
{#if item.pictureUrl}<label class="text-sm"
|
||||
><input type="checkbox" name="removePicture" /> Remove current picture</label
|
||||
>{/if}
|
||||
<label
|
||||
>Description (optional)<textarea
|
||||
name="description"
|
||||
maxlength="5000"
|
||||
rows="3"
|
||||
class="block w-full rounded border p-2">{item.description ?? ''}</textarea
|
||||
></label
|
||||
>
|
||||
<button
|
||||
disabled={uploading}
|
||||
class="justify-self-start rounded bg-gray-900 px-4 py-2 text-white disabled:opacity-50"
|
||||
>Save item</button
|
||||
>
|
||||
</form>
|
||||
<form method="POST" action="?/removeItem" use:enhance class="mt-3">
|
||||
<input type="hidden" name="itemId" value={item.id} />
|
||||
<button class="text-sm text-red-700 underline" aria-label={`Remove ${item.name}`}
|
||||
>Remove item</button
|
||||
>
|
||||
</form>
|
||||
</details>
|
||||
{/if}
|
||||
</li>
|
||||
{:else}<li class="py-5 text-[var(--site-muted)]">no items yet</li>{/each}
|
||||
{:else}<li class="py-5 text-[var(--site-muted)]">
|
||||
{search ? 'No matching items' : 'no items yet'}
|
||||
</li>{/each}
|
||||
</ul>
|
||||
{#if data.isAdmin && managing && editingItem}
|
||||
<div class="fixed inset-0 z-40 bg-black/60"></div>
|
||||
<div class="fixed inset-0 z-50 overflow-y-auto p-4 sm:p-8">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={`Edit ${editingItem.name}`}
|
||||
class="mx-auto max-w-xl rounded-lg bg-[var(--site-bg)] p-5 shadow-xl"
|
||||
>
|
||||
<div class="mb-4 flex items-center justify-between gap-4">
|
||||
<h2 class="text-xl font-semibold">Edit item</h2>
|
||||
<button type="button" onclick={() => (editingItem = null)} class="underline">Close</button
|
||||
>
|
||||
</div>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateItem"
|
||||
enctype="multipart/form-data"
|
||||
use:enhance={uploadPicture}
|
||||
class="grid gap-3"
|
||||
>
|
||||
<input type="hidden" name="itemId" value={editingItem.id} />
|
||||
<label
|
||||
>Name<input
|
||||
name="name"
|
||||
value={editingItem.name}
|
||||
required
|
||||
maxlength="500"
|
||||
class="block w-full rounded border p-2"
|
||||
/></label
|
||||
>
|
||||
<ListPictureDropzone label="Replace picture (optional)" />
|
||||
{#if editingItem.pictureUrl}<label class="text-sm"
|
||||
><input type="checkbox" name="removePicture" /> Remove current picture</label
|
||||
>{/if}
|
||||
<label
|
||||
>Description (optional)<textarea
|
||||
name="description"
|
||||
maxlength="5000"
|
||||
rows="3"
|
||||
class="block w-full rounded border p-2">{editingItem.description ?? ''}</textarea
|
||||
></label
|
||||
>
|
||||
<button
|
||||
disabled={uploading}
|
||||
class="justify-self-start rounded bg-gray-900 px-4 py-2 text-white disabled:opacity-50"
|
||||
>Save item</button
|
||||
>
|
||||
</form>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/removeItem"
|
||||
use:enhance={() =>
|
||||
async ({ result, update }) => {
|
||||
await update();
|
||||
if (result.type === 'success') editingItem = null;
|
||||
}}
|
||||
class="mt-5 border-t border-[var(--site-border)] pt-4"
|
||||
>
|
||||
<input type="hidden" name="itemId" value={editingItem.id} />
|
||||
<button class="text-sm text-red-700 underline" aria-label={`Remove ${editingItem.name}`}
|
||||
>Remove item</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.isAdmin && managing}
|
||||
<details class="border-t border-[var(--site-border)] pt-5">
|
||||
<summary class="cursor-pointer font-medium">add item</summary>
|
||||
|
||||
Reference in New Issue
Block a user