feat: loading indicator on browse cards and improved footer
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 17s
CI / Scraper / Test (pull_request) Successful in 18s
CI / Scraper / Build (pull_request) Successful in 17s
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 17s
CI / Scraper / Test (pull_request) Successful in 18s
CI / Scraper / Build (pull_request) Successful in 17s
This commit is contained in:
@@ -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