feat: unify search with browse page

This commit is contained in:
TerryM
2026-05-27 11:33:48 +08:00
parent f169144378
commit 3f0a395f40
6 changed files with 45 additions and 140 deletions

View File

@@ -1,12 +1,15 @@
import { useSearchParams } from "react-router-dom";
import { MessageStream } from "../../components/messageStream/MessageStream";
import { useI18n } from "../../i18n";
export function Browse() {
const { t } = useI18n();
const [sp] = useSearchParams();
const q = sp.get("q") || "";
return (
<section className="space-y-3">
<h1 className="mx-auto max-w-full px-3 text-2xl font-bold md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
{t("all")}
{q ? `${t("search")}: ${q}` : t("all")}
</h1>
<MessageStream scope={{ kind: "all" }} />
</section>

View File

@@ -11,6 +11,7 @@ import {
import { RecommendedCard } from "../../components/RecommendedCard";
import { SectionHeader } from "../../components/SectionHeader";
import { langQuery, useI18n } from "../../i18n";
import { sourceLanguageQuery } from "../../i18nLanguages";
import { categoryCardLines } from "../../utils/categoryDisplay";
import {
postToResource,
@@ -28,11 +29,14 @@ export function Home() {
const [canScrollRec, setCanScrollRec] = useState(false);
useEffect(() => {
const q = `?lang=${encodeURIComponent(langQuery(lang))}`;
const langParam = encodeURIComponent(langQuery(lang));
const languageParam = encodeURIComponent(sourceLanguageQuery(lang));
const catQ = `?lang=${langParam}`;
const postQ = `?lang=${langParam}&language=${languageParam}`;
Promise.all([
getJSON<Category[]>(`/api/categories${q}`),
getJSON<{ items: Post[] }>(`/api/posts/recommended${q}&limit=12`),
getJSON<{ items: Post[] }>(`/api/posts/latest${q}&limit=8`),
getJSON<Category[]>(`/api/categories${catQ}`),
getJSON<{ items: Post[] }>(`/api/posts/recommended${postQ}&limit=12`),
getJSON<{ items: Post[] }>(`/api/posts/latest${postQ}&limit=8`),
])
.then(([c, r, l]) => {
setCats(itemsOrEmpty(c));

View File

@@ -1,126 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { getJSON, itemsOrEmpty, postJSON } from "../../api";
import { langQuery, useI18n } from "../../i18n";
import {
LANG_OPTIONS,
languageLabel,
sourceLanguageQuery,
} from "../../i18nLanguages";
import { MessageBubble } from "../../components/messageStream/MessageBubble";
import { typeFilterLabel } from "../../resourceTypeLabels";
import type { Post } from "../../types/post";
const types = [
"all",
"image",
"video",
"music",
"ppt",
"pdf",
"text",
"link",
"archive",
] as const;
const resourceLangCodes = ["", ...LANG_OPTIONS.map((x) => x.code)] as const;
import { Navigate, useSearchParams } from "react-router-dom";
export function SearchPage() {
const { t, lang } = useI18n();
const [sp, setSp] = useSearchParams();
const q = sp.get("q") || "";
const type = sp.get("type") || "all";
const resourceLang = sp.get("language") || "";
const [items, setItems] = useState<Post[]>([]);
const [err, setErr] = useState<string | null>(null);
const query = useMemo(() => {
const p = new URLSearchParams();
p.set("lang", langQuery(lang));
p.set("limit", "50");
p.set("q", q);
if (type && type !== "all") p.set("type", type);
if (resourceLang) p.set("language", sourceLanguageQuery(resourceLang));
return p.toString();
}, [lang, q, type, resourceLang]);
useEffect(() => {
setErr(null);
if (!q.trim()) {
setItems([]);
return;
}
postJSON("/api/search-log", { query: q }).catch(() => {});
getJSON<{ items: Post[] }>(`/api/posts/search?${query}`)
.then((r) => setItems(itemsOrEmpty(r.items)))
.catch((e) => setErr(String(e)));
}, [query, q]);
return (
<div className="mx-auto max-w-[640px] space-y-4 px-3">
<h1 className="text-2xl font-bold">
{t("search")}: {q || "—"}
</h1>
{q ? (
<>
<div className="flex flex-wrap gap-2">
{types.map((tp) => (
<button
key={tp}
type="button"
onClick={() => {
const n = new URLSearchParams(sp);
if (tp === "all") n.delete("type");
else n.set("type", tp);
setSp(n, { replace: true });
}}
className={`rounded-full border px-3 py-1 text-xs transition ${
type === tp || (tp === "all" && !sp.get("type"))
? "border-ark-gold text-ark-gold2"
: "border-ark-line text-neutral-300"
}`}
>
{typeFilterLabel(t, tp)}
</button>
))}
</div>
<div className="flex flex-wrap gap-2">
{resourceLangCodes.map((code) => (
<button
key={code || "all"}
type="button"
onClick={() => {
const n = new URLSearchParams(sp);
if (!code) n.delete("language");
else n.set("language", code);
setSp(n, { replace: true });
}}
className={`rounded-full border px-3 py-1 text-xs transition ${
(code === "" && !resourceLang) || resourceLang === code
? "border-ark-gold text-ark-gold2"
: "border-ark-line text-neutral-300"
}`}
>
{languageLabel(t, code)}
</button>
))}
</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="flex flex-col gap-2">
{items.map((post) => (
<MessageBubble key={post.id} post={post} />
))}
</div>
</div>
);
const [sp] = useSearchParams();
const query = sp.toString();
return <Navigate to={`/browse${query ? `?${query}` : ""}`} replace />;
}