Reading progress
- New ReadingProgressButton client component that reads/writes
localStorage key "sunnymh:last-read:{slug}"
- PageReader writes the currently-visible chapter as the user scrolls
through continuous chapters
- Manga detail CTA now reads: "开始阅读" on first visit, or
"继续阅读 · #{N} {title}" when a prior chapter is stored (with
spacing and truncation for long titles)
Multiple genres
- lib/genres.ts with parseGenres() and collectGenres() helpers to
split "冒险, 恋爱, 魔幻" into a list and aggregate across a collection
- Manga detail renders one pill per genre
- GenreTabs filters via parseGenres(...).includes(activeGenre) so a
multi-genre manga appears under each of its genres
- Home / Genre pages compute the tab list from collectGenres(signedManga)
- Card captions (GenreTabs + TrendingCarousel) show "冒险 · 恋爱 · 魔幻"
with truncation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
65 lines
1.7 KiB
TypeScript
65 lines
1.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
|
|
type ChapterLite = {
|
|
number: number;
|
|
title: string;
|
|
};
|
|
|
|
type Props = {
|
|
mangaSlug: string;
|
|
chapters: ChapterLite[];
|
|
};
|
|
|
|
function storageKey(slug: string) {
|
|
return `sunnymh:last-read:${slug}`;
|
|
}
|
|
|
|
export function readLastReadChapter(slug: string): number | null {
|
|
if (typeof window === "undefined") return null;
|
|
const raw = window.localStorage.getItem(storageKey(slug));
|
|
if (!raw) return null;
|
|
const n = Number(raw);
|
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
}
|
|
|
|
export function writeLastReadChapter(slug: string, chapter: number) {
|
|
if (typeof window === "undefined") return;
|
|
window.localStorage.setItem(storageKey(slug), String(chapter));
|
|
}
|
|
|
|
export function ReadingProgressButton({ mangaSlug, chapters }: Props) {
|
|
const [lastRead, setLastRead] = useState<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
setLastRead(readLastReadChapter(mangaSlug));
|
|
}, [mangaSlug]);
|
|
|
|
if (chapters.length === 0) return null;
|
|
const first = chapters[0];
|
|
const resumeChapter =
|
|
lastRead !== null ? chapters.find((c) => c.number === lastRead) : null;
|
|
const target = resumeChapter ?? first;
|
|
|
|
return (
|
|
<Link
|
|
href={`/manga/${mangaSlug}/${target.number}`}
|
|
className="flex items-center justify-center gap-3 w-full py-3 mb-6 px-4 text-sm font-semibold bg-accent hover:bg-accent-hover text-white rounded-xl transition-colors active:scale-[0.98]"
|
|
>
|
|
{resumeChapter ? (
|
|
<>
|
|
<span>继续阅读</span>
|
|
<span className="opacity-50">·</span>
|
|
<span className="truncate">
|
|
#{resumeChapter.number} {resumeChapter.title}
|
|
</span>
|
|
</>
|
|
) : (
|
|
"开始阅读"
|
|
)}
|
|
</Link>
|
|
);
|
|
}
|