add the search function to website
This commit is contained in:
@@ -5,7 +5,7 @@ import { nanoid } from "nanoid";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { MdMenu, MdOutlineDarkMode, MdOutlineLightMode } from "react-icons/md";
|
import { MdMenu, MdOutlineDarkMode, MdOutlineLightMode, MdSearch } from "react-icons/md";
|
||||||
|
|
||||||
const MenuItems = [
|
const MenuItems = [
|
||||||
{
|
{
|
||||||
@@ -50,15 +50,22 @@ export const NavBar = () => {
|
|||||||
<Link
|
<Link
|
||||||
href={menuItem.href}
|
href={menuItem.href}
|
||||||
key={nanoid()}
|
key={nanoid()}
|
||||||
className="border-b-sky-600 font-bold hover:text-sky-600 dark:hover:border-b-sky-500 dark:hover:text-sky-500 mx-2 my-auto px-2"
|
className="border-b-sky-600 font-bold hover:text-sky-600 dark:hover:border-b-sky-500 dark:hover:text-sky-500 mx-1 my-auto px-2"
|
||||||
onClick={() => setIsSideNavOpen(false)}
|
onClick={() => setIsSideNavOpen(false)}
|
||||||
>
|
>
|
||||||
{menuItem.title}
|
{menuItem.title}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
<Link
|
||||||
|
href={"/search"}
|
||||||
|
key={nanoid()}
|
||||||
|
className="cursor-pointer mx-1 rounded-full p-1 text-3xl text-black hover:bg-gray-200 dark:text-gray-50 dark:hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
<MdSearch />
|
||||||
|
</Link>
|
||||||
<div
|
<div
|
||||||
title={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
|
title={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
|
||||||
className="cursor-pointer mx-2 rounded-full p-1 text-3xl text-black hover:bg-gray-200 dark:text-gray-50 dark:hover:bg-gray-800"
|
className="cursor-pointer mx-1 rounded-full p-1 text-3xl text-black hover:bg-gray-200 dark:text-gray-50 dark:hover:bg-gray-800"
|
||||||
onClick={handleSwitchTheme}
|
onClick={handleSwitchTheme}
|
||||||
>
|
>
|
||||||
{theme === "light" ? <MdOutlineDarkMode /> : <MdOutlineLightMode />}
|
{theme === "light" ? <MdOutlineDarkMode /> : <MdOutlineLightMode />}
|
||||||
@@ -84,6 +91,14 @@ export const NavBar = () => {
|
|||||||
{menuItem.title}
|
{menuItem.title}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
<Link
|
||||||
|
href={"/search"}
|
||||||
|
key={nanoid()}
|
||||||
|
className="border-b border-dashed p-3 text-xl hover:text-sky-500"
|
||||||
|
onClick={() => setIsSideNavOpen(false)}
|
||||||
|
>
|
||||||
|
{"SEARCH"}
|
||||||
|
</Link>
|
||||||
<div
|
<div
|
||||||
title={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
|
title={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
|
||||||
className="cursor-pointer m-2 rounded-full p-1 text-xl text-black dark:text-gray-50"
|
className="cursor-pointer m-2 rounded-full p-1 text-xl text-black dark:text-gray-50"
|
||||||
|
|||||||
33
lib/search.ts
Normal file
33
lib/search.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import minisearch from "minisearch";
|
||||||
|
import { cutForSearch } from "nodejs-jieba";
|
||||||
|
import { getPostFileContent, sortedPosts } from "./post-process";
|
||||||
|
|
||||||
|
function tokenizer(str: string) {
|
||||||
|
return cutForSearch(str, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSearchIndex() {
|
||||||
|
let miniSearch = new minisearch({
|
||||||
|
fields: ["id", "title", "tags", "subtitle", "summary", "content"],
|
||||||
|
storeFields: ["id", "title", "tags"],
|
||||||
|
tokenize: tokenizer,
|
||||||
|
searchOptions: {
|
||||||
|
fuzzy: 0.1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (let index = 0; index < sortedPosts.allPostList.length; index++) {
|
||||||
|
const post = sortedPosts.allPostList[index];
|
||||||
|
const content = getPostFileContent(post.id);
|
||||||
|
miniSearch.add({
|
||||||
|
id: post.id,
|
||||||
|
title: post.frontMatter.title,
|
||||||
|
tags: post.frontMatter.tags,
|
||||||
|
subtitle: post.frontMatter.subtitle,
|
||||||
|
summary: post.frontMatter.summary,
|
||||||
|
content: content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return miniSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchIndex = makeSearchIndex();
|
||||||
939
package-lock.json
generated
939
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@
|
|||||||
"@vercel/analytics": "^1.1.1",
|
"@vercel/analytics": "^1.1.1",
|
||||||
"@vercel/speed-insights": "^1.0.2",
|
"@vercel/speed-insights": "^1.0.2",
|
||||||
"autoprefixer": "10.4.16",
|
"autoprefixer": "10.4.16",
|
||||||
|
"axios": "^1.6.4",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
@@ -45,12 +46,14 @@
|
|||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.292.0",
|
"lucide-react": "^0.292.0",
|
||||||
|
"minisearch": "^6.3.0",
|
||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.3",
|
||||||
"next": "14.0.1",
|
"next": "14.0.1",
|
||||||
"next-mdx-remote": "^4.4.1",
|
"next-mdx-remote": "^4.4.1",
|
||||||
"next-seo": "^6.4.0",
|
"next-seo": "^6.4.0",
|
||||||
"next-share": "^0.27.0",
|
"next-share": "^0.27.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
|
"nodejs-jieba": "^0.1.2",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
"prism": "^1.0.0",
|
"prism": "^1.0.0",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
@@ -59,6 +62,7 @@
|
|||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
|
"react-query": "^3.39.3",
|
||||||
"react-swipeable": "^7.0.1",
|
"react-swipeable": "^7.0.1",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-katex": "^6.0.3",
|
"rehype-katex": "^6.0.3",
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import { SpeedInsights } from "@vercel/speed-insights/next";
|
|||||||
import "katex/dist/katex.min.css";
|
import "katex/dist/katex.min.css";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
|
import { QueryClient, QueryClientProvider } from "react-query";
|
||||||
|
const queryClient = new QueryClient();
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" enableColorScheme enableSystem={false}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Analytics />
|
<ThemeProvider attribute="class" enableColorScheme enableSystem={false}>
|
||||||
<SpeedInsights />
|
<Analytics />
|
||||||
<Component {...pageProps} />
|
<SpeedInsights />
|
||||||
</ThemeProvider>
|
<Component {...pageProps} />
|
||||||
|
</ThemeProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
20
pages/api/search/[keyword].ts
Normal file
20
pages/api/search/[keyword].ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { SearchIndex } from "@/lib/search";
|
||||||
|
import { isEmptyString } from "@/lib/utils";
|
||||||
|
import { TSearchResultItem } from "@/types/search-result";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
type ResponseData = TSearchResultItem[];
|
||||||
|
|
||||||
|
export default function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
|
||||||
|
const searchText = req.query.keyword as string;
|
||||||
|
if (isEmptyString(searchText)) {
|
||||||
|
res.status(200).json([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result: TSearchResultItem[] = SearchIndex.search(searchText).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
tags: item.tags,
|
||||||
|
}));
|
||||||
|
res.status(200).json(result);
|
||||||
|
}
|
||||||
102
pages/search.tsx
Normal file
102
pages/search.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { ContentContainer, Page } from "@/components/layouts/layouts";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { Footer } from "@/components/utils/Footer";
|
||||||
|
import { NavBar } from "@/components/utils/NavBar";
|
||||||
|
import { SEO } from "@/components/utils/SEO";
|
||||||
|
import { Config } from "@/data/config";
|
||||||
|
import { fontFangZhengXiaoBiaoSongCN, fontSourceSerifScreenCN } from "@/styles/font";
|
||||||
|
import { TSearchResultItem } from "@/types/search-result";
|
||||||
|
import axios from "axios";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ChangeEvent, KeyboardEvent, useState } from "react";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
|
||||||
|
export default function SearchPage() {
|
||||||
|
const [searchText, setSearchText] = useState<string>("");
|
||||||
|
const [searchResult, setSearchResult] = useState<TSearchResultItem[]>([]);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const fetchAPI = async (param: string) => {
|
||||||
|
const response = (await axios.get<TSearchResultItem[]>(`/api/search/${param}`)).data;
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const querySearch = useQuery("searchData", () => fetchAPI(searchText), {
|
||||||
|
enabled: false,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setSearchResult(data);
|
||||||
|
if (data.length === 0) {
|
||||||
|
toast({ title: "Empty Result", description: "Change the keyword please." });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({ title: "Network Error", description: "Please try it later." });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputSearchText = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearchText(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnterKeySearch = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
(event.key === "Go" || event.key === "Enter") && handleMakeSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMakeSearch = () => {
|
||||||
|
querySearch.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<SEO title={`${Config.SiteTitle} - Search`} description={"Search the posts on your demand."} />
|
||||||
|
<NavBar />
|
||||||
|
<Toaster />
|
||||||
|
<ContentContainer>
|
||||||
|
<h2 className={`my-5 flex justify-center text-2xl font-bold ${fontFangZhengXiaoBiaoSongCN.className}`}>
|
||||||
|
{"SEARCH POSTS"}
|
||||||
|
</h2>
|
||||||
|
<div className="flex my-10 h-1/2">
|
||||||
|
<Input
|
||||||
|
className="my-auto py-0"
|
||||||
|
placeholder="Input the keyword"
|
||||||
|
value={searchText}
|
||||||
|
onKeyDown={handleEnterKeySearch}
|
||||||
|
onChange={handleInputSearchText}
|
||||||
|
/>
|
||||||
|
<Button className="mx-3 my-auto" disabled={querySearch.isLoading} onClick={handleMakeSearch}>
|
||||||
|
{querySearch.isLoading ? "Loading" : "Search"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<div className={`min-h-full flex flex-col ${fontSourceSerifScreenCN.className}`}>
|
||||||
|
{querySearch.isSuccess &&
|
||||||
|
searchResult.map((item, index) => (
|
||||||
|
<Link
|
||||||
|
className={`py-2 px-5 border-t ${
|
||||||
|
index === searchResult.length - 1 && "border-b"
|
||||||
|
} hover:bg-gray-50 dark:hover:bg-gray-900 flex flex-col`}
|
||||||
|
key={nanoid()}
|
||||||
|
href={`/blog/${item.id}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div className="my-1 capitalize">{item.title}</div>
|
||||||
|
<div className="flex space-x-2 flex-wrap">
|
||||||
|
{item.tags?.map((tagitem) => (
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400" key={nanoid()}>
|
||||||
|
{tagitem}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ContentContainer>
|
||||||
|
<Footer />
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
types/search-result.ts
Normal file
5
types/search-result.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type TSearchResultItem = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
tags: string[] | null;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user