Compare commits

...

1 Commits

Author SHA1 Message Date
root
5b90667b4b fix: replace {#await} IIFE trick with $effect for streamed data on discover page
All checks were successful
Release / Test backend (push) Successful in 49s
Release / Check ui (push) Successful in 2m0s
Release / Docker (push) Successful in 7m4s
Release / Gitea Release (push) Successful in 28s
The {#await ... then} + {@const} IIFE pattern for assigning to $state
variables stopped working reliably in Svelte 5.53+. Replaced with a
proper $effect that awaits both streamed promises and assigns to state,
which correctly triggers reactivity.

Also: switch library page selection mode entry from long-press to a
'Select' button in the page header.
2026-04-13 21:14:14 +05:00
2 changed files with 17 additions and 48 deletions

View File

@@ -53,11 +53,6 @@
filteredBooks.length > 0 && filteredBooks.every((b) => selected.has(b.slug))
);
function enterSelectMode(slug: string) {
selectMode = true;
selected = new Set([slug]);
}
function exitSelectMode() {
selectMode = false;
selected = new Set();
@@ -81,43 +76,11 @@
}
}
// Long-press support (pointer events, works on desktop + mobile)
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
let longPressFired = false;
function onPointerDown(slug: string) {
if (selectMode) return;
longPressFired = false;
longPressTimer = setTimeout(() => {
longPressFired = true;
enterSelectMode(slug);
}, 500);
}
function onPointerUp() {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
}
function onPointerCancel() {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
}
// Prevent navigation click if long-press just fired
// Card click: in selection mode, toggle selection; otherwise navigate normally
function onCardClick(e: MouseEvent, slug: string) {
if (selectMode) {
e.preventDefault();
toggleSelect(slug);
return;
}
if (longPressFired) {
e.preventDefault();
longPressFired = false;
}
}
@@ -175,6 +138,14 @@
>
Cancel
</button>
{:else if data.books?.length}
<button
type="button"
onclick={() => { selectMode = true; }}
class="text-sm text-(--color-muted) hover:text-(--color-text) transition-colors pt-1"
>
Select
</button>
{/if}
</div>
@@ -228,9 +199,6 @@
<a
href="/books/{book.slug}"
onclick={(e) => onCardClick(e, book.slug)}
onpointerdown={() => onPointerDown(book.slug)}
onpointerup={onPointerUp}
onpointercancel={onPointerCancel}
draggable="false"
class="group relative flex flex-col rounded-lg overflow-hidden bg-(--color-surface-2) border transition-colors select-none
{isSelected

View File

@@ -60,10 +60,17 @@
try { return JSON.parse(genres) as string[]; } catch { return []; }
}
// Resolved books from streamed promise (populated in {#await} block via binding trick)
// Resolved books from streamed promises — populated via $effect once promises settle
let resolvedBooks = $state<Book[]>([]);
let resolvedVotedBooks = $state<VotedBook[]>([]);
$effect(() => {
Promise.all([data.streamed.books, data.streamed.votedBooks]).then(([books, vb]) => {
resolvedBooks = books as Book[];
resolvedVotedBooks = vb as VotedBook[];
});
});
let deck = $derived.by(() => {
let books = resolvedBooks;
if (prefs.onboarded && prefs.genres.length > 0) {
@@ -479,12 +486,6 @@
</div>
{/if}
<!-- ── Await streamed data ─────────────────────────────────────────────────────── -->
{#await Promise.all([data.streamed.books, data.streamed.votedBooks]) then [books, vb]}
<!-- Silently populate reactive state once data resolves -->
{@const _ = (() => { resolvedBooks = books as Book[]; resolvedVotedBooks = vb as VotedBook[]; return ''; })()}
{/await}
<!-- ── Page layout ────────────────────────────────────────────────────────────── -->
<div class="select-none -mx-4 -my-8 lg:min-h-[calc(100svh-3.5rem)]
lg:grid lg:grid-cols-[1fr_380px] xl:grid-cols-[1fr_420px]">