2024-01-06 11:47:18 +08:00
|
|
|
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";
|
2024-08-16 14:12:30 +08:00
|
|
|
import { ContentContainer, Page } from "@/components/utils/Layout";
|
2024-01-06 11:47:18 +08:00
|
|
|
import { NavBar } from "@/components/utils/NavBar";
|
|
|
|
|
import { SEO } from "@/components/utils/SEO";
|
|
|
|
|
import { Config } from "@/data/config";
|
2024-01-06 20:20:05 +08:00
|
|
|
import { isEmptyString } from "@/lib/utils";
|
2024-08-14 12:57:22 +08:00
|
|
|
import type { TSearchResultItem } from "@/types/search-result";
|
2024-01-06 11:47:18 +08:00
|
|
|
import axios from "axios";
|
2024-08-12 10:57:33 +08:00
|
|
|
import { isArray } from "lodash";
|
2024-01-06 11:47:18 +08:00
|
|
|
import { nanoid } from "nanoid";
|
2024-08-14 12:57:22 +08:00
|
|
|
import type { GetServerSideProps } from "next";
|
2024-01-06 11:47:18 +08:00
|
|
|
import Link from "next/link";
|
2024-08-12 10:57:33 +08:00
|
|
|
import { useRouter } from "next/router";
|
2024-08-14 12:57:22 +08:00
|
|
|
import { type ChangeEvent, type KeyboardEvent, useEffect, useState } from "react";
|
2024-01-06 11:47:18 +08:00
|
|
|
|
2024-08-12 10:57:33 +08:00
|
|
|
type SearchPageProps = { query: string | null };
|
|
|
|
|
|
|
|
|
|
export default function SearchPage(props: SearchPageProps) {
|
|
|
|
|
const [searchText, setSearchText] = useState<string>(props.query ?? "");
|
2024-01-06 11:47:18 +08:00
|
|
|
const [searchResult, setSearchResult] = useState<TSearchResultItem[]>([]);
|
2024-08-12 10:57:33 +08:00
|
|
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
2024-01-06 11:47:18 +08:00
|
|
|
const { toast } = useToast();
|
2024-08-12 10:57:33 +08:00
|
|
|
const router = useRouter();
|
2024-01-06 11:47:18 +08:00
|
|
|
|
2024-08-12 10:57:33 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isEmptyString(searchText)) {
|
|
|
|
|
handleMakeSearch();
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
2024-01-06 11:47:18 +08:00
|
|
|
|
2024-08-12 10:57:33 +08:00
|
|
|
const fetchSearchAPI = (param: string): Promise<TSearchResultItem[]> => {
|
|
|
|
|
return axios.get<TSearchResultItem[]>(`/api/search/${param}`).then((response) => response.data);
|
|
|
|
|
};
|
2024-01-06 11:47:18 +08:00
|
|
|
|
|
|
|
|
const handleInputSearchText = (event: ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
setSearchText(event.target.value);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEnterKeySearch = (event: KeyboardEvent<HTMLInputElement>) => {
|
2024-08-12 10:57:33 +08:00
|
|
|
if (event.key === "Go" || event.key === "Enter") {
|
|
|
|
|
handleMakeSearch();
|
|
|
|
|
}
|
2024-01-06 11:47:18 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleMakeSearch = () => {
|
2024-08-12 10:57:33 +08:00
|
|
|
const searchQuery = searchText;
|
|
|
|
|
|
|
|
|
|
if (isEmptyString(searchQuery)) {
|
2024-01-06 20:20:05 +08:00
|
|
|
toast({ title: "Enter a Keyword", description: "Please enter one keyword at least." });
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-08-12 10:57:33 +08:00
|
|
|
if (searchQuery && searchQuery.length < 4) {
|
2024-01-15 11:44:48 +08:00
|
|
|
toast({ title: "Keywords too short", description: "Keyword length must be at least 5." });
|
2024-01-09 16:57:42 +08:00
|
|
|
return;
|
|
|
|
|
}
|
2024-08-12 10:57:33 +08:00
|
|
|
|
|
|
|
|
router.push({
|
|
|
|
|
pathname: router.pathname,
|
|
|
|
|
query: { ...router.query, q: searchQuery },
|
|
|
|
|
});
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
|
|
|
|
fetchSearchAPI(searchQuery)
|
|
|
|
|
.then((data) => {
|
|
|
|
|
setSearchResult(data);
|
|
|
|
|
if (data.length === 0) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "Empty Result",
|
|
|
|
|
description: "No results were found for this keyword. Try another keyword.",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
toast({ title: "Network Error", description: "Please try it later." });
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
});
|
2024-01-06 11:47:18 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Page>
|
2024-04-03 22:08:27 +08:00
|
|
|
<SEO description={"Search the posts on your demand."} title={`${Config.SiteTitle} - Search`} />
|
2024-01-06 11:47:18 +08:00
|
|
|
<Toaster />
|
2024-01-09 16:48:48 +08:00
|
|
|
<NavBar />
|
2024-01-06 11:47:18 +08:00
|
|
|
<ContentContainer>
|
2024-08-14 12:57:22 +08:00
|
|
|
<h2 className={"caption-font my-10 flex justify-center font-bold text-2xl"}>{"SEARCH POSTS"}</h2>
|
|
|
|
|
<div className="my-10 flex">
|
2024-01-06 11:47:18 +08:00
|
|
|
<Input
|
|
|
|
|
className="my-auto py-0"
|
2024-04-03 22:08:27 +08:00
|
|
|
onChange={handleInputSearchText}
|
|
|
|
|
onKeyDown={handleEnterKeySearch}
|
2024-01-06 11:47:18 +08:00
|
|
|
placeholder="Input the keyword"
|
|
|
|
|
value={searchText}
|
|
|
|
|
/>
|
2024-08-14 12:57:22 +08:00
|
|
|
<Button className="mx-3 my-auto w-32" disabled={isLoading} onClick={handleMakeSearch}>
|
2024-08-12 10:57:33 +08:00
|
|
|
{isLoading ? "Loading" : "Search"}
|
2024-01-06 11:47:18 +08:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col justify-center">
|
2024-08-14 12:57:22 +08:00
|
|
|
<div className={"flex min-h-full flex-col content-font"}>
|
2024-08-12 10:57:33 +08:00
|
|
|
{searchResult.map((item, index) => (
|
|
|
|
|
<Link
|
2024-08-14 12:57:22 +08:00
|
|
|
className={`border-t p-2 ${index === searchResult.length - 1 && "border-b"} flex flex-col hover:bg-gray-50 dark:hover:bg-gray-900`}
|
2024-08-12 10:57:33 +08:00
|
|
|
href={`/blog/${item.id}`}
|
|
|
|
|
key={nanoid()}
|
|
|
|
|
target="_blank"
|
|
|
|
|
>
|
|
|
|
|
<div className="my-1">
|
2024-08-14 12:57:22 +08:00
|
|
|
<div className="post-list-caption-font font-bold text-md capitalize">{item.title}</div>
|
2024-08-12 10:57:33 +08:00
|
|
|
{item.summary && <div>{item.summary}</div>}
|
|
|
|
|
</div>
|
2024-08-14 12:57:22 +08:00
|
|
|
<div className="flex flex-wrap space-x-2">
|
2024-08-12 10:57:33 +08:00
|
|
|
{item.tags?.map((tagitem) => (
|
2024-08-14 12:57:22 +08:00
|
|
|
<div className="text-gray-500 text-sm dark:text-gray-400" key={nanoid()}>
|
2024-08-12 10:57:33 +08:00
|
|
|
{tagitem}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
2024-01-06 11:47:18 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-08-14 12:57:22 +08:00
|
|
|
<div className="my-3 text-center text-gray-500 dark:text-gray-400">
|
2024-08-12 10:57:33 +08:00
|
|
|
<p className="mx-auto text-sm">{"For search efficiency, only the first 20 results are displayed."}</p>
|
|
|
|
|
</div>
|
2024-01-06 11:47:18 +08:00
|
|
|
</ContentContainer>
|
|
|
|
|
<Footer />
|
|
|
|
|
</Page>
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-08-12 10:57:33 +08:00
|
|
|
|
|
|
|
|
export const getServerSideProps: GetServerSideProps<SearchPageProps> = async (context) => {
|
|
|
|
|
let query = context.query.q;
|
|
|
|
|
if (isArray(query)) query = query.join(" ");
|
|
|
|
|
return { props: { query: query ?? null } };
|
|
|
|
|
};
|