Skip to content

Commit f7a437e

Browse files
committed
use fumadocs search
1 parent feb36cd commit f7a437e

File tree

2 files changed

+50
-106
lines changed

2 files changed

+50
-106
lines changed

www/src/components/core/loader.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "@/registry/core/loader_ring"

www/src/components/search-command-client.tsx

+49-106
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,70 @@
11
"use client";
22

33
import React from "react";
4+
import { useDocsSearch } from "fumadocs-core/search/client";
5+
import type { SortedResult } from "fumadocs-core/server";
46
import {
57
ChevronsUpDownIcon,
68
CornerDownLeftIcon,
79
FileTextIcon,
810
HashIcon,
911
SearchIcon,
1012
} from "lucide-react";
11-
import { useFilter } from "react-aria-components";
13+
import { kekabCaseToTitle } from "@/lib/string";
1214
import { Button } from "@/components/core/button";
1315
import { DialogRoot, Dialog } from "@/components/core/dialog";
16+
import { Loader } from "@/components/core/loader";
1417
import { MenuContent, MenuItem, MenuSection } from "@/components/core/menu";
1518
import { SearchFieldRoot } from "@/components/core/search-field";
1619
import { Command } from "@/registry/core/command_basic";
1720
import { Input, InputRoot } from "@/registry/core/input_basic";
18-
19-
interface Heading {
20-
id: string;
21-
content: string;
22-
}
23-
24-
interface Page {
25-
id: string;
26-
title: string;
27-
headings: Heading[];
28-
url: string;
29-
}
21+
import { searchConfig } from "@/config";
3022

3123
interface SearchCommandClientProps {
3224
keyboardShortcut?: boolean;
3325
children: React.ReactNode;
34-
items: {
35-
title: string;
36-
items: Page[];
37-
}[];
3826
}
3927

4028
export function SearchCommandClient({
4129
keyboardShortcut,
4230
children,
43-
items,
4431
}: SearchCommandClientProps) {
45-
const [inputValue, setInputValue] = React.useState("");
46-
const filteredItems = React.useMemo(
47-
() => filterResults(inputValue, items),
48-
[inputValue, items]
49-
);
32+
const { search, setSearch, query } = useDocsSearch({ type: "fetch" });
33+
const results =
34+
search === "" || query.data === "empty"
35+
? [
36+
{
37+
id: "suggestions",
38+
name: "Suggestions",
39+
results: searchConfig.defaultResults.map((elem) => ({
40+
id: elem.href,
41+
content: elem.name,
42+
url: elem.href,
43+
type: "page",
44+
})),
45+
},
46+
]
47+
: groupByCategory(query.data);
5048

5149
return (
5250
<SearchCommandDialog keyboardShortcut={keyboardShortcut} trigger={children}>
53-
<Command
54-
inputValue={inputValue}
55-
onInputChange={setInputValue}
56-
className="h-72"
57-
>
51+
<Command inputValue={search} onInputChange={setSearch} className="h-72">
5852
<div className="p-1">
5953
<SearchFieldRoot placeholder="Search" autoFocus className="w-full">
6054
<InputRoot className="focus-within:ring-1">
61-
<SearchIcon />
55+
{query.isLoading ? <Loader /> : <SearchIcon />}
6256
<Input />
6357
</InputRoot>
6458
</SearchFieldRoot>
6559
</div>
6660
<MenuContent className="h-full overflow-y-scroll py-1">
67-
{filteredItems.map((category) => (
68-
<MenuSection key={category.id} title={category.title}>
69-
{category.items.map((item) => (
61+
{results.map((group) => (
62+
<MenuSection key={group.id} title={group.name}>
63+
{group.results.map((item) => (
7064
<MenuItem
71-
key={`${item.href}-${item.type}`}
72-
href={item.href}
73-
textValue={item.title}
65+
key={item.id}
66+
href={item.url}
67+
textValue={item.content}
7468
prefix={item.type === "page" ? <FileTextIcon /> : undefined}
7569
className={
7670
item.type === "page"
@@ -79,11 +73,11 @@ export function SearchCommandClient({
7973
}
8074
>
8175
{item.type === "page" ? (
82-
item.title
76+
item.content
8377
) : (
8478
<div className="[&_svg]:text-fg-muted ml-2 flex items-center gap-3 border-l pl-4 [&_svg]:size-4">
8579
<HashIcon />
86-
<p className="flex-1 truncate py-2">{item.title}</p>
80+
<p className="flex-1 truncate py-2">{item.content}</p>
8781
</div>
8882
)}
8983
</MenuItem>
@@ -106,77 +100,26 @@ export function SearchCommandClient({
106100
);
107101
}
108102

109-
type FilteredItems = {
110-
title: string;
111-
href: string;
112-
type: "page" | "heading";
113-
}[];
114-
115-
type FilteredResult = {
103+
type GroupedResults = {
116104
id: string;
117-
title: string;
118-
items: FilteredItems;
105+
name: string;
106+
results: SortedResult[];
119107
}[];
120-
121-
const filterResults = (
122-
query: string,
123-
items: SearchCommandClientProps["items"]
124-
): FilteredResult => {
125-
// When no query, return all pages without headings
126-
if (!query) {
127-
return items.map((category) => ({
128-
id: category.title,
129-
title: category.title,
130-
items: category.items.map((page) => ({
131-
title: page.title,
132-
href: page.url,
133-
type: "page",
134-
})),
135-
}));
136-
}
137-
138-
const normalizedQuery = query.toLowerCase().trim();
139-
const results: FilteredResult = [];
140-
141-
items.forEach((category) => {
142-
const matchedItems: FilteredItems = [];
143-
144-
category.items.forEach((page) => {
145-
const isPageMatch = page.title.toLowerCase().includes(normalizedQuery);
146-
const matchedHeadings = page.headings.filter((heading) =>
147-
heading.content.toLowerCase().includes(normalizedQuery)
148-
);
149-
150-
// Add page if title matches or if there are matched headings
151-
if (isPageMatch || matchedHeadings.length > 0) {
152-
matchedItems.push({
153-
title: page.title,
154-
href: page.url,
155-
type: "page",
156-
});
157-
}
158-
159-
// Add matched headings after their parent page
160-
matchedHeadings.forEach((heading) => {
161-
matchedItems.push({
162-
title: heading.content,
163-
href: `${page.url}#${heading.id}`,
164-
type: "heading",
165-
});
166-
});
167-
});
168-
169-
// Only add categories that have matches
170-
if (matchedItems.length > 0) {
171-
results.push({
172-
id: category.title,
173-
title: category.title,
174-
items: matchedItems,
175-
});
176-
}
177-
});
178-
179-
return results;
108+
const groupByCategory = (results?: SortedResult[]): GroupedResults => {
109+
// We will get the category from the url and group the results by category
110+
// eg url: /docs/components/buttons/button -> category: components
111+
if (!results) return [];
112+
const uniqueCategories = Array.from(
113+
new Set(results.map((result) => result.url.split("/")[2]!))
114+
).filter(Boolean);
115+
116+
const groupedResults: GroupedResults = uniqueCategories.map((category) => ({
117+
id: category,
118+
name: kekabCaseToTitle(category),
119+
results: results.filter((result) => result.url.split("/")[2] === category),
120+
}));
121+
122+
return groupedResults;
180123
};
181124

182125
const SearchCommandDialog = ({

0 commit comments

Comments
 (0)