Files
Arkie-Library-Frontend/src/pages/SearchPage.tsx
2026-05-16 00:18:22 +08:00

192 lines
6.2 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { getJSON, itemsOrEmpty, postJSON, type Resource } from "../api";
import { ResourceCard } from "../components/ResourceCard";
import { ResourceListFooter } from "../components/ResourceListFooter";
import { useI18n } from "../i18n";
import { typeFilterLabel } from "../resourceTypeLabels";
const types = [
"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 SearchPage() {
const { t, lang } = useI18n();
const [sp, setSp] = useSearchParams();
const q = sp.get("q") || "";
const sort = sp.get("sort") || "latest";
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 [err, setErr] = useState<string | null>(null);
const query = useMemo(() => {
const p = new URLSearchParams();
p.set("lang", lang);
p.set("limit", String(limit));
p.set("page", String(page));
p.set("sort", sort);
if (q) p.set("q", q);
if (type && type !== "all") p.set("type", type);
if (resourceLang) p.set("language", resourceLang);
return p.toString();
}, [lang, q, sort, type, resourceLang, page]);
useEffect(() => {
setErr(null);
if (!q) {
setItems([]);
setTotal(0);
return;
}
postJSON("/api/search-log", { query: q }).catch(() => {});
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, q]);
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">
{t("search")}: {q || "—"}
</h1>
{q ? (
<>
<div className="flex flex-wrap gap-2">
{(
[
["latest", t("latest")],
["recommended", t("official")],
["popular", t("popular")],
["published", t("sortPublished")],
] as const
).map(([k, label]) => (
<button
key={k}
type="button"
onClick={() => {
const n = new URLSearchParams(sp);
n.set("sort", k);
n.delete("page");
setSp(n);
}}
className={`rounded-full border px-3 py-1 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg ${
sort === k
? "border-ark-gold text-ark-gold2"
: "border-ark-line"
}`}
>
{label}
</button>
))}
</div>
<div className="flex flex-wrap gap-2">
{types.map((tp) => (
<button
key={tp}
type="button"
onClick={() => {
const n = new URLSearchParams(sp);
n.delete("page");
if (tp === "all") n.delete("type");
else n.set("type", tp);
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 ${
type === tp || (tp === "all" && !sp.get("type"))
? "border-ark-gold text-ark-gold2"
: "border-ark-line"
}`}
>
{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>
</>
) : null}
{err ? <div className="text-red-300">{err}</div> : null}
{!q ? <p className="text-neutral-400">{t("noResults")}</p> : null}
{q && items.length === 0 && !err ? (
<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>
{q ? (
<ResourceListFooter
page={page}
limit={limit}
total={total}
t={t}
onPrev={() => setPage(page - 1)}
onNext={() => setPage(page + 1)}
/>
) : null}
</div>
);
}