120 lines
3.9 KiB
Svelte
120 lines
3.9 KiB
Svelte
<script lang="ts" module>
|
|
import type { NovelsQuery, NovelStatus } from '$lib/graphql/__generated__/graphql';
|
|
|
|
export type NovelNode = NonNullable<NonNullable<NovelsQuery['novels']>['edges']>[number]['node'];
|
|
|
|
export interface NovelCardProps {
|
|
novel: NovelNode;
|
|
}
|
|
|
|
const statusColors: Record<NovelStatus, string> = {
|
|
IN_PROGRESS: 'bg-green-500 text-white',
|
|
COMPLETED: 'bg-blue-500 text-white',
|
|
HIATUS: 'bg-amber-500 text-white',
|
|
ABANDONED: 'bg-gray-500 text-white',
|
|
UNKNOWN: 'bg-gray-500 text-white'
|
|
};
|
|
|
|
const statusLabels: Record<NovelStatus, string> = {
|
|
IN_PROGRESS: 'Ongoing',
|
|
COMPLETED: 'Complete',
|
|
HIATUS: 'Hiatus',
|
|
ABANDONED: 'Dropped',
|
|
UNKNOWN: 'Unknown'
|
|
};
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
|
import { Badge } from '$lib/components/ui/badge';
|
|
import {
|
|
Tooltip,
|
|
TooltipTrigger,
|
|
TooltipContent,
|
|
TooltipProvider
|
|
} from '$lib/components/ui/tooltip';
|
|
import { formatRelativeTime, formatAbsoluteTime } from '$lib/utils/time';
|
|
import { sanitizeHtml } from '$lib/utils/sanitize';
|
|
|
|
let { novel }: NovelCardProps = $props();
|
|
|
|
function pickText(novelText?: NovelNode['name'] | NovelNode['description']) {
|
|
const texts = novelText?.texts ?? [];
|
|
const english = texts.find((t) => t.language === 'EN');
|
|
return (english ?? texts[0])?.text ?? 'No description available.';
|
|
}
|
|
|
|
const title = $derived(pickText(novel.name));
|
|
const descriptionRaw = $derived(pickText(novel.description));
|
|
const descriptionHtml = $derived(sanitizeHtml(descriptionRaw));
|
|
const coverSrc = $derived(novel.coverImage?.newPath ?? novel.coverImage?.originalPath);
|
|
|
|
const latestChapter = $derived(
|
|
novel.chapters?.slice().sort((a, b) => b.order - a.order)[0] ?? null
|
|
);
|
|
const chapterDisplay = $derived(latestChapter ? `Ch. ${latestChapter.order}` : null);
|
|
|
|
const lastUpdated = $derived(novel.lastUpdatedTime ? new Date(novel.lastUpdatedTime) : null);
|
|
const relativeTime = $derived(lastUpdated ? formatRelativeTime(lastUpdated) : null);
|
|
const absoluteTime = $derived(lastUpdated ? formatAbsoluteTime(lastUpdated) : null);
|
|
|
|
const status = $derived(novel.rawStatus ?? 'UNKNOWN');
|
|
const statusColor = $derived(statusColors[status]);
|
|
const statusLabel = $derived(statusLabels[status]);
|
|
</script>
|
|
|
|
<a
|
|
href="/novels/{novel.id}"
|
|
class="block focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg"
|
|
>
|
|
<Card class="overflow-hidden border shadow-sm transition-shadow hover:shadow-md h-full pt-0 gap-0">
|
|
<div class="relative">
|
|
{#if coverSrc}
|
|
<div class="aspect-[3/4] w-full overflow-hidden bg-muted/50">
|
|
<img src={coverSrc} alt={title} class="h-full w-full object-cover" loading="lazy" />
|
|
</div>
|
|
{:else}
|
|
<div class="aspect-[3/4] w-full bg-muted/50"></div>
|
|
{/if}
|
|
<Badge
|
|
class="absolute top-2 right-2 {statusColor} shadow-sm"
|
|
aria-label="Status: {statusLabel}"
|
|
>
|
|
{statusLabel}
|
|
</Badge>
|
|
</div>
|
|
<CardHeader class="space-y-2 pt-4">
|
|
<CardTitle class="line-clamp-2 text-lg leading-tight" title={title}>
|
|
{title}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent class="pt-0 pb-4 space-y-3">
|
|
<div class="line-clamp-3 text-sm text-muted-foreground" title={descriptionRaw}>
|
|
{@html descriptionHtml}
|
|
</div>
|
|
{#if chapterDisplay || relativeTime}
|
|
<div class="flex items-center gap-1 text-xs text-muted-foreground/80">
|
|
{#if chapterDisplay}
|
|
<span>{chapterDisplay}</span>
|
|
{/if}
|
|
{#if chapterDisplay && relativeTime}
|
|
<span aria-hidden="true">·</span>
|
|
{/if}
|
|
{#if relativeTime && absoluteTime}
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger class="cursor-default hover:text-foreground transition-colors">
|
|
<time datetime={lastUpdated?.toISOString()}>{relativeTime}</time>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{absoluteTime}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</CardContent>
|
|
</Card>
|
|
</a>
|