Expand website and game trackers
Some checks failed
Test and deploy staging / staging (push) Failing after 5m29s

This commit is contained in:
2026-09-25 22:43:04 +00:00
parent f6b34150d4
commit c2ba8febb4
4076 changed files with 97237 additions and 1453 deletions

View File

@@ -1,4 +1,11 @@
<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 } from '$app/forms';
import { resolve } from '$app/paths';
import ProgressBar from '$lib/components/ProgressBar.svelte';
@@ -45,30 +52,44 @@
<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}<details class="relative">
<summary class="cursor-pointer text-base underline underline-offset-4">new list</summary>
<form
method="POST"
action="?/newList"
enctype="multipart/form-data"
use:enhance={uploadPicture}
class="absolute top-full right-0 z-10 mt-3 w-80 max-w-[calc(100vw-2rem)] space-y-3 rounded-lg border border-[var(--site-border)] bg-[var(--site-surface)] p-4 shadow-lg"
{#if data.isAdmin}<Dialog.Root>
<Dialog.Trigger disabled={!hydrated} class="underline underline-offset-4"
>new list</Dialog.Trigger
>
<label class="block"
>name<Input name="name" required maxlength={200} class="mt-2" /></label
<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
>
<label class="block"
>type<select
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
></select
></label
{#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"
>
<ListPictureDropzone label="List picture (optional)" />
<Button type="submit" disabled={uploading}>create list</Button>
</form>
</details>{/if}
<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}

View File

@@ -1,4 +1,13 @@
<script lang="ts">
import { NativeSelect } from '$lib/components/ui/native-select/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Textarea } from '$lib/components/ui/textarea/index.js';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Button } from '$lib/components/ui/button/index.js';
let addOpen = $state(false);
import { resolve } from '$app/paths';
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
import Check from 'lucide-svelte/icons/check';
@@ -28,7 +37,10 @@
update: () => Promise<void>;
}) => {
await update();
if (result.type === 'success') editingItem = null;
if (result.type === 'success') {
editingItem = null;
addOpen = false;
}
};
uploadError = '';
const picture = formData.get('picture');
@@ -65,12 +77,6 @@
};
</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')}
@@ -94,11 +100,11 @@
</p>
</div>
</div>
{#if data.isAdmin}<button
{#if data.isAdmin}<Button
type="button"
onclick={() => (managing = !managing)}
aria-pressed={managing}
class="text-base underline underline-offset-4">{managing ? 'done' : 'manage list'}</button
class="text-base underline underline-offset-4">{managing ? 'done' : 'manage list'}</Button
>{/if}
</div>
{#if form?.message}<p role="alert" class="text-red-700">{form.message}</p>{/if}
@@ -123,9 +129,11 @@
{/if}
{/if}
{#if data.isAdmin && managing}
<details class="border-b border-[var(--site-border)] pb-5">
<summary class="cursor-pointer text-base text-[var(--site-muted)]">list settings</summary>
<div class="mt-5 space-y-5">
<Collapsible.Root class="border-b border-[var(--site-border)] pb-5">
<Collapsible.Trigger class="cursor-pointer text-base text-[var(--site-muted)]"
>list settings</Collapsible.Trigger
>
<Collapsible.Content class="mt-5 space-y-5">
<form
method="POST"
action="?/updateList"
@@ -134,65 +142,78 @@
class="grid gap-3 sm:grid-cols-2 sm:items-end"
>
<label
>List name<input
>List name<Input
name="name"
value={data.list.name}
required
maxlength="200"
maxlength={200}
class="block w-full rounded border p-2"
/></label
>
<label
>Slug<input
>Slug<Input
name="slug"
value={data.list.slug}
required
maxlength="120"
maxlength={120}
pattern="[a-z0-9]+(-[a-z0-9]+)*"
class="block w-full rounded border p-2"
/></label
>
<label
>Type<select name="type" class="block w-full rounded border p-2">
>Type<NativeSelect name="type" class="block w-full rounded border p-2">
<option value="normal" selected={!data.list.isProgress}>Normal</option>
<option value="progress" selected={data.list.isProgress}>Progress</option>
</select></label
</NativeSelect></label
>
<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
><Checkbox name="removePicture" value="on" /> Remove current picture</label
>{/if}
</div>
<button disabled={uploading} class="rounded border p-2 disabled:opacity-50"
>Save list</button
<Button type="submit" 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>
<form method="POST" action="?/deleteList" class="mt-3 space-y-3">
<p class="text-sm">
This deletes the list, its items, and everyone’s saved progress. Type <strong
>{data.list.name}</strong
> to confirm.
</p>
<label class="block"
>List name<input
name="confirmName"
required
autocomplete="off"
class="mt-1 block w-full rounded border p-2"
/></label
<AlertDialog.Root>
<AlertDialog.Trigger class="font-medium text-destructive"
>Delete this list</AlertDialog.Trigger
><AlertDialog.Content
><AlertDialog.Header
><AlertDialog.Title>Delete this list?</AlertDialog.Title><AlertDialog.Description
>Deleting this list also removes its items and saved progress.</AlertDialog.Description
></AlertDialog.Header
>
<button class="rounded bg-red-700 px-4 py-2 text-white">Delete list</button>
</form>
</details>
</div>
</details>
<form method="POST" action="?/deleteList" class="mt-3 space-y-3">
<p class="text-sm">
This deletes the list, its items, and everyone’s saved progress. Type <strong
>{data.list.name}</strong
> to confirm.
</p>
<label class="block"
>List name<Input
name="confirmName"
required
autocomplete="off"
class="mt-1 block w-full rounded border p-2"
/></label
>
<AlertDialog.Footer
><AlertDialog.Cancel>Keep list</AlertDialog.Cancel><Button
type="submit"
variant="destructive">Delete list</Button
></AlertDialog.Footer
>
</form>
</AlertDialog.Content></AlertDialog.Root
>
</Collapsible.Content>
</Collapsible.Root>
{/if}
<label class="block">
<span class="sr-only">Search list items</span>
<input
<Input
type="search"
bind:value={search}
placeholder="Search list items"
@@ -223,19 +244,20 @@
<form method="POST" action="?/setCompletion" use:enhance class="shrink-0">
<input type="hidden" name="itemId" value={item.id} />
<input type="hidden" name="completed" value={completed.has(item.id) ? 'no' : 'yes'} />
<button
<Button
type="submit"
class="flex h-8 w-8 items-center justify-center rounded-full border border-[var(--site-border)] text-base"
aria-label={`${completed.has(item.id) ? 'Uncheck' : 'Check'} ${item.name}`}
>
{#if completed.has(item.id)}<Check size={16} aria-hidden="true" />{/if}
</button>
</Button>
</form>
{/if}
{#if data.isAdmin && managing}<button
{#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
aria-label={`Edit ${item.name}`}>Edit</Button
>{/if}
</div>
</li>
@@ -243,20 +265,21 @@
{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"
<Dialog.Root
open={!!(data.isAdmin && managing && editingItem)}
onOpenChange={(open) => {
if (!open) editingItem = null;
}}
>
<Dialog.Content class="max-h-[90dvh] overflow-y-auto sm:max-w-xl">
<Dialog.Header
><Dialog.Title>Edit item</Dialog.Title><Dialog.Description
>Update the name, picture and description.</Dialog.Description
></Dialog.Header
>
<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>
{#if form?.message}<p role="alert" class="text-destructive">{form.message}</p>{/if}
{#if uploadError}<p role="alert" class="text-destructive">{uploadError}</p>{/if}
{#if editingItem}
<form
method="POST"
action="?/updateItem"
@@ -266,30 +289,32 @@
>
<input type="hidden" name="itemId" value={editingItem.id} />
<label
>Name<input
>Name<Input
name="name"
value={editingItem.name}
required
maxlength="500"
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
><Checkbox name="removePicture" value="on" /> Remove current picture</label
>{/if}
<label
>Description (optional)<textarea
>Description (optional)<Textarea
name="description"
maxlength="5000"
rows="3"
class="block w-full rounded border p-2">{editingItem.description ?? ''}</textarea
></label
maxlength={5000}
rows={3}
class="block w-full rounded border p-2"
value={editingItem.description ?? ''}
/></label
>
<button
<Button
type="submit"
disabled={uploading}
class="justify-self-start rounded bg-gray-900 px-4 py-2 text-white disabled:opacity-50"
>Save item</button
>Save item</Button
>
</form>
<form
@@ -298,51 +323,66 @@
use:enhance={() =>
async ({ result, update }) => {
await update();
if (result.type === 'success') editingItem = null;
if (result.type === 'success') {
editingItem = null;
addOpen = false;
}
}}
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
<Button
type="submit"
class="text-sm text-red-700 underline"
aria-label={`Remove ${editingItem.name}`}>Remove item</Button
>
</form>
</div>
</div>
{/if}
{/if}</Dialog.Content
></Dialog.Root
>
{#if data.isAdmin && managing}
<details class="border-t border-[var(--site-border)] pt-5">
<summary class="cursor-pointer font-medium">add item</summary>
<form
method="POST"
action="?/addItem"
enctype="multipart/form-data"
use:enhance={uploadPicture}
class="mt-5 grid gap-3"
>
<label
>Name<input
name="name"
required
maxlength="500"
class="block w-full rounded border p-2"
/></label
<Dialog.Root bind:open={addOpen}
><Dialog.Trigger class="underline underline-offset-4">add item</Dialog.Trigger><Dialog.Content
><Dialog.Header
><Dialog.Title>Add item</Dialog.Title><Dialog.Description
>Add a new entry to this list.</Dialog.Description
></Dialog.Header
>
<ListPictureDropzone />
<label
>Description (optional)<textarea
name="description"
maxlength="5000"
rows="3"
class="block w-full rounded border p-2"></textarea></label
{#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="?/addItem"
enctype="multipart/form-data"
use:enhance={uploadPicture}
class="mt-5 grid gap-3"
>
<button
disabled={uploading}
class="justify-self-start rounded bg-gray-900 px-4 py-2 text-white disabled:opacity-50"
>Add item</button
>
</form>
</details>
<label
>Name<Input
name="name"
required
maxlength={500}
class="block w-full rounded border p-2"
/></label
>
<ListPictureDropzone />
<label
>Description (optional)<Textarea
name="description"
maxlength={5000}
rows={3}
class="block w-full rounded border p-2"
/></label
>
<Button
type="submit"
disabled={uploading}
class="justify-self-start rounded bg-gray-900 px-4 py-2 text-white disabled:opacity-50"
>Add item</Button
>
</form>
</Dialog.Content></Dialog.Root
>
<noscript><p>Enable JavaScript to upload item pictures.</p></noscript>
{/if}
</section>