Files
Arkie-Library-Frontend/src/pages/CategoryPage.tsx

157 lines
5.1 KiB
TypeScript
Raw Normal View History

2026-05-16 00:18:22 +08:00
import { useEffect, useMemo, useState } from "react";
import { useParams, useSearchParams } from "react-router-dom";
import { getJSON, itemsOrEmpty, type Category, type Resource } from "../api";
import { ResourceCard } from "../components/ResourceCard";
import { ResourceListFooter } from "../components/ResourceListFooter";
import { useI18n } from "../i18n";
import { typeFilterLabel } from "../resourceTypeLabels";
const TYPE_FILTERS = [
"all",
"image",
"video",
"ppt",
"pdf",
"text",
"link",
"archive",
] as const;
const resourceLangCodes = ["", "zh-TW", "zh-CN", "en"] as const;
function resourceLangLabel(t: (k: string) => string, code: string) {
if (!code) return t("filterLanguageAll");
if (code === "zh-TW") return t("lang_zh_TW");
if (code === "zh-CN") return t("lang_zh_CN");
return t("lang_en");
}
export function CategoryPage() {
const { slug } = useParams();
const { t, lang } = useI18n();
const [sp, setSp] = useSearchParams();
const type = sp.get("type") || "all";
const resourceLang = sp.get("language") || "";
const page = Math.max(1, parseInt(sp.get("page") || "1", 10) || 1);
const limit = 24;
const [items, setItems] = useState<Resource[]>([]);
const [total, setTotal] = useState(0);
const [categoryTitle, setCategoryTitle] = useState<string>("");
const [err, setErr] = useState<string | null>(null);
const query = useMemo(() => {
const p = new URLSearchParams();
p.set("lang", lang);
p.set("category", slug || "");
p.set("limit", String(limit));
p.set("page", String(page));
if (type !== "all") p.set("type", type);
if (resourceLang) p.set("language", resourceLang);
return p.toString();
}, [lang, slug, type, resourceLang, page]);
useEffect(() => {
if (!slug) return;
setErr(null);
getJSON<{ items: Resource[]; total?: number }>(`/api/resources?${query}`)
.then((r) => {
setItems(itemsOrEmpty(r.items));
setTotal(typeof r.total === "number" ? r.total : 0);
})
.catch((e) => setErr(String(e)));
}, [query, slug]);
useEffect(() => {
if (!slug) return;
const langQ = encodeURIComponent(lang);
getJSON<Category[]>(`/api/categories?lang=${langQ}`)
.then((cats) => {
const c = itemsOrEmpty(cats).find((x) => x.slug === slug);
setCategoryTitle(c?.name ?? slug);
})
.catch(() => setCategoryTitle(slug));
}, [slug, lang]);
const setPage = (next: number) => {
const n = new URLSearchParams(sp);
if (next <= 1) n.delete("page");
else n.set("page", String(next));
setSp(n);
};
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">{categoryTitle || slug}</h1>
<div className="flex flex-wrap gap-2">
{TYPE_FILTERS.map((tp) => (
<button
key={tp}
type="button"
className={`rounded-full border px-3 py-1 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg ${
type === tp || (tp === "all" && !sp.get("type"))
? "border-ark-gold text-ark-gold2"
: "border-ark-line"
}`}
onClick={() => {
const n = new URLSearchParams(sp);
n.delete("page");
if (tp === "all") n.delete("type");
else n.set("type", tp);
setSp(n);
}}
>
{typeFilterLabel(t, tp)}
</button>
))}
</div>
<div className="space-y-2">
<div className="text-xs font-medium uppercase tracking-wide text-neutral-500">
{t("resourceLangFilter")}
</div>
<div className="flex flex-wrap gap-2">
{resourceLangCodes.map((code) => (
<button
key={code || "all"}
type="button"
onClick={() => {
const n = new URLSearchParams(sp);
n.delete("page");
if (!code) n.delete("language");
else n.set("language", code);
setSp(n);
}}
className={`rounded-full border px-3 py-1 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg ${
(code === "" && !resourceLang) || resourceLang === code
? "border-ark-gold text-ark-gold2"
: "border-ark-line"
}`}
>
{resourceLangLabel(t, code)}
</button>
))}
</div>
</div>
{err ? <div className="text-red-300">{err}</div> : null}
{!err && items.length === 0 ? (
<p className="text-neutral-400">{t("noResults")}</p>
) : null}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{items.map((r) => (
<ResourceCard key={r.id} r={r} />
))}
</div>
<ResourceListFooter
page={page}
limit={limit}
total={total}
t={t}
onPrev={() => setPage(page - 1)}
onNext={() => setPage(page + 1)}
/>
</div>
);
}