diff --git a/locale/en/translation.json b/locale/en/translation.json
index c1faed08..8d2f165f 100644
--- a/locale/en/translation.json
+++ b/locale/en/translation.json
@@ -117,7 +117,8 @@
"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 channels0>."
},
- "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:"
},
"tools": {
"tidbOperator": {
diff --git a/locale/zh/translation.json b/locale/zh/translation.json
index 0b7fbebb..88730bc5 100644
--- a/locale/zh/translation.json
+++ b/locale/zh/translation.json
@@ -115,7 +115,8 @@
"content1": "请尝试使用其他关键词再次搜索。",
"content2": "如果您无法找到需要的内容,请尝试前往 <0>AskTUG (TiDB User Group)0> 进行提问。"
},
- "searchTip": "使用半角双引号进行精确搜索。例如,\"time_zone\"。"
+ "searchTip": "使用半角双引号进行精确搜索。例如,\"time_zone\"。",
+ "filters": "过滤搜索结果:"
},
"tools": {
"tidbOperator": {
diff --git a/src/components/Search/Results.tsx b/src/components/Search/Results.tsx
index 54bae09b..15569b7d 100644
--- a/src/components/Search/Results.tsx
+++ b/src/components/Search/Results.tsx
@@ -10,14 +10,16 @@ import Chip from "@mui/material/Chip";
import {
getSearchCategoryLabelKey,
resolveSearchCategory,
+ type SearchCategory,
} from "shared/utils/searchCategory";
export default function SearchResults(props: {
loading: boolean;
className?: string;
data: any[];
+ onFilterChange?: (category: SearchCategory | null) => void;
}) {
- const { data, loading } = props;
+ const { data, loading, onFilterChange } = props;
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
@@ -101,7 +103,11 @@ export default function SearchResults(props: {
*/}
{filteredDataMemo.map((item) => (
-
+
))}
{data.length === 0 && !loading && (
@@ -177,8 +183,11 @@ function SearchItemSkeleton() {
);
}
-function SearchItem(props: { data: any }) {
- const { data } = props;
+function SearchItem(props: {
+ data: any;
+ onFilterChange?: (category: SearchCategory | null) => void;
+}) {
+ const { data, onFilterChange } = props;
const { t } = useI18next();
const category = React.useMemo(
() => resolveSearchCategory(data.url),
@@ -214,12 +223,27 @@ function SearchItem(props: { data: any }) {
size="small"
variant="outlined"
label={categoryLabel}
+ clickable={!!onFilterChange}
+ onClick={
+ onFilterChange
+ ? (e) => {
+ e.preventDefault();
+ onFilterChange(category);
+ }
+ : undefined
+ }
sx={{
height: "20px",
fontSize: "12px",
borderRadius: "10px",
backgroundColor: "carbon.100",
color: "carbon.800",
+ ...(onFilterChange && {
+ cursor: "pointer",
+ "&:hover": {
+ backgroundColor: "carbon.200",
+ },
+ }),
}}
/>
)}
@@ -267,3 +291,74 @@ function SearchItem(props: { data: any }) {
);
}
+
+export function SearchFilterBar(props: {
+ categoryCountMap: Map;
+ activeFilter: SearchCategory | null;
+ onFilterChange: (category: SearchCategory | null) => void;
+ visible: boolean;
+}) {
+ const { categoryCountMap, 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 (
+
+
+
+
+ {entries.map(([category, count]) => {
+ const label = t(getSearchCategoryLabelKey(category));
+ const isActive = activeFilter === category;
+ return (
+ onFilterChange(category)}
+ sx={{
+ 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",
+ },
+ }),
+ }}
+ />
+ );
+ })}
+
+ );
+}
diff --git a/src/templates/DocSearchTemplate.tsx b/src/templates/DocSearchTemplate.tsx
index 30c915a6..b51dbd69 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,41 @@ 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 +87,7 @@ export default function DocSearchTemplate({
const indexName = SEARCH_INDEX_BY_LANGUAGE[language as Locale];
if (!indexName) {
setResults([]);
+ setActiveFilter(null);
setIsLoading(false);
return;
}
@@ -69,11 +101,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 +119,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 +163,20 @@ export default function DocSearchTemplate({
}}
/>
+ 0}
+ />
-
+