diff --git a/locale/en/translation.json b/locale/en/translation.json index 0d64a522..2cf23afd 100644 --- a/locale/en/translation.json +++ b/locale/en/translation.json @@ -117,7 +117,9 @@ "content1": "Try changing your keywords and search again.", "content2": "If you can't find what you're looking for, seek help from our <0>Discord channels." }, - "searchTip": "Use double quotes for exact match. For example, \"time_zone\"." + "searchTip": "Use double quotes for exact match. For example, \"time_zone\".", + "filters": "Filter search results:", + "allResults": "All" }, "tools": { "tidbOperator": { diff --git a/locale/ja/translation.json b/locale/ja/translation.json index b536c5d9..f91b5f4f 100644 --- a/locale/ja/translation.json +++ b/locale/ja/translation.json @@ -113,7 +113,9 @@ "content1": "現時点では日本語の検索は提供されていないため、英語と中国語のドキュメントに行ってもう一度試してください。", "content2": "お探しのものが見つからない場合は、<0>Discord Channelsがお勧めです。" }, - "searchTip": "フレーズを正確に検索したい場合に「\"」で囲って利用します。例えば、\"time_zone\"。" + "searchTip": "フレーズを正確に検索したい場合に「\"」で囲って利用します。例えば、\"time_zone\"。", + "filters": "検索結果を絞り込む:", + "allResults": "すべての結果" }, "tools": { "tidbOperator": { diff --git a/locale/zh/translation.json b/locale/zh/translation.json index bc6794a1..684466e6 100644 --- a/locale/zh/translation.json +++ b/locale/zh/translation.json @@ -115,7 +115,9 @@ "content1": "请尝试使用其他关键词再次搜索。", "content2": "如果您无法找到需要的内容,请尝试前往 <0>AskTUG (TiDB User Group) 进行提问。" }, - "searchTip": "使用半角双引号进行精确搜索。例如,\"time_zone\"。" + "searchTip": "使用半角双引号进行精确搜索。例如,\"time_zone\"。", + "filters": "过滤搜索结果:", + "allResults": "所有结果" }, "tools": { "tidbOperator": { diff --git a/src/components/Search/Results.tsx b/src/components/Search/Results.tsx index 54bae09b..2bd65374 100644 --- a/src/components/Search/Results.tsx +++ b/src/components/Search/Results.tsx @@ -7,9 +7,11 @@ import Skeleton from "@mui/material/Skeleton"; import clsx from "clsx"; import TablePagination from "@mui/material/TablePagination"; import Chip from "@mui/material/Chip"; +import type { SxProps, Theme } from "@mui/material/styles"; import { getSearchCategoryLabelKey, resolveSearchCategory, + type SearchCategory, } from "shared/utils/searchCategory"; export default function SearchResults(props: { @@ -267,3 +269,93 @@ function SearchItem(props: { data: any }) { ); } + +function getSearchFilterChipSx(isActive: boolean): SxProps { + return { + height: "24px", + fontSize: "12px", + borderRadius: "12px", + cursor: "pointer", + ...(isActive + ? { + backgroundColor: "carbon.800", + color: "white", + "&:hover": { + backgroundColor: "carbon.700", + }, + } + : { + backgroundColor: "carbon.100", + color: "carbon.800", + "&:hover": { + backgroundColor: "carbon.200", + }, + }), + }; +} + +export function SearchFilterBar(props: { + categoryCountMap: Map; + totalCount: number; + activeFilter: SearchCategory | null; + onFilterChange: (category: SearchCategory | null) => void; + visible: boolean; +}) { + const { + categoryCountMap, + totalCount, + activeFilter, + onFilterChange, + visible, + } = props; + const { t } = useI18next(); + + const entries = Array.from(categoryCountMap.entries()).filter( + ([category]) => { + const labelKey = getSearchCategoryLabelKey(category); + return labelKey && t(labelKey); + } + ); + + if (!visible || entries.length <= 1) { + return null; + } + + return ( + + + + + onFilterChange(null)} + sx={getSearchFilterChipSx(activeFilter === null)} + /> + {entries.map(([category, count]) => { + const label = t(getSearchCategoryLabelKey(category)); + const isActive = activeFilter === category; + return ( + onFilterChange(category)} + sx={getSearchFilterChipSx(isActive)} + /> + ); + })} + + ); +} diff --git a/src/templates/DocSearchTemplate.tsx b/src/templates/DocSearchTemplate.tsx index 30c915a6..a36aecc2 100644 --- a/src/templates/DocSearchTemplate.tsx +++ b/src/templates/DocSearchTemplate.tsx @@ -12,6 +12,7 @@ import "styles/docTemplate.css"; import Layout from "components/Layout"; import SearchResults from "components/Search/Results"; +import { SearchFilterBar } from "components/Search/Results"; import SearchInput from "components/Search"; import { Tip } from "components/MDXComponents"; import Seo from "components/Seo"; @@ -20,6 +21,10 @@ import { Locale, TOCNamespace } from "shared/interface"; import { FeedbackSurveyCampaign } from "components/Campaign/FeedbackSurvey"; import ScrollToTopBtn from "components/Button/ScrollToTopBtn"; import { useIsAutoTranslation } from "shared/useIsAutoTranslation"; +import { + resolveSearchCategory, + type SearchCategory, +} from "shared/utils/searchCategory"; const SEARCH_INDEX_BY_LANGUAGE: Partial> = { [Locale.en]: "en-tidb-all-stable", @@ -40,15 +45,42 @@ export default function DocSearchTemplate({ }: DocSearchTemplateProps) { const [isLoading, setIsLoading] = React.useState(false); const [results, setResults] = React.useState([]); + const [activeFilter, setActiveFilter] = React.useState( + null + ); const { language } = useI18next(); const { search } = useLocation(); + const categoryCountMap = React.useMemo(() => { + const map = new Map(); + for (const item of results) { + const cat = resolveSearchCategory(item.url); + if (cat) map.set(cat, (map.get(cat) || 0) + 1); + } + return map; + }, [results]); + + const filteredResults = React.useMemo(() => { + if (!activeFilter) return results; + return results.filter( + (item) => resolveSearchCategory(item.url) === activeFilter + ); + }, [results, activeFilter]); + + const handleFilterChange = React.useCallback( + (category: SearchCategory | null) => { + setActiveFilter((prev) => (prev === category ? null : category)); + }, + [] + ); + const execSearch = React.useCallback( (query: string) => { const trimmedQuery = query.trim(); if (!trimmedQuery) { setResults([]); + setActiveFilter(null); setIsLoading(false); return; } @@ -56,6 +88,7 @@ export default function DocSearchTemplate({ const indexName = SEARCH_INDEX_BY_LANGUAGE[language as Locale]; if (!indexName) { setResults([]); + setActiveFilter(null); setIsLoading(false); return; } @@ -69,11 +102,13 @@ export default function DocSearchTemplate({ }) .then(({ hits }) => { setResults(hits); + setActiveFilter(null); setIsLoading(false); }) .catch((reason: any) => { console.error(reason); setResults([]); + setActiveFilter(null); setIsLoading(false); }); }, @@ -85,12 +120,14 @@ export default function DocSearchTemplate({ const query = searchParams.get("q") || ""; if (language === Locale.ja) { setResults([]); + setActiveFilter(null); setIsLoading(false); return; } if (!query.trim()) { setResults([]); + setActiveFilter(null); setIsLoading(false); return; } @@ -127,10 +164,17 @@ export default function DocSearchTemplate({ }} /> + 0} + /> - +