Some checks failed
Test and deploy staging / staging (push) Failing after 43s
Run the sveltekit-3 migration and finish its manual tasks:
- Move kit config into the sveltekit() Vite plugin; drop svelte.config.js
- Replace $lib with #lib subpath imports
- Declare env vars in src/env.ts and import them from $app/env/{private,public}
- Consolidate param matchers into src/params.ts
- Use error(status, message), Response.json, refreshAll and goto reset/shallow
- Fall back to a full page load when goto rejects a login return path
Switch from adapter-node to adapter-bun: the build script runs Vite under
Bun (the adapter requires it) and the image starts with `bun ./build`.
Runtime env names (PORT, BODY_SIZE_LIMIT, ADDRESS_HEADER, ...) are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
4.8 KiB
Svelte
141 lines
4.8 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
let hydrated = $state(false);
|
|
onMount(() => {
|
|
hydrated = true;
|
|
});
|
|
import { NativeSelect } from '#lib/components/ui/native-select/index.js';
|
|
import * as Dialog from '#lib/components/ui/dialog/index.js';
|
|
import { enhance, type SubmitFunction } from '$app/forms';
|
|
import { resolve } from '$app/paths';
|
|
import ProgressBar from '#lib/components/ProgressBar.svelte';
|
|
import ListPictureDropzone from '#lib/components/ListPictureDropzone.svelte';
|
|
import { Button } from '#lib/components/ui/button/index.js';
|
|
import { Input } from '#lib/components/ui/input/index.js';
|
|
import type { PageProps } from './$types';
|
|
let { data, form }: PageProps = $props();
|
|
let uploadError = $state('');
|
|
let uploading = $state(false);
|
|
const uploadPicture: SubmitFunction = async ({ formData, cancel }) => {
|
|
uploadError = '';
|
|
const picture = formData.get('picture');
|
|
formData.delete('picture');
|
|
if (!(picture instanceof File) || !picture.size) return;
|
|
if (!['image/jpeg', 'image/png', 'image/webp'].includes(picture.type)) {
|
|
uploadError = 'Choose a JPEG, PNG, or WebP image.';
|
|
cancel();
|
|
return;
|
|
}
|
|
if (picture.size > 5 * 1024 * 1024) {
|
|
uploadError = 'Images must be 5 MB or smaller.';
|
|
cancel();
|
|
return;
|
|
}
|
|
uploading = true;
|
|
try {
|
|
const body = new FormData();
|
|
body.set('picture', picture);
|
|
const response = await fetch(resolve('lists/api/upload'), { method: 'POST', body });
|
|
if (!response.ok) throw new Error('Could not upload the image. Please try again.');
|
|
const { key } = await response.json();
|
|
formData.set('pictureKey', key);
|
|
} catch (cause) {
|
|
uploadError = cause instanceof Error ? cause.message : 'Image upload failed.';
|
|
cancel();
|
|
} finally {
|
|
uploading = false;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<div class="mx-auto max-w-6xl px-4 py-12 sm:px-6 sm:py-16">
|
|
<div class="mb-10 flex flex-wrap items-center justify-between gap-4">
|
|
<h1 class="text-4xl font-semibold tracking-tight sm:text-5xl">lists</h1>
|
|
{#if data.isAdmin}<Dialog.Root>
|
|
<Dialog.Trigger disabled={!hydrated} class="underline underline-offset-4"
|
|
>new list</Dialog.Trigger
|
|
>
|
|
<Dialog.Content
|
|
><Dialog.Header
|
|
><Dialog.Title>new list</Dialog.Title><Dialog.Description
|
|
>Create a collection or track progress through a list.</Dialog.Description
|
|
></Dialog.Header
|
|
>
|
|
{#if form?.message}<p role="alert" class="text-destructive">
|
|
{form.message}
|
|
</p>{/if}{#if uploadError}<p role="alert" class="text-destructive">
|
|
{uploadError}
|
|
</p>{/if}
|
|
<form
|
|
method="POST"
|
|
action="?/newList"
|
|
enctype="multipart/form-data"
|
|
use:enhance={uploadPicture}
|
|
class="space-y-4"
|
|
>
|
|
<label class="block"
|
|
>name<Input name="name" required maxlength={200} class="mt-2" /></label
|
|
>
|
|
<label class="block"
|
|
>type<NativeSelect
|
|
name="type"
|
|
class="mt-2 w-full rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
|
|
><option value="normal">normal</option><option value="progress">progress</option
|
|
></NativeSelect
|
|
></label
|
|
>
|
|
<ListPictureDropzone label="List picture (optional)" />
|
|
<Button type="submit" disabled={uploading}>create list</Button>
|
|
</form>
|
|
</Dialog.Content></Dialog.Root
|
|
>{/if}
|
|
</div>
|
|
{#if form?.message}<p role="alert" class="mb-5 text-red-500">{form.message}</p>{/if}
|
|
{#if uploadError}<p role="alert" class="mb-5 text-red-500">{uploadError}</p>{/if}
|
|
<ul>
|
|
{#each data.lists as list (list.id)}
|
|
<li class="border-t border-[var(--site-border)]">
|
|
<a
|
|
href={resolve('/lists/[slug]', { slug: list.slug })}
|
|
class={list.isProgress
|
|
? 'grid grid-cols-[minmax(0,1fr)_8rem_auto] items-center gap-3 py-5 hover:text-[var(--site-muted)]'
|
|
: 'flex items-center justify-between gap-3 py-5 hover:text-[var(--site-muted)]'}
|
|
>
|
|
<span class="flex min-w-0 items-center gap-3">
|
|
{#if list.pictureUrl}<img
|
|
src={list.pictureUrl}
|
|
alt=""
|
|
loading="lazy"
|
|
width="48"
|
|
height="48"
|
|
class="h-12 w-12 rounded bg-[var(--site-surface)] object-cover"
|
|
/>{/if}
|
|
<span class="truncate text-lg font-medium">{list.name}</span>
|
|
</span>
|
|
{#if list.isProgress}
|
|
<span class="block w-32"
|
|
><ProgressBar
|
|
value={list.checked}
|
|
max={list.total}
|
|
label={`Progress on ${list.name}`}
|
|
small
|
|
/></span
|
|
>
|
|
{/if}
|
|
<span class="min-w-16 whitespace-nowrap text-right text-base text-[var(--site-muted)]">
|
|
{#if list.isProgress && data.user}
|
|
{list.total ? Math.round((list.checked / list.total) * 100) : 0}%
|
|
{:else}
|
|
{list.total} {list.total === 1 ? 'item' : 'items'}
|
|
{/if}
|
|
</span>
|
|
</a>
|
|
</li>
|
|
{:else}
|
|
<li class="border-t border-[var(--site-border)] py-5 text-[var(--site-muted)]">
|
|
no lists yet
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|