app: lists on meta tables, schema-qualified SQL, typed columns

- lists read/write real lists.* tables via meta.tables/meta.columns and the
  SECURITY DEFINER meta functions; typed extra columns in the item forms
- old numeric list URLs 308-redirect to the slug
- include_recurring is boolean, osu accuracy/pp/rate numeric
- raw SQL qualified with the new schemas
This commit is contained in:
2026-10-23 22:03:42 +00:00
parent 64fb8df262
commit 4cfe6bad6c
25 changed files with 529 additions and 218 deletions

View File

@@ -1,5 +1,4 @@
<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';
@@ -14,6 +13,7 @@
import { enhance, type SubmitFunction } from '$app/forms';
import ListPictureDropzone from '#lib/components/ListPictureDropzone.svelte';
import ProgressBar from '#lib/components/ProgressBar.svelte';
import ListExtraFields from '#lib/components/ListExtraFields.svelte';
import type { PageProps } from './$types';
let { data, form }: PageProps = $props();
let completed = $derived(new Set(data.completedIds));
@@ -109,23 +109,16 @@
{#if form?.message}<p role="alert" class="text-red-700">{form.message}</p>{/if}
{#if uploadError}<p role="alert" class="text-red-700">{uploadError}</p>{/if}
{#if data.list.isProgress}
{#if data.user}
<div class="border-y border-[var(--site-border)] py-4">
<p class="font-medium">
{data.checked} of {data.total} checked · {data.total
? Math.round((data.checked / data.total) * 100)
: 0}%
</p>
<div class="mt-2">
<ProgressBar value={data.checked} max={data.total} label="List progress" small />
</div>
</div>
{:else}
<p>
<a class="underline" href={resolve(`login?returnTo=/lists/${data.list.slug}`)}>Log in</a> to save
your progress.
<div class="border-y border-[var(--site-border)] py-4">
<p class="font-medium">
{data.checked} of {data.total} checked · {data.total
? Math.round((data.checked / data.total) * 100)
: 0}%
</p>
{/if}
<div class="mt-2">
<ProgressBar value={data.checked} max={data.total} label="List progress" small />
</div>
</div>
{/if}
{#if data.isAdmin && managing}
<Collapsible.Root class="border-b border-[var(--site-border)] pb-5">
@@ -159,12 +152,9 @@
class="block w-full rounded border p-2"
/></label
>
<label
>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>
</NativeSelect></label
>
<p class="self-end text-sm text-[var(--site-muted)]">
{data.list.isProgress ? 'Progress' : 'Normal'} list (type is fixed once created)
</p>
<div class="sm:col-span-2">
<ListPictureDropzone label="List picture (optional)" />
{#if data.list.pictureUrl}<label class="mt-2 block text-sm"
@@ -238,8 +228,21 @@
>
{item.description}
</p>{/if}
{#if data.list.columns.length}<p class="mt-1 text-sm text-[var(--site-muted)]">
{#each data.list.columns.filter((c) => item.extras[c.name] !== null && item.extras[c.name] !== '') as column, i (column.name)}{#if i}
·
{/if}{column.label}: {#if column.uiType === 'url'}<a
class="underline"
href={String(item.extras[column.name])}
rel="noopener noreferrer">link</a
>{:else if column.uiType === 'bool'}{item.extras[column.name]
? 'yes'
: 'no'}{:else if column.uiType === 'cents'}{(
Number(item.extras[column.name]) / 100
).toFixed(2)}{:else}{item.extras[column.name]}{/if}{/each}
</p>{/if}
</div>
{#if data.list.isProgress && data.user}
{#if data.list.isProgress && data.isAdmin}
<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'} />
@@ -251,6 +254,8 @@
{#if completed.has(item.id)}<Check size={16} aria-hidden="true" />{/if}
</Button>
</form>
{:else if data.list.isProgress && completed.has(item.id)}
<Check size={16} aria-label="Done" class="shrink-0" />
{/if}
{#if data.isAdmin && managing}<Button
type="button"
@@ -309,6 +314,7 @@
value={editingItem.description ?? ''}
/></label
>
<ListExtraFields columns={data.list.columns} values={editingItem.extras} />
<Button
type="submit"
disabled={uploading}
@@ -373,6 +379,7 @@
class="block w-full rounded border p-2"
/></label
>
<ListExtraFields columns={data.list.columns} />
<Button
type="submit"
disabled={uploading}