chore: migrate to v3 and adopt Doppler for secrets management #3
@@ -199,7 +199,7 @@
|
||||
<div class="min-h-screen flex flex-col" class:pb-24={audioStore.active}>
|
||||
<!-- Navigation progress bar — shown while SSR is running for any page transition -->
|
||||
{#if navigating}
|
||||
<div class="fixed top-0 left-0 right-0 z-[100] h-0.5 bg-zinc-800">
|
||||
<div class="fixed top-0 left-0 right-0 z-[100] h-1 bg-zinc-800">
|
||||
<div class="h-full bg-amber-400 animate-progress-bar"></div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -251,8 +251,26 @@
|
||||
{/key}
|
||||
</main>
|
||||
|
||||
<footer class="border-t border-zinc-800 text-zinc-600 text-xs text-center py-4">
|
||||
libnovel
|
||||
<footer class="border-t border-zinc-800 mt-auto">
|
||||
<div class="max-w-6xl mx-auto px-4 py-5 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||
<span class="text-zinc-500 text-sm font-semibold tracking-tight">libnovel</span>
|
||||
<nav class="flex items-center gap-5 text-xs text-zinc-600">
|
||||
<a href="/books" class="hover:text-zinc-400 transition-colors">Library</a>
|
||||
<a href="/browse" class="hover:text-zinc-400 transition-colors">Discover</a>
|
||||
<a
|
||||
href="https://novelfire.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="hover:text-zinc-400 transition-colors flex items-center gap-1"
|
||||
>
|
||||
novelfire.net
|
||||
<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h1 class="text-2xl font-bold text-zinc-100 leading-tight">{data.book.title}</h1>
|
||||
{#if !data.inLib}
|
||||
<span class="text-xs px-2 py-0.5 rounded-full bg-zinc-700 text-zinc-400 border border-zinc-600 shrink-0">
|
||||
<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">
|
||||
not in library
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { navigating } from '$app/state';
|
||||
import type { PageData, ActionData } from './$types';
|
||||
import type { NovelListing } from './+page.server';
|
||||
|
||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||
|
||||
// Track which novel card is currently being navigated to
|
||||
let loadingSlug = $state<string | null>(null);
|
||||
|
||||
// Clear loading state when navigation ends (success or failure)
|
||||
$effect(() => {
|
||||
if (!navigating) loadingSlug = null;
|
||||
});
|
||||
|
||||
function handleNovelClick(slug: string) {
|
||||
loadingSlug = slug;
|
||||
}
|
||||
|
||||
// Filter options
|
||||
const genres = [
|
||||
{ value: 'all', label: 'All Genres' },
|
||||
@@ -282,9 +295,12 @@
|
||||
<!-- ── Grid view ─────────────────────────────────────────────────────── -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
{#each data.novels as novel}
|
||||
{@const isLoading = loadingSlug === novel.slug}
|
||||
<a
|
||||
href="/books/{novel.slug}"
|
||||
class="group flex flex-col rounded-lg overflow-hidden bg-zinc-800 border border-zinc-700 hover:border-zinc-500 transition-colors relative"
|
||||
onclick={() => handleNovelClick(novel.slug)}
|
||||
class="group flex flex-col rounded-lg overflow-hidden bg-zinc-800 border transition-colors relative
|
||||
{isLoading ? 'border-amber-400/60' : 'border-zinc-700 hover:border-zinc-500'}"
|
||||
>
|
||||
<!-- Cover -->
|
||||
<div class="aspect-[2/3] bg-zinc-900 overflow-hidden relative">
|
||||
@@ -313,6 +329,15 @@
|
||||
{novel.rating}
|
||||
</span>
|
||||
{/if}
|
||||
<!-- Loading overlay -->
|
||||
{#if isLoading}
|
||||
<div class="absolute inset-0 bg-zinc-900/70 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 animate-spin text-amber-400" 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>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
@@ -355,14 +380,18 @@
|
||||
<!-- ── List view ─────────────────────────────────────────────────────── -->
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each data.novels as novel}
|
||||
<div class="flex items-center gap-4 bg-zinc-800 border border-zinc-700 rounded-lg px-4 py-3 hover:border-zinc-500 transition-colors">
|
||||
{@const isLoading = loadingSlug === novel.slug}
|
||||
<div
|
||||
class="flex items-center gap-4 bg-zinc-800 border rounded-lg px-4 py-3 transition-colors
|
||||
{isLoading ? 'border-amber-400/60' : 'border-zinc-700 hover:border-zinc-500'}"
|
||||
>
|
||||
<!-- Rank / index -->
|
||||
{#if novel.rank}
|
||||
<span class="text-amber-400 font-bold text-sm w-8 shrink-0 text-right">{novel.rank}</span>
|
||||
{/if}
|
||||
|
||||
<!-- Cover thumbnail -->
|
||||
<div class="w-10 h-14 shrink-0 rounded overflow-hidden bg-zinc-900">
|
||||
<div class="w-10 h-14 shrink-0 rounded overflow-hidden bg-zinc-900 relative">
|
||||
{#if novel.cover}
|
||||
<img src={novel.cover} alt={novel.title} class="w-full h-full object-cover" loading="lazy" />
|
||||
{:else}
|
||||
@@ -373,6 +402,14 @@
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isLoading}
|
||||
<div class="absolute inset-0 bg-zinc-900/70 flex items-center justify-center">
|
||||
<svg class="w-4 h-4 animate-spin text-amber-400" 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>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Title + meta -->
|
||||
@@ -380,7 +417,9 @@
|
||||
{#if novel.slug}
|
||||
<a
|
||||
href="/books/{novel.slug}"
|
||||
class="text-sm font-semibold text-zinc-100 hover:text-amber-400 transition-colors line-clamp-1"
|
||||
onclick={() => handleNovelClick(novel.slug)}
|
||||
class="text-sm font-semibold transition-colors line-clamp-1
|
||||
{isLoading ? 'text-amber-400' : 'text-zinc-100 hover:text-amber-400'}"
|
||||
>
|
||||
{novel.title}
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user