From 5c2c9b1b67e11f3223e417993e8a39dd866e2d49 Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 6 Apr 2026 00:16:39 +0500 Subject: [PATCH] feat(home): hero carousel with auto-advance, arrows, and dot indicators Cycles through all in-progress books every 6s; prev/next arrow buttons overlay the card edges; active dot stretches to a pill; cover fades in on slide change via {#key} + animate-fade-in; shelf excludes the current hero to avoid duplication. Co-Authored-By: Claude Sonnet 4.6 --- ui/src/app.css | 9 ++ ui/src/routes/+page.svelte | 176 +++++++++++++++++++++++++++---------- 2 files changed, 137 insertions(+), 48 deletions(-) diff --git a/ui/src/app.css b/ui/src/app.css index bbe88a1..ab806d7 100644 --- a/ui/src/app.css +++ b/ui/src/app.css @@ -189,6 +189,15 @@ html { display: none; /* Chrome / Safari / WebKit */ } +/* ── Hero carousel fade ─────────────────────────────────────────────── */ +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} +.animate-fade-in { + animation: fade-in 0.4s ease-out forwards; +} + /* ── Navigation progress bar ───────────────────────────────────────── */ @keyframes progress-bar { 0% { width: 0%; opacity: 1; } diff --git a/ui/src/routes/+page.svelte b/ui/src/routes/+page.svelte index 67c7ee7..60fc35d 100644 --- a/ui/src/routes/+page.svelte +++ b/ui/src/routes/+page.svelte @@ -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 | 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 @@ {m.home_title()} - + {#if heroBook}
-
- - - {#if heroBook.book.cover} - {heroBook.book.title} - {:else} -
- - - -
- {/if} -
+
+ +
+ + + {#if heroBook.book.cover} + {#key heroIndex} + {heroBook.book.title} + {/key} + {:else} +
+ + + +
+ {/if} +
- -
-
-

{m.home_continue_reading()}

-

{heroBook.book.title}

- {#if heroBook.book.author} -

{heroBook.book.author}

- {/if} - {#if heroBook.book.summary} - - {/if} + +
+
+

{m.home_continue_reading()}

+

{heroBook.book.title}

+ {#if heroBook.book.author} +

{heroBook.book.author}

+ {/if} + {#if heroBook.book.summary} + + {/if} +
+
+ + + {m.home_chapter_badge({ n: String(heroBook.chapter) })} + + + {#if heroBook.book.total_chapters > 0 && heroBook.chapter < heroBook.book.total_chapters} + {@const ahead = heroBook.book.total_chapters - heroBook.chapter} + + {/if} + {#each parseGenres(heroBook.book.genres).slice(0, 2) as genre} + {genre} + {/each} +
-
- - - {m.home_chapter_badge({ n: String(heroBook.chapter) })} - + + + {#if heroBooks.length > 1} - {#if heroBook.book.total_chapters > 0 && heroBook.chapter < heroBook.book.total_chapters} - {@const ahead = heroBook.book.total_chapters - heroBook.chapter} - - {/if} - {#each parseGenres(heroBook.book.genres).slice(0, 2) as genre} - {genre} + + {/if} +
+ + + {#if heroBooks.length > 1} +
+ {#each heroBooks as _, i} + {/each}
-
+ {/if}
{/if}