diff --git a/components/GenreTabs.tsx b/components/GenreTabs.tsx
index 5183968..44ce709 100644
--- a/components/GenreTabs.tsx
+++ b/components/GenreTabs.tsx
@@ -2,6 +2,7 @@
import { useState } from "react";
import Link from "next/link";
+import { parseGenres } from "@/lib/genres";
type MangaItem = {
slug: string;
@@ -24,7 +25,7 @@ export function GenreTabs({
const filtered =
activeGenre === "All"
? manga
- : manga.filter((m) => m.genre === activeGenre);
+ : manga.filter((m) => parseGenres(m.genre).includes(activeGenre));
return (
@@ -84,7 +85,9 @@ export function GenreTabs({
{m.title}
-
{m.genre}
+
+ {parseGenres(m.genre).join(" · ")}
+
diff --git a/components/PageReader.tsx b/components/PageReader.tsx
index f0c9a50..6484d1b 100644
--- a/components/PageReader.tsx
+++ b/components/PageReader.tsx
@@ -3,6 +3,7 @@
import { useState, useEffect, useRef, useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
+import { writeLastReadChapter } from "@/components/ReadingProgressButton";
type ChapterMeta = {
id: number;
@@ -251,6 +252,11 @@ export function PageReader({
};
}, [pages, startChapterNumber]);
+ // Persist reading progress whenever the visible chapter changes
+ useEffect(() => {
+ writeLastReadChapter(mangaSlug, currentChapterNum);
+ }, [mangaSlug, currentChapterNum]);
+
const currentChapter =
chapters.find((c) => c.number === currentChapterNum) ??
chapters.find((c) => c.number === startChapterNumber);
@@ -354,10 +360,10 @@ export function PageReader({
{mangaTitle}
- Back to Manga
+ Back to Home
)}
diff --git a/components/ReadingProgressButton.tsx b/components/ReadingProgressButton.tsx
new file mode 100644
index 0000000..eb7abf3
--- /dev/null
+++ b/components/ReadingProgressButton.tsx
@@ -0,0 +1,64 @@
+"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(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 (
+
+ {resumeChapter ? (
+ <>
+ 继续阅读
+ ·
+
+ #{resumeChapter.number} {resumeChapter.title}
+
+ >
+ ) : (
+ "开始阅读"
+ )}
+
+ );
+}
diff --git a/components/TrendingCarousel.tsx b/components/TrendingCarousel.tsx
index 20bba87..4004410 100644
--- a/components/TrendingCarousel.tsx
+++ b/components/TrendingCarousel.tsx
@@ -2,6 +2,7 @@
import { useRef, useState, useEffect, useCallback } from "react";
import Link from "next/link";
+import { parseGenres } from "@/lib/genres";
type TrendingManga = {
slug: string;
@@ -138,8 +139,8 @@ export function TrendingCarousel({ manga }: { manga: TrendingManga[] }) {
{m.title}
-
- {m.genre}
+
+ {parseGenres(m.genre).join(" · ")}
diff --git a/lib/genres.ts b/lib/genres.ts
new file mode 100644
index 0000000..7a29974
--- /dev/null
+++ b/lib/genres.ts
@@ -0,0 +1,31 @@
+/**
+ * A manga's `genre` field may hold multiple comma-separated genres
+ * (e.g. "冒险, 恋爱, 魔幻"). This normalizes the raw string into a
+ * deduped, trimmed list.
+ */
+export function parseGenres(raw: string): string[] {
+ if (!raw) return [];
+ const seen = new Set();
+ const out: string[] = [];
+ for (const part of raw.split(",")) {
+ const g = part.trim();
+ if (!g) continue;
+ if (seen.has(g)) continue;
+ seen.add(g);
+ out.push(g);
+ }
+ return out;
+}
+
+/**
+ * Flatten all genres across a collection of manga into a sorted unique list.
+ */
+export function collectGenres(
+ mangas: { genre: string }[]
+): string[] {
+ const seen = new Set();
+ for (const m of mangas) {
+ for (const g of parseGenres(m.genre)) seen.add(g);
+ }
+ return [...seen].sort();
+}