From 3f0a395f40f1c9ed72929dc81cc91ea2e1bfd45f Mon Sep 17 00:00:00 2001 From: TerryM Date: Wed, 27 May 2026 11:33:48 +0800 Subject: [PATCH] feat: unify search with browse page --- .../messageStream/MessageStream.tsx | 11 +- .../messageStream/hooks/usePostStream.ts | 19 ++- src/layouts/PublicLayout.tsx | 11 +- src/pages/Browse/index.tsx | 5 +- src/pages/Home/index.tsx | 12 +- src/pages/Search/index.tsx | 127 +----------------- 6 files changed, 45 insertions(+), 140 deletions(-) diff --git a/src/components/messageStream/MessageStream.tsx b/src/components/messageStream/MessageStream.tsx index a297365..b09a32b 100644 --- a/src/components/messageStream/MessageStream.tsx +++ b/src/components/messageStream/MessageStream.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useRef } from "react"; import { useSearchParams } from "react-router-dom"; +import { postJSON } from "../../api"; import { useI18n } from "../../i18n"; import type { PostScope } from "../../types/post"; import { FilterChips } from "./FilterChips"; @@ -16,8 +17,12 @@ export function MessageStream({ scope }: MessageStreamProps) { const [sp, setSp] = useSearchParams(); const type = sp.get("type") || "all"; + const q = (sp.get("q") || "").trim(); - const params = useMemo(() => ({ scope, type, lang }), [scope, type, lang]); + const params = useMemo( + () => ({ scope, type, q, lang }), + [scope, type, q, lang], + ); const { items, isLoading, error, hasMore, loadMore, reset } = usePostStream(params); @@ -34,6 +39,10 @@ export function MessageStream({ scope }: MessageStreamProps) { isLoadingRef.current = isLoading; }, [isLoading]); + useEffect(() => { + if (q) postJSON("/api/search-log", { query: q }).catch(() => {}); + }, [q]); + useEffect(() => { const el = sentinelRef.current; if (!el) return; diff --git a/src/components/messageStream/hooks/usePostStream.ts b/src/components/messageStream/hooks/usePostStream.ts index b85d05f..e855af6 100644 --- a/src/components/messageStream/hooks/usePostStream.ts +++ b/src/components/messageStream/hooks/usePostStream.ts @@ -14,6 +14,7 @@ export type PostStreamParams = { scope: PostScope; type?: string; language?: string; + q?: string; lang: Lang; }; @@ -56,7 +57,20 @@ function filterMock(params: PostStreamParams): Post[] { p.categorySlug !== params.scope.slug ) return false; + const q = params.q?.trim().toLowerCase(); if (params.language && p.language !== params.language) return false; + if (q) { + const haystack = [ + p.text ?? "", + p.categorySlug, + p.postType ?? "", + ...(p.tags ?? []), + ...p.attachments.flatMap((a) => [a.filename, a.mime, a.kind]), + ] + .join("\n") + .toLowerCase(); + if (!haystack.includes(q)) return false; + } if (!postMatchesType(p, params.type ?? "all")) return false; return true; }).sort( @@ -67,13 +81,15 @@ function filterMock(params: PostStreamParams): Post[] { function buildRealUrl(params: PostStreamParams, cursor?: string): string { const sp = new URLSearchParams(); + const q = params.q?.trim(); sp.set("lang", langQuery(params.lang)); sp.set("limit", String(PAGE_SIZE)); + if (q) sp.set("q", q); if (params.scope.kind === "category") sp.set("category", params.scope.slug); if (params.type && params.type !== "all") sp.set("type", params.type); if (params.language) sp.set("language", sourceLanguageQuery(params.language)); if (cursor) sp.set("cursor", cursor); - return `/api/posts?${sp.toString()}`; + return `${q ? "/api/posts/search" : "/api/posts"}?${sp.toString()}`; } export function usePostStream(params: PostStreamParams): PostStreamResult { @@ -146,6 +162,7 @@ export function usePostStream(params: PostStreamParams): PostStreamResult { params.scope.kind === "category" ? params.scope.slug : "", params.type, params.language, + params.q, params.lang, ]); diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index 406e324..14e4881 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -11,7 +11,6 @@ import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; import { ArkLogoMark } from "../components/ArkLogoMark"; import { useI18n, type Lang } from "../i18n"; import { LANG_OPTIONS } from "../i18nLanguages"; -import { adminUiPrefix } from "../adminPaths"; type PublicNavWhich = | "home" @@ -268,7 +267,7 @@ export function PublicLayout() { const goSearch = () => { const s = q.trim(); if (!s) return; - nav(`/search?q=${encodeURIComponent(s)}`); + nav(`/browse?q=${encodeURIComponent(s)}`); setOpen(false); setMobileSearchOpen(false); }; @@ -545,14 +544,6 @@ export function PublicLayout() { > {t("footerAbout")} - {import.meta.env.VITE_DISABLE_ADMIN !== "true" ? ( - - {t("footerAdminLogin")} - - ) : null} diff --git a/src/pages/Browse/index.tsx b/src/pages/Browse/index.tsx index b5af555..ef8119f 100644 --- a/src/pages/Browse/index.tsx +++ b/src/pages/Browse/index.tsx @@ -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 (

- {t("all")} + {q ? `${t("search")}: ${q}` : t("all")}

diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index a5f339f..c7db3e4 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -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(`/api/categories${q}`), - getJSON<{ items: Post[] }>(`/api/posts/recommended${q}&limit=12`), - getJSON<{ items: Post[] }>(`/api/posts/latest${q}&limit=8`), + getJSON(`/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)); diff --git a/src/pages/Search/index.tsx b/src/pages/Search/index.tsx index 678bc60..bb537f0 100644 --- a/src/pages/Search/index.tsx +++ b/src/pages/Search/index.tsx @@ -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([]); - const [err, setErr] = useState(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 ( -
-

- {t("search")}: {q || "—"} -

- - {q ? ( - <> -
- {types.map((tp) => ( - - ))} -
- -
- {resourceLangCodes.map((code) => ( - - ))} -
- - ) : null} - - {err ?
{err}
: null} - {!q ?

{t("noResults")}

: null} - {q && items.length === 0 && !err ? ( -

{t("noResults")}

- ) : null} - -
- {items.map((post) => ( - - ))} -
-
- ); + const [sp] = useSearchParams(); + const query = sp.toString(); + return ; }