|
|
|
|
@@ -36,6 +36,7 @@
|
|
|
|
|
let prefs = $state<Prefs>(loadPrefs());
|
|
|
|
|
// svelte-ignore state_referenced_locally
|
|
|
|
|
let showOnboarding = $state(!prefs.onboarded);
|
|
|
|
|
let isEditingPrefs = $state(false);
|
|
|
|
|
|
|
|
|
|
// Onboarding temp state
|
|
|
|
|
// svelte-ignore state_referenced_locally
|
|
|
|
|
@@ -46,11 +47,10 @@
|
|
|
|
|
function finishOnboarding(skip = false) {
|
|
|
|
|
if (!skip) {
|
|
|
|
|
prefs = { genres: tempGenres, status: tempStatus, onboarded: true };
|
|
|
|
|
} else {
|
|
|
|
|
prefs = { ...prefs, onboarded: true };
|
|
|
|
|
savePrefs(prefs);
|
|
|
|
|
}
|
|
|
|
|
savePrefs(prefs);
|
|
|
|
|
showOnboarding = false;
|
|
|
|
|
isEditingPrefs = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Book deck (client-side filtered) ───────────────────────────────────────
|
|
|
|
|
@@ -60,6 +60,14 @@
|
|
|
|
|
try { return JSON.parse(genres) as string[]; } catch { return []; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cleanSummary(text: string): string {
|
|
|
|
|
return text.replace(/^summary\s*/i, '').trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function capitalizeFirst(s: string): string {
|
|
|
|
|
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolved books from streamed promises — populated via $effect once promises settle
|
|
|
|
|
let resolvedBooks = $state<Book[]>([]);
|
|
|
|
|
let resolvedVotedBooks = $state<VotedBook[]>([]);
|
|
|
|
|
@@ -237,6 +245,8 @@
|
|
|
|
|
read_now: { x: 30, y: -1300 },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let votedToastTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
|
|
|
|
|
async function doAction(action: VoteAction) {
|
|
|
|
|
if (animating || !currentBook) return;
|
|
|
|
|
animating = true;
|
|
|
|
|
@@ -271,6 +281,10 @@
|
|
|
|
|
animating = false;
|
|
|
|
|
showPreview = false;
|
|
|
|
|
|
|
|
|
|
// Auto-clear toast after 4 s
|
|
|
|
|
if (votedToastTimer) clearTimeout(votedToastTimer);
|
|
|
|
|
votedToastTimer = setTimeout(() => { voted = null; }, 4000);
|
|
|
|
|
|
|
|
|
|
if (action === 'read_now') {
|
|
|
|
|
goto(`/books/${book.slug}`);
|
|
|
|
|
} else {
|
|
|
|
|
@@ -278,6 +292,16 @@
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function undoLast() {
|
|
|
|
|
if (!voted || animating) return;
|
|
|
|
|
const { slug } = voted;
|
|
|
|
|
voted = null;
|
|
|
|
|
if (votedToastTimer) { clearTimeout(votedToastTimer); votedToastTimer = null; }
|
|
|
|
|
votedBooks = votedBooks.filter((v) => v.slug !== slug);
|
|
|
|
|
if (idx > 0) idx--;
|
|
|
|
|
await fetch(`/api/discover/vote?slug=${encodeURIComponent(slug)}`, { method: 'DELETE' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function resetDeck() {
|
|
|
|
|
await fetch('/api/discover/vote', { method: 'DELETE' });
|
|
|
|
|
votedBooks = [];
|
|
|
|
|
@@ -301,6 +325,10 @@
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<svelte:head>
|
|
|
|
|
<title>Discover — libnovel</title>
|
|
|
|
|
</svelte:head>
|
|
|
|
|
|
|
|
|
|
<!-- ── Onboarding modal ───────────────────────────────────────────────────────── -->
|
|
|
|
|
{#if showOnboarding}
|
|
|
|
|
<div class="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
|
|
|
|
@@ -349,11 +377,11 @@
|
|
|
|
|
<div class="flex gap-3">
|
|
|
|
|
<button type="button" onclick={() => finishOnboarding(true)}
|
|
|
|
|
class="flex-1 py-2.5 rounded-xl text-sm font-medium text-(--color-muted) hover:text-(--color-text) transition-colors">
|
|
|
|
|
Skip
|
|
|
|
|
{isEditingPrefs ? 'Cancel' : 'Skip'}
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" onclick={() => finishOnboarding(false)}
|
|
|
|
|
class="flex-[2] py-2.5 rounded-xl text-sm font-bold bg-(--color-brand) text-(--color-surface) hover:bg-(--color-brand-dim) transition-colors">
|
|
|
|
|
Start Discovering
|
|
|
|
|
{isEditingPrefs ? 'Save Preferences' : 'Start Discovering'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -395,11 +423,11 @@
|
|
|
|
|
<p class="text-sm text-(--color-muted) mb-3">{previewBook.author}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if previewBook.summary}
|
|
|
|
|
<p class="text-sm text-(--color-muted) leading-relaxed line-clamp-5 mb-4">{previewBook.summary}</p>
|
|
|
|
|
<p class="text-sm text-(--color-muted) leading-relaxed line-clamp-5 mb-4">{cleanSummary(previewBook.summary)}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
<div class="flex flex-wrap gap-2 mb-5">
|
|
|
|
|
{#each parseBookGenres(previewBook.genres).slice(0, 4) as genre}
|
|
|
|
|
<span class="text-xs px-2 py-0.5 rounded-full bg-(--color-surface-3) text-(--color-muted)">{genre}</span>
|
|
|
|
|
<span class="text-xs px-2 py-0.5 rounded-full bg-(--color-surface-3) text-(--color-muted)">{capitalizeFirst(genre)}</span>
|
|
|
|
|
{/each}
|
|
|
|
|
{#if previewBook.status}
|
|
|
|
|
<span class="text-xs px-2 py-0.5 rounded-full bg-(--color-surface-3) text-(--color-text)">{previewBook.status}</span>
|
|
|
|
|
@@ -486,6 +514,37 @@
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- ── Voted toast ─────────────────────────────────────────────────────────────── -->
|
|
|
|
|
{#if voted}
|
|
|
|
|
{@const toastBg = voted.action === 'like' ? 'bg-green-500/15 border-green-500/30' : voted.action === 'read_now' ? 'bg-blue-500/15 border-blue-500/30' : 'bg-(--color-surface-2) border-(--color-border)'}
|
|
|
|
|
{@const toastColor = voted.action === 'like' ? 'text-green-400' : voted.action === 'read_now' ? 'text-blue-400' : 'text-(--color-muted)'}
|
|
|
|
|
<div class="fixed bottom-24 left-1/2 -translate-x-1/2 z-30 flex items-center gap-3 px-4 py-3
|
|
|
|
|
rounded-2xl {toastBg} border shadow-xl text-sm whitespace-nowrap pointer-events-auto animate-in slide-in-from-bottom-2 duration-200">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
{#if voted.action === 'like'}
|
|
|
|
|
<svg class="w-4 h-4 {toastColor}" 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>
|
|
|
|
|
{:else if voted.action === 'read_now'}
|
|
|
|
|
<svg class="w-4 h-4 {toastColor}" fill="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path d="M8 5v14l11-7z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
{:else}
|
|
|
|
|
<svg class="w-4 h-4 {toastColor}" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
|
|
|
|
</svg>
|
|
|
|
|
{/if}
|
|
|
|
|
<span class="font-semibold {toastColor}">
|
|
|
|
|
{voted.action === 'like' ? 'Added to Library' : voted.action === 'read_now' ? 'Opening Book...' : 'Skipped'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button type="button" onclick={undoLast}
|
|
|
|
|
class="px-2 py-1 rounded-lg text-xs font-bold text-(--color-text) hover:bg-(--color-surface-3)/50 transition-colors">
|
|
|
|
|
Undo
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- ── 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]">
|
|
|
|
|
@@ -496,34 +555,43 @@
|
|
|
|
|
min-h-[calc(100svh-3.5rem)] lg:border-r lg:border-(--color-border)">
|
|
|
|
|
|
|
|
|
|
<!-- Header row -->
|
|
|
|
|
<div class="w-full max-w-sm lg:max-w-none flex items-center justify-between mb-4 shrink-0">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="text-xl font-bold text-(--color-text)">Discover</h1>
|
|
|
|
|
{#if !loading && !deckEmpty}
|
|
|
|
|
<p class="text-xs text-(--color-muted)">{totalRemaining} books left</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-1">
|
|
|
|
|
<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>
|
|
|
|
|
<div class="w-full max-w-sm lg:max-w-none flex flex-col gap-3 mb-4 shrink-0">
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="text-xl font-bold text-(--color-text)">Discover</h1>
|
|
|
|
|
{#if !loading && !deckEmpty}
|
|
|
|
|
<p class="text-xs text-(--color-muted)">{totalRemaining} of {deck.length} books remaining</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</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 class="flex items-center gap-1">
|
|
|
|
|
<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>
|
|
|
|
|
<button type="button"
|
|
|
|
|
onclick={() => { showOnboarding = true; isEditingPrefs = 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>
|
|
|
|
|
<!-- Progress bar -->
|
|
|
|
|
{#if !loading && !deckEmpty && deck.length > 0}
|
|
|
|
|
<div class="w-full h-1.5 bg-(--color-surface-2) rounded-full overflow-hidden">
|
|
|
|
|
<div class="h-full bg-(--color-brand) transition-all duration-500 ease-out rounded-full"
|
|
|
|
|
style="width: {((idx / deck.length) * 100).toFixed(1)}%"></div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if loading}
|
|
|
|
|
@@ -539,23 +607,41 @@
|
|
|
|
|
|
|
|
|
|
{:else if deckEmpty}
|
|
|
|
|
<!-- ── Empty state ───────────────────────────────────────────────── -->
|
|
|
|
|
<div class="flex-1 flex flex-col items-center justify-center gap-6 text-center max-w-xs">
|
|
|
|
|
<div class="w-20 h-20 rounded-full bg-(--color-surface-2) flex items-center justify-center text-4xl">📚</div>
|
|
|
|
|
<div class="flex-1 flex flex-col items-center justify-center gap-6 text-center max-w-sm px-4">
|
|
|
|
|
<div class="relative">
|
|
|
|
|
<div class="w-24 h-24 rounded-full bg-(--color-brand)/10 flex items-center justify-center text-5xl border-4 border-(--color-brand)/20">
|
|
|
|
|
🎉
|
|
|
|
|
</div>
|
|
|
|
|
<div class="absolute -top-1 -right-1 w-8 h-8 rounded-full bg-green-500/20 border-2 border-green-500/40 flex items-center justify-center">
|
|
|
|
|
<svg class="w-4 h-4 text-green-400" fill="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="text-lg font-bold text-(--color-text) mb-2">All caught up!</h2>
|
|
|
|
|
<p class="text-sm text-(--color-muted)">
|
|
|
|
|
You've seen all available books.
|
|
|
|
|
{#if prefs.genres.length > 0}Try adjusting your preferences to see more.
|
|
|
|
|
{:else}Check your library for books you liked.{/if}
|
|
|
|
|
<h2 class="text-xl font-bold text-(--color-text) mb-2">All Caught Up!</h2>
|
|
|
|
|
<p class="text-sm text-(--color-muted) leading-relaxed">
|
|
|
|
|
You've explored all {deck.length} books in your discover queue.
|
|
|
|
|
{#if prefs.genres.length > 0}
|
|
|
|
|
<br />Try adjusting your genre preferences to discover more.
|
|
|
|
|
{:else}
|
|
|
|
|
<br />Set your preferences to get personalized recommendations.
|
|
|
|
|
{/if}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex flex-col gap-2 w-full">
|
|
|
|
|
<a href="/books" class="py-2.5 rounded-xl text-sm font-bold bg-(--color-brand) text-(--color-surface) text-center hover:bg-(--color-brand-dim) transition-colors">
|
|
|
|
|
My Library
|
|
|
|
|
</a>
|
|
|
|
|
<div class="flex flex-col gap-3 w-full">
|
|
|
|
|
{#if votedBooks.filter(v => v.action === 'like' || v.action === 'read_now').length > 0}
|
|
|
|
|
<a href="/books" class="py-3 rounded-xl text-sm font-bold bg-(--color-brand) text-(--color-surface) text-center hover:bg-(--color-brand-dim) transition-colors shadow-lg shadow-(--color-brand)/20">
|
|
|
|
|
View My Library
|
|
|
|
|
</a>
|
|
|
|
|
{/if}
|
|
|
|
|
<button type="button" onclick={() => { showOnboarding = true; isEditingPrefs = true; tempGenres = [...prefs.genres]; tempStatus = prefs.status; }}
|
|
|
|
|
class="py-3 rounded-xl text-sm font-semibold bg-(--color-surface-2) text-(--color-text) hover:bg-(--color-surface-3) transition-colors border border-(--color-border)">
|
|
|
|
|
Change Preferences
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" onclick={resetDeck}
|
|
|
|
|
class="py-2.5 rounded-xl text-sm font-medium bg-(--color-surface-2) text-(--color-muted) hover:text-(--color-text) transition-colors">
|
|
|
|
|
Start over
|
|
|
|
|
class="py-2 rounded-xl text-sm font-medium text-(--color-muted) hover:text-(--color-text) transition-colors">
|
|
|
|
|
Start Over
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -619,7 +705,7 @@
|
|
|
|
|
{#if book.author}<p class="text-white/70 text-sm mb-2">{book.author}</p>{/if}
|
|
|
|
|
<div class="flex flex-wrap gap-1.5 items-center">
|
|
|
|
|
{#each parseBookGenres(book.genres).slice(0, 2) as genre}
|
|
|
|
|
<span class="text-xs bg-white/15 text-white/90 px-2 py-0.5 rounded-full backdrop-blur-sm">{genre}</span>
|
|
|
|
|
<span class="text-xs bg-white/15 text-white/90 px-2 py-0.5 rounded-full backdrop-blur-sm">{capitalizeFirst(genre)}</span>
|
|
|
|
|
{/each}
|
|
|
|
|
{#if book.status}
|
|
|
|
|
<span class="text-xs bg-white/10 text-white/60 px-2 py-0.5 rounded-full">{book.status}</span>
|
|
|
|
|
@@ -674,8 +760,27 @@
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Swipe hint (mobile only, shown while cards remain) -->
|
|
|
|
|
<p class="lg:hidden text-xs text-(--color-muted)/60 mt-2 shrink-0 text-center">Swipe ← skip · swipe → like · tap for details</p>
|
|
|
|
|
<!-- Keyboard hint (desktop only) -->
|
|
|
|
|
<p class="hidden lg:block text-xs text-(--color-muted)/40 mt-2 shrink-0">← Skip · ↑ Read now · → Like · Space for details</p>
|
|
|
|
|
<div class="hidden lg:flex items-center justify-center gap-4 mt-3 shrink-0">
|
|
|
|
|
<div class="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-(--color-surface-2)/50 border border-(--color-border)/30">
|
|
|
|
|
<kbd class="px-1.5 py-0.5 rounded text-[10px] font-bold bg-(--color-surface-3) text-(--color-muted) border border-(--color-border)">←</kbd>
|
|
|
|
|
<span class="text-xs text-(--color-muted)">Skip</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-(--color-surface-2)/50 border border-(--color-border)/30">
|
|
|
|
|
<kbd class="px-1.5 py-0.5 rounded text-[10px] font-bold bg-(--color-surface-3) text-(--color-muted) border border-(--color-border)">↑</kbd>
|
|
|
|
|
<span class="text-xs text-(--color-muted)">Read</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-(--color-surface-2)/50 border border-(--color-border)/30">
|
|
|
|
|
<kbd class="px-1.5 py-0.5 rounded text-[10px] font-bold bg-(--color-surface-3) text-(--color-muted) border border-(--color-border)">→</kbd>
|
|
|
|
|
<span class="text-xs text-(--color-muted)">Like</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-(--color-surface-2)/50 border border-(--color-border)/30">
|
|
|
|
|
<kbd class="px-1.5 py-0.5 rounded text-[10px] font-bold bg-(--color-surface-3) text-(--color-muted) border border-(--color-border)">Space</kbd>
|
|
|
|
|
<span class="text-xs text-(--color-muted)">Details</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@@ -711,7 +816,7 @@
|
|
|
|
|
<!-- Metadata pills -->
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
{#each parseBookGenres(book.genres).slice(0, 5) as genre}
|
|
|
|
|
<span class="text-xs px-2.5 py-1 rounded-full bg-(--color-surface-3) text-(--color-muted)">{genre}</span>
|
|
|
|
|
<span class="text-xs px-2.5 py-1 rounded-full bg-(--color-surface-3) text-(--color-muted)">{capitalizeFirst(genre)}</span>
|
|
|
|
|
{/each}
|
|
|
|
|
{#if book.status}
|
|
|
|
|
<span class="text-xs px-2.5 py-1 rounded-full bg-(--color-surface-3) text-(--color-text) font-medium">{book.status}</span>
|
|
|
|
|
@@ -735,7 +840,7 @@
|
|
|
|
|
{#if book.summary}
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-xs font-semibold text-(--color-muted) uppercase tracking-wider mb-2">Summary</p>
|
|
|
|
|
<p class="text-sm text-(--color-muted) leading-relaxed">{book.summary}</p>
|
|
|
|
|
<p class="text-sm text-(--color-muted) leading-relaxed">{cleanSummary(book.summary)}</p>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
|