|
|
|
|
@@ -86,7 +86,7 @@
|
|
|
|
|
let transitioning = $state(false);
|
|
|
|
|
let showPreview = $state(false);
|
|
|
|
|
let voted = $state<{ slug: string; action: string } | null>(null); // last voted, for undo
|
|
|
|
|
let activeTab = $state<'discover' | 'history'>('discover');
|
|
|
|
|
let showHistory = $state(false);
|
|
|
|
|
|
|
|
|
|
// svelte-ignore state_referenced_locally
|
|
|
|
|
let votedBooks = $state<VotedBook[]>(data.votedBooks ?? []);
|
|
|
|
|
@@ -239,6 +239,16 @@
|
|
|
|
|
body: JSON.stringify({ slug: book.slug, action })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Optimistically add/update the history list so the drawer shows it immediately.
|
|
|
|
|
// If this slug was already voted (e.g. swiped twice via undo+re-swipe), replace it.
|
|
|
|
|
const existing = votedBooks.findIndex((v) => v.slug === book.slug);
|
|
|
|
|
const entry: VotedBook = { slug: book.slug, action, votedAt: new Date().toISOString(), book };
|
|
|
|
|
if (existing !== -1) {
|
|
|
|
|
votedBooks = [entry, ...votedBooks.filter((_, i) => i !== existing)];
|
|
|
|
|
} else {
|
|
|
|
|
votedBooks = [entry, ...votedBooks];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fly out
|
|
|
|
|
transitioning = true;
|
|
|
|
|
const target = flyTargets[action];
|
|
|
|
|
@@ -426,49 +436,125 @@
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- ── History drawer ─────────────────────────────────────────────────────────── -->
|
|
|
|
|
{#if showHistory}
|
|
|
|
|
<div
|
|
|
|
|
class="fixed inset-0 z-40 flex items-end sm:items-center justify-center p-4"
|
|
|
|
|
role="presentation"
|
|
|
|
|
onclick={() => (showHistory = false)}
|
|
|
|
|
onkeydown={(e) => { if (e.key === 'Escape') showHistory = false; }}
|
|
|
|
|
>
|
|
|
|
|
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
|
|
|
|
<div
|
|
|
|
|
class="relative w-full max-w-md bg-(--color-surface-2) rounded-2xl border border-(--color-border) shadow-2xl overflow-hidden max-h-[80vh] flex flex-col"
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
onclick={(e) => e.stopPropagation()}
|
|
|
|
|
onkeydown={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<div class="flex items-center justify-between p-5 border-b border-(--color-border) flex-shrink-0">
|
|
|
|
|
<h3 class="font-bold text-(--color-text)">History {#if votedBooks.length}<span class="text-(--color-muted) font-normal text-sm">({votedBooks.length})</span>{/if}</h3>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => (showHistory = false)}
|
|
|
|
|
class="w-8 h-8 flex items-center justify-center rounded-lg text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-3) transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="overflow-y-auto flex-1 p-4 space-y-2">
|
|
|
|
|
{#if !votedBooks.length}
|
|
|
|
|
<p class="text-center text-(--color-muted) text-sm py-12">No votes yet — start swiping!</p>
|
|
|
|
|
{:else}
|
|
|
|
|
{#each votedBooks as v (v.slug)}
|
|
|
|
|
{@const actionColor = v.action === 'like' ? 'text-green-400' : v.action === 'read_now' ? 'text-blue-400' : 'text-(--color-muted)'}
|
|
|
|
|
{@const actionLabel = v.action === 'like' ? 'Liked' : v.action === 'read_now' ? 'Read Now' : v.action === 'skip' ? 'Skipped' : 'Noped'}
|
|
|
|
|
<div class="flex items-center gap-3 bg-(--color-surface-3) rounded-xl p-3">
|
|
|
|
|
{#if v.book?.cover}
|
|
|
|
|
<img src={v.book.cover} alt="" class="w-10 h-14 rounded-md object-cover flex-shrink-0" />
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="w-10 h-14 rounded-md bg-(--color-surface-2) flex-shrink-0"></div>
|
|
|
|
|
{/if}
|
|
|
|
|
<div class="flex-1 min-w-0">
|
|
|
|
|
<a href="/books/{v.slug}" class="text-sm font-semibold text-(--color-text) hover:text-(--color-brand) transition-colors line-clamp-1">
|
|
|
|
|
{v.book?.title ?? v.slug}
|
|
|
|
|
</a>
|
|
|
|
|
{#if v.book?.author}
|
|
|
|
|
<p class="text-xs text-(--color-muted) truncate">{v.book.author}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
<span class="text-xs font-medium {actionColor}">{actionLabel}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => undoVote(v.slug)}
|
|
|
|
|
title="Undo"
|
|
|
|
|
class="w-8 h-8 flex items-center justify-center rounded-lg text-(--color-muted) hover:text-(--color-danger) hover:bg-(--color-danger)/10 transition-colors flex-shrink-0"
|
|
|
|
|
aria-label="Undo vote for {v.book?.title ?? v.slug}"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{/each}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={resetDeck}
|
|
|
|
|
class="w-full py-2 rounded-xl text-sm text-(--color-muted) hover:text-(--color-text) transition-colors mt-2"
|
|
|
|
|
>
|
|
|
|
|
Clear all history
|
|
|
|
|
</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- ── Main layout ────────────────────────────────────────────────────────────── -->
|
|
|
|
|
<div class="min-h-screen bg-(--color-surface) flex flex-col items-center px-4 pt-8 pb-6 select-none">
|
|
|
|
|
<div class="min-h-screen bg-(--color-surface) flex flex-col items-center px-3 pt-6 pb-6 select-none">
|
|
|
|
|
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="w-full max-w-sm flex items-center justify-between mb-6">
|
|
|
|
|
<div class="w-full max-w-sm flex items-center justify-between mb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="text-xl font-bold text-(--color-text)">Discover</h1>
|
|
|
|
|
{#if !deckEmpty}
|
|
|
|
|
<p class="text-xs text-(--color-muted)">{totalRemaining} books left</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => { showOnboarding = true; tempGenres = [...prefs.genres]; tempStatus = prefs.status; }}
|
|
|
|
|
title="Preferences"
|
|
|
|
|
class="w-8 h-8 flex items-center justify-center rounded-lg text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
<div class="flex items-center gap-1">
|
|
|
|
|
<!-- History button -->
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => (showHistory = true)}
|
|
|
|
|
title="History"
|
|
|
|
|
class="relative w-9 h-9 flex items-center justify-center rounded-lg text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
{#if votedBooks.length}
|
|
|
|
|
<span class="absolute -top-0.5 -right-0.5 w-4 h-4 rounded-full bg-(--color-brand) text-(--color-surface) text-[9px] font-bold flex items-center justify-center leading-none">
|
|
|
|
|
{votedBooks.length > 9 ? '9+' : votedBooks.length}
|
|
|
|
|
</span>
|
|
|
|
|
{/if}
|
|
|
|
|
</button>
|
|
|
|
|
<!-- Preferences button -->
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => { showOnboarding = true; tempGenres = [...prefs.genres]; tempStatus = prefs.status; }}
|
|
|
|
|
title="Preferences"
|
|
|
|
|
class="w-9 h-9 flex items-center justify-center rounded-lg text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Tab switcher -->
|
|
|
|
|
<div class="flex gap-1 bg-(--color-surface-2) rounded-xl p-1 w-full max-w-sm border border-(--color-border) mb-4">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => (activeTab = 'discover')}
|
|
|
|
|
class="flex-1 py-2 rounded-lg text-sm font-medium transition-colors
|
|
|
|
|
{activeTab === 'discover' ? 'bg-(--color-surface-3) text-(--color-text)' : 'text-(--color-muted) hover:text-(--color-text)'}"
|
|
|
|
|
>
|
|
|
|
|
Discover
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => (activeTab = 'history')}
|
|
|
|
|
class="flex-1 py-2 rounded-lg text-sm font-medium transition-colors
|
|
|
|
|
{activeTab === 'history' ? 'bg-(--color-surface-3) text-(--color-text)' : 'text-(--color-muted) hover:text-(--color-text)'}"
|
|
|
|
|
>
|
|
|
|
|
History {#if votedBooks.length}({votedBooks.length}){/if}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if activeTab === 'discover'}
|
|
|
|
|
{#if deckEmpty}
|
|
|
|
|
<!-- Empty state -->
|
|
|
|
|
<div class="flex-1 flex flex-col items-center justify-center gap-6 text-center max-w-xs">
|
|
|
|
|
@@ -501,8 +587,9 @@
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
{@const book = currentBook!}
|
|
|
|
|
<!-- Card stack -->
|
|
|
|
|
<div class="w-full max-w-sm relative" style="aspect-ratio: 3/4.2;">
|
|
|
|
|
|
|
|
|
|
<!-- Card stack — fills available width, taller ratio -->
|
|
|
|
|
<div class="w-full max-w-sm relative" style="aspect-ratio: 3/4.6;">
|
|
|
|
|
|
|
|
|
|
<!-- Back card 2 -->
|
|
|
|
|
{#if nextNextBook}
|
|
|
|
|
@@ -561,9 +648,9 @@
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- Bottom gradient + info -->
|
|
|
|
|
<div class="absolute inset-0 bg-gradient-to-t from-black/85 via-black/25 to-transparent pointer-events-none"></div>
|
|
|
|
|
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent pointer-events-none"></div>
|
|
|
|
|
<div class="absolute bottom-0 left-0 right-0 p-5 pointer-events-none">
|
|
|
|
|
<h2 class="text-white font-bold text-xl leading-snug line-clamp-2 mb-1">{book.title}</h2>
|
|
|
|
|
<h2 class="text-white font-bold text-2xl leading-snug line-clamp-2 mb-1">{book.title}</h2>
|
|
|
|
|
{#if book.author}
|
|
|
|
|
<p class="text-white/70 text-sm mb-2">{book.author}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
@@ -582,164 +669,91 @@
|
|
|
|
|
|
|
|
|
|
<!-- LIKE indicator (right swipe) -->
|
|
|
|
|
<div
|
|
|
|
|
class="absolute top-8 right-6 px-3 py-1.5 rounded-lg border-2 border-green-400 rotate-[-15deg] pointer-events-none"
|
|
|
|
|
class="absolute top-8 right-6 px-4 py-2 rounded-xl border-[3px] border-green-400 rotate-[-15deg] pointer-events-none bg-green-400/10"
|
|
|
|
|
style="opacity: {indicator === 'like' ? indicatorOpacity : 0}; transition: opacity 0.1s;"
|
|
|
|
|
>
|
|
|
|
|
<span class="text-green-400 font-black text-lg tracking-widest">LIKE</span>
|
|
|
|
|
<span class="text-green-400 font-black text-2xl tracking-widest">LIKE</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- SKIP indicator (left swipe) -->
|
|
|
|
|
<div
|
|
|
|
|
class="absolute top-8 left-6 px-3 py-1.5 rounded-lg border-2 border-red-400 rotate-[15deg] pointer-events-none"
|
|
|
|
|
class="absolute top-8 left-6 px-4 py-2 rounded-xl border-[3px] border-red-400 rotate-[15deg] pointer-events-none bg-red-400/10"
|
|
|
|
|
style="opacity: {indicator === 'skip' ? indicatorOpacity : 0}; transition: opacity 0.1s;"
|
|
|
|
|
>
|
|
|
|
|
<span class="text-red-400 font-black text-lg tracking-widest">SKIP</span>
|
|
|
|
|
<span class="text-red-400 font-black text-2xl tracking-widest">SKIP</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- READ NOW indicator (swipe up) -->
|
|
|
|
|
<div
|
|
|
|
|
class="absolute top-8 left-1/2 -translate-x-1/2 px-3 py-1.5 rounded-lg border-2 border-blue-400 pointer-events-none"
|
|
|
|
|
class="absolute top-8 left-1/2 -translate-x-1/2 px-4 py-2 rounded-xl border-[3px] border-blue-400 pointer-events-none bg-blue-400/10"
|
|
|
|
|
style="opacity: {indicator === 'read_now' ? indicatorOpacity : 0}; transition: opacity 0.1s;"
|
|
|
|
|
>
|
|
|
|
|
<span class="text-blue-400 font-black text-lg tracking-widest">READ NOW</span>
|
|
|
|
|
<span class="text-blue-400 font-black text-2xl tracking-widest">READ NOW</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- NOPE indicator (swipe down) -->
|
|
|
|
|
<div
|
|
|
|
|
class="absolute bottom-28 left-1/2 -translate-x-1/2 px-3 py-1.5 rounded-lg border-2 border-(--color-muted) pointer-events-none"
|
|
|
|
|
class="absolute bottom-32 left-1/2 -translate-x-1/2 px-4 py-2 rounded-xl border-[3px] border-zinc-400 pointer-events-none bg-black/20"
|
|
|
|
|
style="opacity: {indicator === 'nope' ? indicatorOpacity : 0}; transition: opacity 0.1s;"
|
|
|
|
|
>
|
|
|
|
|
<span class="text-(--color-muted) font-black text-lg tracking-widest">NOPE</span>
|
|
|
|
|
<span class="text-zinc-300 font-black text-2xl tracking-widest">NOPE</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Action buttons -->
|
|
|
|
|
<div class="w-full max-w-sm flex items-center justify-center gap-4 mt-6">
|
|
|
|
|
<!-- Skip (left) -->
|
|
|
|
|
<!-- Action buttons — 3 prominent labeled buttons -->
|
|
|
|
|
<div class="w-full max-w-sm flex items-stretch gap-3 mt-5">
|
|
|
|
|
<!-- Skip -->
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => doAction('skip')}
|
|
|
|
|
disabled={animating}
|
|
|
|
|
title="Skip"
|
|
|
|
|
class="w-14 h-14 rounded-full bg-(--color-surface-2) border border-(--color-border) flex items-center justify-center text-red-400 hover:bg-red-400/10 hover:border-red-400/40 hover:scale-105 active:scale-95 transition-all disabled:opacity-40"
|
|
|
|
|
class="flex-1 flex flex-col items-center justify-center gap-1.5 py-3.5 rounded-2xl
|
|
|
|
|
bg-red-500/15 border border-red-500/30 text-red-400
|
|
|
|
|
hover:bg-red-500/25 hover:border-red-500/50
|
|
|
|
|
active:scale-95 transition-all disabled:opacity-40"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="text-xs font-bold tracking-wide">Skip</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Read Now (up) -->
|
|
|
|
|
<!-- Read Now — center, most prominent -->
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => doAction('read_now')}
|
|
|
|
|
disabled={animating}
|
|
|
|
|
title="Read Now"
|
|
|
|
|
class="w-12 h-12 rounded-full bg-(--color-surface-2) border border-(--color-border) flex items-center justify-center text-blue-400 hover:bg-blue-400/10 hover:border-blue-400/40 hover:scale-105 active:scale-95 transition-all disabled:opacity-40"
|
|
|
|
|
class="flex-[1.4] flex flex-col items-center justify-center gap-1.5 py-3.5 rounded-2xl
|
|
|
|
|
bg-blue-500 text-white
|
|
|
|
|
hover:bg-blue-400
|
|
|
|
|
active:scale-95 transition-all disabled:opacity-40 shadow-lg shadow-blue-500/25"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path d="M8 5v14l11-7z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="text-xs font-bold tracking-wide">Read Now</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Preview (center) -->
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => (showPreview = true)}
|
|
|
|
|
disabled={animating}
|
|
|
|
|
title="Details"
|
|
|
|
|
class="w-10 h-10 rounded-full bg-(--color-surface-2) border border-(--color-border) flex items-center justify-center text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-3) hover:scale-105 active:scale-95 transition-all disabled:opacity-40"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Like (right) -->
|
|
|
|
|
<!-- Like -->
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => doAction('like')}
|
|
|
|
|
disabled={animating}
|
|
|
|
|
title="Add to Library"
|
|
|
|
|
class="w-14 h-14 rounded-full bg-(--color-surface-2) border border-(--color-border) flex items-center justify-center text-(--color-success) hover:bg-green-400/10 hover:border-green-400/40 hover:scale-105 active:scale-95 transition-all disabled:opacity-40"
|
|
|
|
|
class="flex-1 flex flex-col items-center justify-center gap-1.5 py-3.5 rounded-2xl
|
|
|
|
|
bg-green-500/15 border border-green-500/30 text-green-400
|
|
|
|
|
hover:bg-green-500/25 hover:border-green-500/50
|
|
|
|
|
active:scale-95 transition-all disabled:opacity-40"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Nope (down) -->
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => doAction('nope')}
|
|
|
|
|
disabled={animating}
|
|
|
|
|
title="Never show again"
|
|
|
|
|
class="w-12 h-12 rounded-full bg-(--color-surface-2) border border-(--color-border) flex items-center justify-center text-(--color-muted) hover:text-(--color-muted)/60 hover:bg-(--color-surface-3) hover:scale-105 active:scale-95 transition-all disabled:opacity-40"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="text-xs font-bold tracking-wide">Like</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Swipe hint (shown briefly) -->
|
|
|
|
|
<p class="mt-4 text-xs text-(--color-muted)/50 text-center">
|
|
|
|
|
Swipe or tap buttons · Tap card for details
|
|
|
|
|
</p>
|
|
|
|
|
{/if}
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if activeTab === 'history'}
|
|
|
|
|
<div class="w-full max-w-sm space-y-2">
|
|
|
|
|
{#if !votedBooks.length}
|
|
|
|
|
<p class="text-center text-(--color-muted) text-sm py-12">No votes yet — start swiping!</p>
|
|
|
|
|
{:else}
|
|
|
|
|
{#each votedBooks as v (v.slug)}
|
|
|
|
|
{@const actionColor = v.action === 'like' ? 'text-green-400' : v.action === 'read_now' ? 'text-blue-400' : 'text-(--color-muted)'}
|
|
|
|
|
{@const actionLabel = v.action === 'like' ? 'Liked' : v.action === 'read_now' ? 'Read Now' : v.action === 'skip' ? 'Skipped' : 'Noped'}
|
|
|
|
|
<div class="flex items-center gap-3 bg-(--color-surface-2) rounded-xl border border-(--color-border) p-3">
|
|
|
|
|
<!-- Cover thumbnail -->
|
|
|
|
|
{#if v.book?.cover}
|
|
|
|
|
<img src={v.book.cover} alt="" class="w-10 h-14 rounded-md object-cover flex-shrink-0" />
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="w-10 h-14 rounded-md bg-(--color-surface-3) flex-shrink-0"></div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- Info -->
|
|
|
|
|
<div class="flex-1 min-w-0">
|
|
|
|
|
<a href="/books/{v.slug}" class="text-sm font-semibold text-(--color-text) hover:text-(--color-brand) transition-colors line-clamp-1">
|
|
|
|
|
{v.book?.title ?? v.slug}
|
|
|
|
|
</a>
|
|
|
|
|
{#if v.book?.author}
|
|
|
|
|
<p class="text-xs text-(--color-muted) truncate">{v.book.author}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
<span class="text-xs font-medium {actionColor}">{actionLabel}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Undo button -->
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => undoVote(v.slug)}
|
|
|
|
|
title="Undo"
|
|
|
|
|
class="w-8 h-8 flex items-center justify-center rounded-lg text-(--color-muted) hover:text-(--color-danger) hover:bg-(--color-danger)/10 transition-colors flex-shrink-0"
|
|
|
|
|
aria-label="Undo vote for {v.book?.title ?? v.slug}"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={resetDeck}
|
|
|
|
|
class="w-full py-2 rounded-xl text-sm text-(--color-muted) hover:text-(--color-text) transition-colors mt-2"
|
|
|
|
|
>
|
|
|
|
|
Clear all history
|
|
|
|
|
</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|