|
|
|
|
@@ -72,9 +72,44 @@
|
|
|
|
|
'Reincarnation', 'Sci-Fi', 'Horror', 'Slice of Life', 'Adventure',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const heroBook = $derived(data.continueInProgress[0] ?? null);
|
|
|
|
|
const shelfBooks = $derived(data.continueInProgress.slice(1));
|
|
|
|
|
const streak = $derived(data.stats.streak ?? 0);
|
|
|
|
|
// ── Hero carousel ────────────────────────────────────────────────────────
|
|
|
|
|
const heroBooks = $derived(data.continueInProgress);
|
|
|
|
|
let heroIndex = $state(0);
|
|
|
|
|
const heroBook = $derived(heroBooks[heroIndex] ?? null);
|
|
|
|
|
// Shelf shows remaining books not in the hero
|
|
|
|
|
const shelfBooks = $derived(
|
|
|
|
|
heroBooks.length > 1 ? heroBooks.filter((_, i) => i !== heroIndex) : []
|
|
|
|
|
);
|
|
|
|
|
const streak = $derived(data.stats.streak ?? 0);
|
|
|
|
|
|
|
|
|
|
function heroPrev() {
|
|
|
|
|
heroIndex = (heroIndex - 1 + heroBooks.length) % heroBooks.length;
|
|
|
|
|
resetAutoAdvance();
|
|
|
|
|
}
|
|
|
|
|
function heroNext() {
|
|
|
|
|
heroIndex = (heroIndex + 1) % heroBooks.length;
|
|
|
|
|
resetAutoAdvance();
|
|
|
|
|
}
|
|
|
|
|
function heroDot(i: number) {
|
|
|
|
|
heroIndex = i;
|
|
|
|
|
resetAutoAdvance();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let autoAdvanceTimer = $state<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
|
|
|
|
|
|
function resetAutoAdvance() {
|
|
|
|
|
if (autoAdvanceTimer) clearInterval(autoAdvanceTimer);
|
|
|
|
|
if (heroBooks.length > 1) {
|
|
|
|
|
autoAdvanceTimer = setInterval(() => {
|
|
|
|
|
heroIndex = (heroIndex + 1) % heroBooks.length;
|
|
|
|
|
}, 6000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
resetAutoAdvance();
|
|
|
|
|
return () => { if (autoAdvanceTimer) clearInterval(autoAdvanceTimer); };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function playChapter(slug: string, chapter: number) {
|
|
|
|
|
audioStore.autoStartChapter = chapter;
|
|
|
|
|
@@ -86,63 +121,108 @@
|
|
|
|
|
<title>{m.home_title()}</title>
|
|
|
|
|
</svelte:head>
|
|
|
|
|
|
|
|
|
|
<!-- ── Hero resume card ──────────────────────────────────────────────────────── -->
|
|
|
|
|
<!-- ── Hero carousel ──────────────────────────────────────────────────────────── -->
|
|
|
|
|
{#if heroBook}
|
|
|
|
|
<section class="mb-6">
|
|
|
|
|
<div class="group relative flex gap-0 rounded-xl overflow-hidden bg-(--color-surface-2) border border-(--color-border) hover:border-(--color-brand)/50 transition-all">
|
|
|
|
|
<!-- Cover -->
|
|
|
|
|
<a href="/books/{heroBook.book.slug}/chapters/{heroBook.chapter}"
|
|
|
|
|
class="w-32 sm:w-44 shrink-0 aspect-[2/3] overflow-hidden block">
|
|
|
|
|
{#if heroBook.book.cover}
|
|
|
|
|
<img src={heroBook.book.cover} alt={heroBook.book.title}
|
|
|
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" loading="eager" />
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="w-full h-full bg-(--color-surface-3) flex items-center justify-center">
|
|
|
|
|
<svg class="w-10 h-10 text-(--color-muted)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</a>
|
|
|
|
|
<div class="relative">
|
|
|
|
|
<!-- Card -->
|
|
|
|
|
<div class="group relative flex gap-0 rounded-xl overflow-hidden bg-(--color-surface-2) border border-(--color-border) hover:border-(--color-brand)/50 transition-all">
|
|
|
|
|
<!-- Cover -->
|
|
|
|
|
<a href="/books/{heroBook.book.slug}/chapters/{heroBook.chapter}"
|
|
|
|
|
class="w-32 sm:w-44 shrink-0 aspect-[2/3] overflow-hidden block">
|
|
|
|
|
{#if heroBook.book.cover}
|
|
|
|
|
{#key heroIndex}
|
|
|
|
|
<img src={heroBook.book.cover} alt={heroBook.book.title}
|
|
|
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 animate-fade-in" loading="eager" />
|
|
|
|
|
{/key}
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="w-full h-full bg-(--color-surface-3) flex items-center justify-center">
|
|
|
|
|
<svg class="w-10 h-10 text-(--color-muted)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<!-- Info -->
|
|
|
|
|
<div class="flex flex-col justify-between p-5 sm:p-7 min-w-0">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-xs font-semibold text-(--color-brand) uppercase tracking-widest mb-2">{m.home_continue_reading()}</p>
|
|
|
|
|
<h2 class="text-xl sm:text-2xl font-bold text-(--color-text) leading-snug line-clamp-2 mb-1">{heroBook.book.title}</h2>
|
|
|
|
|
{#if heroBook.book.author}
|
|
|
|
|
<p class="text-sm text-(--color-muted)">{heroBook.book.author}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if heroBook.book.summary}
|
|
|
|
|
<p class="hidden sm:block text-sm text-(--color-muted) mt-3 line-clamp-2 max-w-prose">{heroBook.book.summary}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
<!-- Info -->
|
|
|
|
|
<div class="flex flex-col justify-between p-5 sm:p-7 min-w-0 flex-1">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-xs font-semibold text-(--color-brand) uppercase tracking-widest mb-2">{m.home_continue_reading()}</p>
|
|
|
|
|
<h2 class="text-xl sm:text-2xl font-bold text-(--color-text) leading-snug line-clamp-2 mb-1">{heroBook.book.title}</h2>
|
|
|
|
|
{#if heroBook.book.author}
|
|
|
|
|
<p class="text-sm text-(--color-muted)">{heroBook.book.author}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if heroBook.book.summary}
|
|
|
|
|
<p class="hidden sm:block text-sm text-(--color-muted) mt-3 line-clamp-2 max-w-prose">{heroBook.book.summary}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-3 mt-4 flex-wrap">
|
|
|
|
|
<a href="/books/{heroBook.book.slug}/chapters/{heroBook.chapter}"
|
|
|
|
|
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg bg-(--color-brand) text-(--color-surface) font-semibold text-sm hover:bg-(--color-brand-dim) transition-colors">
|
|
|
|
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
|
|
|
|
{m.home_chapter_badge({ n: String(heroBook.chapter) })}
|
|
|
|
|
</a>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => playChapter(heroBook!.book.slug, heroBook!.chapter)}
|
|
|
|
|
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg bg-(--color-surface-3) border border-(--color-border) text-(--color-muted) hover:text-(--color-text) hover:border-(--color-brand)/40 font-semibold text-sm transition-colors"
|
|
|
|
|
title="Listen to narration"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 9a3 3 0 114 2.83V17m0 0a2 2 0 11-4 0m4 0H9m9-8a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
Listen
|
|
|
|
|
</button>
|
|
|
|
|
{#if heroBook.book.total_chapters > 0 && heroBook.chapter < heroBook.book.total_chapters}
|
|
|
|
|
{@const ahead = heroBook.book.total_chapters - heroBook.chapter}
|
|
|
|
|
<span class="text-xs text-(--color-muted) hidden sm:inline">{ahead} chapters ahead</span>
|
|
|
|
|
{/if}
|
|
|
|
|
{#each parseGenres(heroBook.book.genres).slice(0, 2) as genre}
|
|
|
|
|
<span class="text-xs px-2 py-1 rounded-full bg-(--color-surface-3) text-(--color-muted)">{genre}</span>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-3 mt-4 flex-wrap">
|
|
|
|
|
<a href="/books/{heroBook.book.slug}/chapters/{heroBook.chapter}"
|
|
|
|
|
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg bg-(--color-brand) text-(--color-surface) font-semibold text-sm hover:bg-(--color-brand-dim) transition-colors">
|
|
|
|
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
|
|
|
|
{m.home_chapter_badge({ n: String(heroBook.chapter) })}
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<!-- Prev / Next arrow buttons (only when multiple books) -->
|
|
|
|
|
{#if heroBooks.length > 1}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => playChapter(heroBook!.book.slug, heroBook!.chapter)}
|
|
|
|
|
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg bg-(--color-surface-3) border border-(--color-border) text-(--color-muted) hover:text-(--color-text) hover:border-(--color-brand)/40 font-semibold text-sm transition-colors"
|
|
|
|
|
title="Listen to narration"
|
|
|
|
|
onclick={heroPrev}
|
|
|
|
|
class="absolute left-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-(--color-surface)/80 border border-(--color-border) text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors flex items-center justify-center backdrop-blur-sm z-10"
|
|
|
|
|
aria-label="Previous book"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 9a3 3 0 114 2.83V17m0 0a2 2 0 11-4 0m4 0H9m9-8a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
|
|
|
</svg>
|
|
|
|
|
Listen
|
|
|
|
|
</button>
|
|
|
|
|
{#if heroBook.book.total_chapters > 0 && heroBook.chapter < heroBook.book.total_chapters}
|
|
|
|
|
{@const ahead = heroBook.book.total_chapters - heroBook.chapter}
|
|
|
|
|
<span class="text-xs text-(--color-muted) hidden sm:inline">{ahead} chapters ahead</span>
|
|
|
|
|
{/if}
|
|
|
|
|
{#each parseGenres(heroBook.book.genres).slice(0, 2) as genre}
|
|
|
|
|
<span class="text-xs px-2 py-1 rounded-full bg-(--color-surface-3) text-(--color-muted)">{genre}</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={heroNext}
|
|
|
|
|
class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-(--color-surface)/80 border border-(--color-border) text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors flex items-center justify-center backdrop-blur-sm z-10"
|
|
|
|
|
aria-label="Next book"
|
|
|
|
|
>
|
|
|
|
|
<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="M9 5l7 7-7 7"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Dot indicators -->
|
|
|
|
|
{#if heroBooks.length > 1}
|
|
|
|
|
<div class="flex items-center justify-center gap-1.5 mt-2.5">
|
|
|
|
|
{#each heroBooks as _, i}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => heroDot(i)}
|
|
|
|
|
aria-label="Go to book {i + 1}"
|
|
|
|
|
class="rounded-full transition-all duration-300 {i === heroIndex
|
|
|
|
|
? 'w-4 h-1.5 bg-(--color-brand)'
|
|
|
|
|
: 'w-1.5 h-1.5 bg-(--color-border) hover:bg-(--color-muted)'}"
|
|
|
|
|
></button>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|