Redesign book detail page: hero with blurred cover bg, CTA hierarchy, collapsible admin panel
Some checks failed
Deploy / Deploy Production (push) Has been skipped
Deploy / Cleanup Preview (push) Has been skipped
Deploy / Deploy Preview (push) Failing after 1s
CI / UI / Build (pull_request) Successful in 16s
CI / Scraper / Lint (pull_request) Successful in 19s
CI / Scraper / Test (pull_request) Successful in 20s
CI / Scraper / Build (pull_request) Successful in 16s

This commit is contained in:
Admin
2026-03-06 19:59:21 +05:00
parent 5d3a1a09ef
commit a72c1f6b52

View File

@@ -148,211 +148,180 @@
rangeScraping = false; rangeScraping = false;
} }
} }
// ── Summary expand/collapse ───────────────────────────────────────────────
let summaryExpanded = $state(false);
// ── Admin panel expand/collapse ───────────────────────────────────────────
let adminOpen = $state(false);
</script> </script>
<svelte:head> <svelte:head>
<title>{data.book.title} — libnovel</title> <title>{data.book.title} — libnovel</title>
</svelte:head> </svelte:head>
<!-- Book header --> <!-- ═══════════════════════════════════════════════════════════════ Hero ══ -->
<div class="flex gap-6 mb-8"> <div class="relative rounded-xl overflow-hidden mb-8">
<!-- Blurred cover background -->
{#if data.book.cover} {#if data.book.cover}
<img <div
src={data.book.cover} class="absolute inset-0 bg-cover bg-center scale-110"
alt={data.book.title} style="background-image: url('{data.book.cover}'); filter: blur(24px); opacity: 0.18;"
class="w-32 sm:w-40 rounded-lg object-cover flex-shrink-0 border border-zinc-700" aria-hidden="true"
/> ></div>
{/if} {/if}
<div class="absolute inset-0 bg-gradient-to-b from-zinc-900/60 to-zinc-900/95 pointer-events-none" aria-hidden="true"></div>
<div class="flex flex-col gap-2 min-w-0"> <div class="relative flex gap-5 sm:gap-8 p-5 sm:p-7">
<div class="flex items-center gap-2 flex-wrap"> <!-- Cover image -->
<h1 class="text-2xl font-bold text-zinc-100 leading-tight">{data.book.title}</h1> {#if data.book.cover}
{#if !data.inLib} <img
<span class="text-xs px-2 py-0.5 rounded-full bg-zinc-700 text-zinc-400 border border-zinc-600 shrink-0" title="This book was fetched live from the source and is not yet in your library"> src={data.book.cover}
not in library alt={data.book.title}
</span> class="w-36 sm:w-48 rounded-lg object-cover flex-shrink-0 border border-zinc-700 shadow-xl self-start"
{/if} />
</div>
{#if data.book.author}
<p class="text-zinc-400 text-sm">{data.book.author}</p>
{/if} {/if}
<div class="flex flex-wrap gap-2 mt-1"> <!-- Meta -->
{#if data.book.status} <div class="flex flex-col gap-2 min-w-0 flex-1">
<span class="text-xs px-2 py-1 rounded bg-zinc-700 text-zinc-300">{data.book.status}</span> <!-- Title + "not in library" badge -->
{/if} <div class="flex items-start gap-2 flex-wrap">
{#each genres as genre} <h1 class="text-2xl sm:text-3xl font-bold text-zinc-100 leading-tight">{data.book.title}</h1>
<span class="text-xs px-2 py-1 rounded bg-zinc-800 text-zinc-400">{genre}</span> {#if !data.inLib}
{/each} <span
</div> class="mt-1 text-xs px-2 py-0.5 rounded-full bg-zinc-700 text-zinc-400 border border-zinc-600 shrink-0"
title="This book was fetched live from the source and is not yet in your library"
>
not in library
</span>
{/if}
</div>
{#if data.book.summary} <!-- Author -->
<p class="text-zinc-400 text-sm leading-relaxed line-clamp-4 mt-1">{data.book.summary}</p> {#if data.book.author}
{/if} <p class="text-zinc-400 text-sm">{data.book.author}</p>
{/if}
<div class="flex gap-3 mt-2 flex-wrap items-center"> <!-- Status + genres -->
{#if data.lastChapter} <div class="flex flex-wrap gap-1.5 mt-0.5">
<a {#if data.book.status}
href="/books/{data.book.slug}/chapters/{data.lastChapter}" <span class="text-xs px-2 py-0.5 rounded bg-zinc-700 text-zinc-300 border border-zinc-600">{data.book.status}</span>
class="px-4 py-2 bg-amber-400 text-zinc-900 font-semibold rounded text-sm hover:bg-amber-300 transition-colors" {/if}
> {#each genres as genre}
Continue ch.{data.lastChapter} <span class="text-xs px-2 py-0.5 rounded bg-zinc-800 text-zinc-400 border border-zinc-700">{genre}</span>
</a> {/each}
{/if} </div>
{#if chapterList.length > 0}
<a <!-- Summary with expand toggle -->
href="/books/{data.book.slug}/chapters/1" {#if data.book.summary}
class="px-4 py-2 bg-zinc-700 text-zinc-100 font-semibold rounded text-sm hover:bg-zinc-600 transition-colors" <div class="mt-1">
> <p class="text-zinc-400 text-sm leading-relaxed {summaryExpanded ? '' : 'line-clamp-3'}">
{data.inLib ? 'Start from ch.1' : 'Preview ch.1'} {data.book.summary}
</a> </p>
{/if} {#if data.book.summary.length > 220}
<!-- Save / unsave button --> <button
{#if data.inLib} onclick={() => (summaryExpanded = !summaryExpanded)}
<button class="text-xs text-amber-400/70 hover:text-amber-400 mt-1 transition-colors"
onclick={toggleSave} >
disabled={saving} {summaryExpanded ? 'Less' : 'More'}
title={saved ? 'Remove from library' : 'Add to library'} </button>
class="flex items-center gap-1.5 px-3 py-2 rounded text-sm font-medium transition-colors disabled:opacity-50
{saved
? 'bg-amber-400/20 text-amber-300 hover:bg-red-500/20 hover:text-red-300 border border-amber-400/30 hover:border-red-400/30'
: 'bg-zinc-700 text-zinc-400 hover:text-zinc-100 hover:bg-zinc-600 border border-zinc-600'}"
>
{#if saving}
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
{:else if saved}
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M5 4a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 20V4z"/>
</svg>
{:else}
<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="M5 4a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 20V4z"/>
</svg>
{/if} {/if}
{saved ? 'Saved' : 'Save'} </div>
</button>
{/if} {/if}
<!-- CTA buttons -->
<div class="flex gap-2 mt-3 flex-wrap items-center">
<!-- Primary: Continue (shown only when there's a last chapter) -->
{#if data.lastChapter}
<a
href="/books/{data.book.slug}/chapters/{data.lastChapter}"
class="px-5 py-2 bg-amber-400 text-zinc-900 font-semibold rounded-lg text-sm hover:bg-amber-300 transition-colors shadow"
>
Continue ch.{data.lastChapter}
</a>
{/if}
<!-- Secondary: Start / Preview ch.1 -->
{#if chapterList.length > 0}
<a
href="/books/{data.book.slug}/chapters/1"
class="px-4 py-2 rounded-lg text-sm font-semibold transition-colors
{data.lastChapter
? 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600'
: 'bg-amber-400 text-zinc-900 hover:bg-amber-300 shadow'}"
>
{data.inLib ? 'Start from ch.1' : 'Preview ch.1'}
</a>
{/if}
<!-- Save: icon-only bookmark button -->
{#if data.inLib}
<button
onclick={toggleSave}
disabled={saving}
title={saved ? 'Remove from library' : 'Add to library'}
class="flex items-center justify-center w-9 h-9 rounded-lg border transition-colors disabled:opacity-50
{saved
? 'bg-amber-400/20 text-amber-300 border-amber-400/30 hover:bg-red-500/20 hover:text-red-300 hover:border-red-400/30'
: 'bg-zinc-700 text-zinc-400 border-zinc-600 hover:bg-zinc-600 hover:text-zinc-100'}"
>
{#if saving}
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
{:else if saved}
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M5 4a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 20V4z"/>
</svg>
{:else}
<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="M5 4a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 20V4z"/>
</svg>
{/if}
</button>
{/if}
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Chapter list --> <!-- ══════════════════════════════════════════════════ Chapter list ══ -->
<div class="mt-4"> <div>
<!-- Header row: title + pagination -->
<div class="flex items-center justify-between mb-3 flex-wrap gap-2"> <div class="flex items-center justify-between mb-3 flex-wrap gap-2">
<h2 class="text-lg font-semibold text-zinc-100"> <h2 class="text-base font-semibold text-zinc-200">
Chapters Chapters
<span class="text-zinc-500 font-normal text-sm ml-1">({chapterList.length})</span> {#if chapterList.length > 0}
<span class="text-zinc-500 font-normal text-sm ml-1">({chapterList.length})</span>
{/if}
</h2> </h2>
<div class="flex items-center gap-3 flex-wrap"> {#if totalPages > 1}
{#if data.isAdmin && data.book.source_url} <div class="flex gap-2 items-center text-sm">
<button <button
onclick={rescrape} onclick={() => (page = Math.max(0, page - 1))}
disabled={scraping} disabled={page === 0}
class="px-3 py-1 rounded text-xs font-medium transition-colors flex items-center gap-1.5 class="px-2 py-1 rounded bg-zinc-800 text-zinc-300 disabled:opacity-40 hover:bg-zinc-700 transition-colors"
{scraping
? 'bg-zinc-700 text-zinc-500 cursor-not-allowed'
: 'bg-zinc-700 text-zinc-200 hover:bg-zinc-600'}"
title="Re-scrape this book from source"
> >
{#if scraping} &larr;
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
Queuing…
{:else}
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Rescrape
{/if}
</button> </button>
{/if} <span class="text-zinc-500 text-xs tabular-nums">
{page * PAGE_SIZE + 1}{Math.min((page + 1) * PAGE_SIZE, chapterList.length)} of {chapterList.length}
{#if totalPages > 1} </span>
<div class="flex gap-2 items-center text-sm"> <button
<button onclick={() => (page = Math.min(totalPages - 1, page + 1))}
onclick={() => (page = Math.max(0, page - 1))} disabled={page === totalPages - 1}
disabled={page === 0} class="px-2 py-1 rounded bg-zinc-800 text-zinc-300 disabled:opacity-40 hover:bg-zinc-700 transition-colors"
class="px-2 py-1 rounded bg-zinc-700 text-zinc-300 disabled:opacity-40 hover:bg-zinc-600 transition-colors" >
> &rarr;
&larr; </button>
</button> </div>
<span class="text-zinc-400">{page + 1} / {totalPages}</span> {/if}
<button
onclick={() => (page = Math.min(totalPages - 1, page + 1))}
disabled={page === totalPages - 1}
class="px-2 py-1 rounded bg-zinc-700 text-zinc-300 disabled:opacity-40 hover:bg-zinc-600 transition-colors"
>
&rarr;
</button>
</div>
{/if}
</div>
</div> </div>
{#if scrapeResult} <!-- Chapter rows -->
<div class="mb-3 px-3 py-2 rounded text-xs font-medium
{scrapeResult === 'queued' ? 'bg-green-900/40 text-green-300 border border-green-800' :
scrapeResult === 'busy' ? 'bg-amber-900/40 text-amber-300 border border-amber-800' :
'bg-red-900/40 text-red-300 border border-red-800'}">
{scrapeResult === 'queued' ? 'Rescrape queued — running in background.' :
scrapeResult === 'busy' ? 'Scraper is busy with another job. Try again shortly.' :
'Failed to queue rescrape. Check server logs.'}
</div>
{/if}
<!-- Admin: range scrape controls -->
{#if data.isAdmin && data.book.source_url}
<div class="mb-4 p-3 rounded bg-zinc-800/60 border border-zinc-700 flex flex-wrap items-end gap-3">
<div class="flex flex-col gap-1">
<label for="range-from" class="text-xs text-zinc-500">From chapter</label>
<input
id="range-from"
type="number"
min="1"
bind:value={rangeFrom}
placeholder="1"
class="w-24 px-2 py-1 rounded bg-zinc-700 border border-zinc-600 text-zinc-200 text-xs focus:outline-none focus:border-amber-400"
/>
</div>
<div class="flex flex-col gap-1">
<label for="range-to" class="text-xs text-zinc-500">To chapter (optional)</label>
<input
id="range-to"
type="number"
min="1"
bind:value={rangeTo}
placeholder="end"
class="w-24 px-2 py-1 rounded bg-zinc-700 border border-zinc-600 text-zinc-200 text-xs focus:outline-none focus:border-amber-400"
/>
</div>
<button
onclick={scrapeRange}
disabled={rangeScraping || !rangeFrom}
class="px-3 py-1.5 rounded text-xs font-medium transition-colors
{rangeScraping || !rangeFrom
? 'bg-zinc-700 text-zinc-500 cursor-not-allowed'
: 'bg-amber-500/20 text-amber-300 hover:bg-amber-500/40 border border-amber-500/30'}"
>
{rangeScraping ? 'Queuing…' : 'Scrape range'}
</button>
{#if rangeResult}
<span class="text-xs {rangeResult === 'queued' ? 'text-green-400' : rangeResult === 'busy' ? 'text-amber-400' : 'text-red-400'}">
{rangeResult === 'queued' ? 'Range scrape queued.' : rangeResult === 'busy' ? 'Scraper busy.' : 'Error queuing.'}
</span>
{/if}
</div>
{/if}
{#if pollingChapters} {#if pollingChapters}
<!-- Chapter list is being indexed in the background -->
<div class="flex items-center gap-3 py-4 text-zinc-500 text-sm"> <div class="flex items-center gap-3 py-4 text-zinc-500 text-sm">
<svg class="w-4 h-4 animate-spin flex-shrink-0" fill="none" viewBox="0 0 24 24"> <svg class="w-4 h-4 animate-spin flex-shrink-0" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
@@ -360,7 +329,7 @@
</svg> </svg>
Indexing chapter list… Indexing chapter list…
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-1 opacity-40 pointer-events-none"> <div class="flex flex-col gap-0.5 opacity-40 pointer-events-none">
{#each Array(8) as _} {#each Array(8) as _}
<div class="h-9 rounded bg-zinc-800 animate-pulse"></div> <div class="h-9 rounded bg-zinc-800 animate-pulse"></div>
{/each} {/each}
@@ -368,35 +337,32 @@
{:else if chapterList.length === 0} {:else if chapterList.length === 0}
<p class="text-zinc-500 text-sm">No chapters available yet.</p> <p class="text-zinc-500 text-sm">No chapters available yet.</p>
{:else} {:else}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-1"> <div class="flex flex-col gap-0.5">
{#each visibleChapters as chapter} {#each visibleChapters as chapter}
{@const isCurrent = data.lastChapter === chapter.number} {@const isCurrent = data.lastChapter === chapter.number}
{@const chapterUrl = data.inLib {@const chapterUrl = data.inLib
? `/books/${data.book.slug}/chapters/${chapter.number}` ? `/books/${data.book.slug}/chapters/${chapter.number}`
: `/books/${data.book.slug}/chapters/${chapter.number}?preview=1&chapter_url=${encodeURIComponent((chapter as { url?: string }).url ?? '')}&title=${encodeURIComponent(chapter.title ?? '')}`} : `/books/${data.book.slug}/chapters/${chapter.number}?preview=1&chapter_url=${encodeURIComponent((chapter as { url?: string }).url ?? '')}&title=${encodeURIComponent(chapter.title ?? '')}`}
<div class="flex items-center gap-3 px-3 py-2 rounded hover:bg-zinc-800 transition-colors group {isCurrent ? 'bg-zinc-800' : ''}"> <div class="flex items-center gap-2 px-3 py-2 rounded hover:bg-zinc-800/70 transition-colors group {isCurrent ? 'bg-zinc-800' : ''}">
<a <a href={chapterUrl} class="flex items-baseline gap-2 flex-1 min-w-0">
href={chapterUrl} <!-- Chapter number -->
class="flex items-center gap-3 flex-1 min-w-0" <span class="text-xs font-mono w-9 text-right flex-shrink-0 {isCurrent ? 'text-amber-400' : 'text-zinc-600'}">
>
<span
class="text-xs font-mono w-10 text-right flex-shrink-0 {isCurrent
? 'text-amber-400'
: 'text-zinc-500'}"
>
{chapter.number} {chapter.number}
</span> </span>
<span class="text-sm text-zinc-300 group-hover:text-zinc-100 truncate flex-1"> <!-- Title -->
<span class="text-sm {isCurrent ? 'text-amber-300' : 'text-zinc-300 group-hover:text-zinc-100'} truncate flex-1 transition-colors">
{chapter.title || `Chapter ${chapter.number}`} {chapter.title || `Chapter ${chapter.number}`}
</span> </span>
{#if isCurrent} <!-- Date label, separated visually -->
<span class="text-xs text-amber-400 flex-shrink-0">reading</span>
{/if}
{#if (chapter as { date_label?: string }).date_label} {#if (chapter as { date_label?: string }).date_label}
<span class="text-xs text-zinc-600 flex-shrink-0 hidden sm:block">{(chapter as { date_label?: string }).date_label}</span> <span class="text-xs text-zinc-600 flex-shrink-0 hidden sm:block">&middot; {(chapter as { date_label?: string }).date_label}</span>
{/if}
<!-- "reading" badge -->
{#if isCurrent}
<span class="text-xs text-amber-500 flex-shrink-0 font-medium">reading</span>
{/if} {/if}
</a> </a>
<!-- Admin: scrape from this chapter up --> <!-- Admin: scrape from this chapter up (hover-only) -->
{#if data.isAdmin && data.book.source_url && data.inLib} {#if data.isAdmin && data.book.source_url && data.inLib}
<button <button
onclick={() => scrapeFromChapter(chapter.number)} onclick={() => scrapeFromChapter(chapter.number)}
@@ -411,4 +377,96 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<!-- ── Admin panel (collapsed by default) ── -->
{#if data.isAdmin && data.book.source_url}
<div class="mt-6 border border-zinc-800 rounded-lg overflow-hidden">
<button
onclick={() => (adminOpen = !adminOpen)}
class="w-full flex items-center gap-2 px-4 py-2.5 text-xs font-medium text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/50 transition-colors text-left"
>
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Admin
<svg class="w-3 h-3 ml-auto transition-transform {adminOpen ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
{#if adminOpen}
<div class="px-4 py-3 border-t border-zinc-800 flex flex-col gap-4">
<!-- Rescrape -->
<div class="flex items-center gap-3 flex-wrap">
<button
onclick={rescrape}
disabled={scraping}
class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
{scraping ? 'bg-zinc-700 text-zinc-500 cursor-not-allowed' : 'bg-zinc-700 text-zinc-200 hover:bg-zinc-600'}"
>
{#if scraping}
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
Queuing…
{:else}
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Rescrape book
{/if}
</button>
{#if scrapeResult}
<span class="text-xs {scrapeResult === 'queued' ? 'text-green-400' : scrapeResult === 'busy' ? 'text-amber-400' : 'text-red-400'}">
{scrapeResult === 'queued' ? 'Queued.' : scrapeResult === 'busy' ? 'Scraper busy.' : 'Error.'}
</span>
{/if}
</div>
<!-- Range scrape -->
<div class="flex flex-wrap items-end gap-3">
<div class="flex flex-col gap-1">
<label for="range-from" class="text-xs text-zinc-500">From chapter</label>
<input
id="range-from"
type="number"
min="1"
bind:value={rangeFrom}
placeholder="1"
class="w-24 px-2 py-1 rounded bg-zinc-700 border border-zinc-600 text-zinc-200 text-xs focus:outline-none focus:border-amber-400"
/>
</div>
<div class="flex flex-col gap-1">
<label for="range-to" class="text-xs text-zinc-500">To chapter (optional)</label>
<input
id="range-to"
type="number"
min="1"
bind:value={rangeTo}
placeholder="end"
class="w-24 px-2 py-1 rounded bg-zinc-700 border border-zinc-600 text-zinc-200 text-xs focus:outline-none focus:border-amber-400"
/>
</div>
<button
onclick={scrapeRange}
disabled={rangeScraping || !rangeFrom}
class="px-3 py-1.5 rounded text-xs font-medium transition-colors
{rangeScraping || !rangeFrom
? 'bg-zinc-700 text-zinc-500 cursor-not-allowed'
: 'bg-amber-500/20 text-amber-300 hover:bg-amber-500/40 border border-amber-500/30'}"
>
{rangeScraping ? 'Queuing…' : 'Scrape range'}
</button>
{#if rangeResult}
<span class="text-xs {rangeResult === 'queued' ? 'text-green-400' : rangeResult === 'busy' ? 'text-amber-400' : 'text-red-400'}">
{rangeResult === 'queued' ? 'Range scrape queued.' : rangeResult === 'busy' ? 'Scraper busy.' : 'Error queuing.'}
</span>
{/if}
</div>
</div>
{/if}
</div>
{/if}
</div> </div>