Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b90667b4b |
@@ -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
|
||||
|
||||
@@ -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]">
|
||||
|
||||
Reference in New Issue
Block a user