[upgrade] Abstract components on the page into subcomponents
This commit is contained in:
		
							
								
								
									
										19
									
								
								components/about-page/Introduction.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								components/about-page/Introduction.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| export const Introduction = () => { | ||||
|   return ( | ||||
|     <div className={"my-5 justify-center content-font md:flex md:space-x-10"}> | ||||
|       <div className="my-auto flex md:w-1/3"> | ||||
|         <img alt="my-profile" className="mx-auto my-auto max-h-[23rem] rounded-lg" src="/images/profile.webp" /> | ||||
|       </div> | ||||
|       <div className="my-auto md:w-1/3"> | ||||
|         <div className="mt-5 mb-3 font-bold text-3xl">Hi, there👋</div>I am a student / entrepreneur / engineer (Your | ||||
|         profession) majoring in (Your Research Field) born in XXXX (Your birth year) | ||||
|         <br /> | ||||
|         <br /> | ||||
|         My main research interests includes XXXX | ||||
|         <br /> | ||||
|         <br /> | ||||
|         Additionally, I am also interested in XXXX. | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										30
									
								
								components/about-page/PersonalStatus.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								components/about-page/PersonalStatus.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { Config } from "@/data/config"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| export const PersonalStatus = () => { | ||||
|   return ( | ||||
|     <ul className="mx-auto my-10 list-disc px-5 md:w-2/3"> | ||||
|       {Config.SocialLinks.github && ( | ||||
|         <li className="my-2"> | ||||
|           {"📕 Check out my github profile at "} | ||||
|           <Link className="underline" href={`https://github.com/${Config.SocialLinks.github}`} target="_blank"> | ||||
|             Github | ||||
|           </Link> | ||||
|         </li> | ||||
|       )} | ||||
|       <li className="my-2">🖥️ Programming stack: TypeScript, JavaScript, C++, C, Rust, Go and so on.</li> | ||||
|       <li className="my-2">🤝 I am looking for friends who are fund of XXXX</li> | ||||
|       {Config.SocialLinks.twitter && ( | ||||
|         <li className="my-2"> | ||||
|           {"📫 How to reach me on Twitter: "} | ||||
|           <Link className="link" href={`https://twitter.com/${Config.SocialLinks.twitter}`} target="_blank"> | ||||
|             {Config.SocialLinks.twitter} | ||||
|           </Link> | ||||
|         </li> | ||||
|       )} | ||||
|       <li className="my-2">Language : 汉语 / English / 한국어 / 日本語 </li> | ||||
|       <li className="my-2">Pronouns : Male / Female / MTF / FTM / And Others</li> | ||||
|       <li className="my-2">From : Your Country, State / Province</li> | ||||
|     </ul> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										29
									
								
								components/friend-links-page/FriendLinksList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								components/friend-links-page/FriendLinksList.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import { Separator } from "@/components/ui/separator"; | ||||
| import { Config } from "@/data/config"; | ||||
| import { FriendsList } from "@/data/friends"; | ||||
| import type { TFriendItem } from "@/types/friend.type"; | ||||
| import { nanoid } from "nanoid"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| export const FriendLinkList = (props: { friends: TFriendItem[] }) => { | ||||
|   return ( | ||||
|     <div> | ||||
|       <div className={"my-5 flex flex-wrap justify-center text-2xl content-font"}> | ||||
|         {FriendsList.map((item) => ( | ||||
|           <Link className="mx-2 p-2 underline underline-offset-4" href={item.url} key={nanoid()} target="_blank"> | ||||
|             {item.title} | ||||
|           </Link> | ||||
|         ))} | ||||
|       </div> | ||||
|       <Separator /> | ||||
|       <div className="my-2 flex flex-col justify-start text-base"> | ||||
|         <div className="mx-auto"> | ||||
|           {"Welcome to exchange our friend links and every high-quality blog websites are welcomed. "} | ||||
|           <Link className="underline" href={`mailto:${Config.SocialLinks.email}`}> | ||||
|             {"Email me please"} | ||||
|           </Link> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										29
									
								
								components/post-list-page/TagsList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								components/post-list-page/TagsList.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion"; | ||||
| import { Separator } from "@/components/ui/separator"; | ||||
| import Link from "next/link"; | ||||
| import { nanoid } from "nanoid"; | ||||
| import type { TTagListItem } from "@/types/docs.type"; | ||||
|  | ||||
| export const TagsList = (props: { tagsList: TTagListItem[] }) => { | ||||
|   return ( | ||||
|     <Accordion collapsible type="single"> | ||||
|       <AccordionItem className="border-t" value="item-1"> | ||||
|         <AccordionTrigger className="font-bold hover:no-underline">{"TAG FILTER"}</AccordionTrigger> | ||||
|         <AccordionContent> | ||||
|           <Separator /> | ||||
|           <div className={"my-5 flex flex-wrap justify-center text-wrap px-2 text-sm"}> | ||||
|             {props.tagsList.map((item) => ( | ||||
|               <Link | ||||
|                 className="m-1 my-auto p-1 font-bold text-gray-700 underline decoration-2 underline-offset-[5px] hover:text-black dark:text-gray-300 dark:hover:text-white" | ||||
|                 href={`/tags/${item.name}`} | ||||
|                 key={`tags-${nanoid()}`} | ||||
|               > | ||||
|                 {`${item.name} (${item.count})`} | ||||
|               </Link> | ||||
|             ))} | ||||
|           </div> | ||||
|         </AccordionContent> | ||||
|       </AccordionItem> | ||||
|     </Accordion> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,10 +1,14 @@ | ||||
| import seal from "@/assets/icons/seal.svg"; | ||||
| import { Config } from "@/data/config"; | ||||
|  | ||||
| export const BottomCard = () => { | ||||
|   return ( | ||||
|     <div className="flex w-full flex-col justify-center p-8"> | ||||
|       <img alt={Config.AuthorName} className="mx-auto h-24 w-24 rounded-full" src={Config.AvatarURL} /> | ||||
|       <p className="mx-auto mt-5 content-font">{Config.Sentence}</p> | ||||
|     <div className="flex w-full select-none flex-col justify-center p-8"> | ||||
|       <div | ||||
|         className="mx-auto h-24 w-24" | ||||
|         style={{ backgroundImage: `url(${seal.src})`, backgroundRepeat: "no-repeat", backgroundSize: "contain" }} | ||||
|       /> | ||||
|       <p className={"mx-auto mt-5 content-font"}>{Config.Sentence}</p> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; | ||||
| import { useActiveHeading } from "@/hooks/useActiveHeading"; | ||||
| import useDrawerTOCState from "@/stores/useDrawerTOCState"; | ||||
| import type { TTOCItem } from "@/types/docs.type"; | ||||
| import type { TPostTOCItem } from "@/types/docs.type"; | ||||
| import Link from "next/link"; | ||||
| import { MdMenuBook } from "react-icons/md"; | ||||
| import { twMerge } from "tailwind-merge"; | ||||
|  | ||||
| export const DrawerTOC = (props: { data: TTOCItem[] }) => { | ||||
| export const DrawerTOC = (props: { data: TPostTOCItem[] }) => { | ||||
|   const isTOCOpen = useDrawerTOCState((state) => state.isOpen); | ||||
|   const setIsTOCOpen = useDrawerTOCState((state) => state.changeDrawerTOCOpen); | ||||
|   const activeId = useActiveHeading(props.data.map((item) => `#${item.anchorId}`)); | ||||
| @@ -16,13 +16,18 @@ export const DrawerTOC = (props: { data: TTOCItem[] }) => { | ||||
|         className="fixed right-5 bottom-16 border border-gray-700 bg-white shadow-xl dark:border-gray-500 dark:bg-black" | ||||
|         title="Open the table of contents" | ||||
|       > | ||||
|         <div className="p-1 font-bold" onClick={() => setIsTOCOpen(!isTOCOpen)} title="Open the table of contents"> | ||||
|         <div | ||||
|           className="p-1 font-bold" | ||||
|           onClick={() => setIsTOCOpen(!isTOCOpen)} | ||||
|           onKeyDown={() => {}} | ||||
|           title="Open the table of contents" | ||||
|         > | ||||
|           <MdMenuBook className="text-3xl" /> | ||||
|         </div> | ||||
|       </SheetTrigger> | ||||
|       <SheetContent side={"left"}> | ||||
|       <SheetContent side={"right"}> | ||||
|         <SheetHeader> | ||||
|           <SheetTitle className="mt-8 font-bold">{"TABLE OF CONTENTS"}</SheetTitle> | ||||
|           <SheetTitle className="mt-8 text-center font-bold text-base">{"TABLE OF CONTENTS"}</SheetTitle> | ||||
|         </SheetHeader> | ||||
|         <ul className="flat-scrollbar flat-scrollbar-normal my-3 flex h-[70vh] flex-col overflow-y-auto"> | ||||
|           {props.data?.map((item) => ( | ||||
|   | ||||
							
								
								
									
										26
									
								
								components/reader-page/MorePostLinks.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								components/reader-page/MorePostLinks.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import type { TPostListItem } from "@/types/docs.type"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| export const MorePostLinks = (props: { | ||||
|   prevPostListItem: TPostListItem | null; | ||||
|   nextPostListItem: TPostListItem | null; | ||||
| }) => { | ||||
|   return ( | ||||
|     <ul className="my-5 flex list-disc flex-col justify-center px-5"> | ||||
|       {props.prevPostListItem && ( | ||||
|         <li className="my-1"> | ||||
|           <Link className=" hover:text-sky-600 dark:hover:text-sky-500" href={`/blog/${props.prevPostListItem?.id}`}> | ||||
|             {props.prevPostListItem?.frontMatter.title} | ||||
|           </Link> | ||||
|         </li> | ||||
|       )} | ||||
|       {props.nextPostListItem && ( | ||||
|         <li className="my-1"> | ||||
|           <Link className=" hover:text-sky-600 dark:hover:text-sky-500" href={`/blog/${props.nextPostListItem?.id}`}> | ||||
|             {props.nextPostListItem?.frontMatter.title} | ||||
|           </Link> | ||||
|         </li> | ||||
|       )} | ||||
|     </ul> | ||||
|   ); | ||||
| }; | ||||
| @@ -11,7 +11,6 @@ export const PostComments = (props: { postId: string }) => { | ||||
|           category={Config.Giscus.category} | ||||
|           categoryId={Config.Giscus.categoryId} | ||||
|           emitMetadata="0" | ||||
|           id={props.postId} | ||||
|           inputPosition="top" | ||||
|           lang="en" | ||||
|           loading="eager" | ||||
|   | ||||
| @@ -3,7 +3,7 @@ export const PostCover = (props: { coverURL: string }) => { | ||||
|     <div | ||||
|       className="mt-0 mb-8 flex w-full justify-center rounded-md" | ||||
|       style={{ | ||||
|         aspectRatio: "5/2", | ||||
|         aspectRatio: "5/1", | ||||
|         background: `url(${props.coverURL})`, | ||||
|         backgroundSize: "cover", | ||||
|         backgroundRepeat: "no-repeat", | ||||
|   | ||||
							
								
								
									
										70
									
								
								components/reader-page/PostRender.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								components/reader-page/PostRender.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| import { MDXComponentsSet } from "@/components/mdx"; | ||||
| import { normalizeDate } from "@/lib/date"; | ||||
| import type { TPostFrontmatter, TPostListItem, TPostTOCItem } from "@/types/docs.type"; | ||||
| import { nanoid } from "nanoid"; | ||||
| import { MDXRemote, type MDXRemoteSerializeResult } from "next-mdx-remote"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| export const PostRender = (props: { | ||||
|   compiledSource: MDXRemoteSerializeResult; | ||||
|   tocList: TPostTOCItem[]; | ||||
|   frontMatter: TPostFrontmatter; | ||||
|   postId: string; | ||||
|   nextPostListItem: TPostListItem | null; | ||||
|   prevPostListItem: TPostListItem | null; | ||||
| }) => { | ||||
|   const compiledSource = props.compiledSource; | ||||
|   return ( | ||||
|     <div className="typesetting"> | ||||
|       <div className="border-black border-b-2 pb-1 dark:border-gray-300"> | ||||
|         <div | ||||
|           className={ | ||||
|             "caption-font my-2 flex justify-center whitespace-normal break-words font-bold text-[1.8rem] text-black leading-[2.1rem] dark:text-white" | ||||
|           } | ||||
|         > | ||||
|           {props.frontMatter?.title} | ||||
|         </div> | ||||
|         {props.frontMatter?.subtitle && ( | ||||
|           <div className={"my-1 flex justify-center font-bold text-xl content-font"}>{props.frontMatter.subtitle}</div> | ||||
|         )} | ||||
|         <div className="my-1 flex justify-center text-sm italic">{normalizeDate(props.frontMatter?.time)}</div> | ||||
|         {props.frontMatter?.summary && ( | ||||
|           <p | ||||
|             className={ | ||||
|               "my-4 border-gray-400 border-l-4 bg-gray-100 py-5 pr-2 pl-5 text-[16px] text-gray-800 content-font dark:border-gray-600 dark:bg-gray-900 dark:text-gray-300" | ||||
|             } | ||||
|           > | ||||
|             {props.frontMatter?.summary} | ||||
|           </p> | ||||
|         )} | ||||
|         {props.frontMatter.tags && ( | ||||
|           <div className={"flex flex-wrap border-black border-t-2 pt-1 dark:border-gray-300"}> | ||||
|             {props.frontMatter.tags.map((tagName) => ( | ||||
|               <Link | ||||
|                 className="not-prose my-1 mr-2 border-2 border-black px-2 py-1 font-bold text-gray-700 text-xs hover:text-black dark:border-gray-300 dark:text-gray-300 dark:hover:text-gray-200" | ||||
|                 href={`/tags/${tagName}`} | ||||
|                 key={`tags-${nanoid()}`} | ||||
|               > | ||||
|                 {tagName} | ||||
|               </Link> | ||||
|             ))} | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|       <div | ||||
|         className={`text-wrap border-gray-500 content-font ${!props.frontMatter.allowShare ? "select-none" : ""}`} | ||||
|         // {...handleLeftSwipe} | ||||
|       > | ||||
|         {compiledSource && ( | ||||
|           <MDXRemote | ||||
|             compiledSource={compiledSource.compiledSource} | ||||
|             // @ts-ignore | ||||
|             components={MDXComponentsSet} | ||||
|             frontmatter={compiledSource.frontmatter} | ||||
|             scope={compiledSource.scope} | ||||
|           /> | ||||
|         )} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,13 +1,13 @@ | ||||
| import { useActiveHeading } from "@/hooks/useActiveHeading"; | ||||
| import type { TTOCItem } from "@/types/docs.type"; | ||||
| import type { TPostTOCItem } from "@/types/docs.type"; | ||||
| import Link from "next/link"; | ||||
| import { twMerge } from "tailwind-merge"; | ||||
|  | ||||
| export const TOC = (props: { data: TTOCItem[] }) => { | ||||
| export const TOC = (props: { data: TPostTOCItem[] }) => { | ||||
|   const activeId = useActiveHeading(props.data.map((item) => `#${item.anchorId}`)); | ||||
|  | ||||
|   return ( | ||||
|     <div className="mx-5"> | ||||
|     <div className="mr-5"> | ||||
|       <div className="border-gray-500 border-t-2 border-b-2 p-2 text-center font-bold text-lg"> | ||||
|         {"TABLE OF CONTENTS"} | ||||
|       </div> | ||||
|   | ||||
							
								
								
									
										43
									
								
								components/search-page/SearchInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								components/search-page/SearchInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { Input } from "@/components/ui/input"; | ||||
| import { isEmptyString } from "@/lib/utils"; | ||||
| import { type ChangeEvent, type KeyboardEvent, useEffect, useState } from "react"; | ||||
|  | ||||
| export const SearchInput = (props: { | ||||
|   handleSearch: (word: string) => any; | ||||
|   isLoading: boolean; | ||||
|   word?: string | null; | ||||
| }) => { | ||||
|   const [searchText, setSearchText] = useState<string>(props.word ?? ""); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!isEmptyString(searchText)) { | ||||
|       props.handleSearch(searchText); | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   const handleInputSearchText = (event: ChangeEvent<HTMLInputElement>) => { | ||||
|     setSearchText(event.target.value); | ||||
|   }; | ||||
|  | ||||
|   const handleEnterKeySearch = (event: KeyboardEvent<HTMLInputElement>) => { | ||||
|     if (event.key === "Go" || event.key === "Enter") { | ||||
|       props.handleSearch(searchText); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div className="my-10 flex"> | ||||
|       <Input | ||||
|         className="my-auto py-0" | ||||
|         onChange={handleInputSearchText} | ||||
|         onKeyDown={handleEnterKeySearch} | ||||
|         placeholder="Input the keyword" | ||||
|         value={searchText} | ||||
|       /> | ||||
|       <Button className="mx-3 my-auto w-32" disabled={props.isLoading} onClick={() => props.handleSearch(searchText)}> | ||||
|         {props.isLoading ? "Loading" : "Search"} | ||||
|       </Button> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										41
									
								
								components/search-page/SearchResultList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								components/search-page/SearchResultList.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import type { TSearchResultItem } from "@/types/docs.type"; | ||||
| import { nanoid } from "nanoid"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| export const SearchResultList = (props: { | ||||
|   searchResult: TSearchResultItem[]; | ||||
| }) => { | ||||
|   return ( | ||||
|     <div> | ||||
|       <div className="flex flex-col justify-center"> | ||||
|         <div className={"flex min-h-full flex-col content-font"}> | ||||
|           {props.searchResult.map((item, index) => ( | ||||
|             <Link | ||||
|               className={`border-t p-2 ${ | ||||
|                 index === props.searchResult.length - 1 && "border-b" | ||||
|               } flex flex-col hover:bg-gray-50 dark:hover:bg-gray-900`} | ||||
|               href={`/blog/${item.id}`} | ||||
|               key={nanoid()} | ||||
|               target="_blank" | ||||
|             > | ||||
|               <div className="my-1"> | ||||
|                 <div className="post-list-caption-font font-bold text-md capitalize">{item.title}</div> | ||||
|                 {item.summary && <div className="">{item.summary}</div>} | ||||
|               </div> | ||||
|               <div className="flex flex-wrap space-x-2"> | ||||
|                 {item.tags?.map((tagitem) => ( | ||||
|                   <div className="text-gray-500 text-sm dark:text-gray-400" key={nanoid()}> | ||||
|                     {tagitem} | ||||
|                   </div> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </Link> | ||||
|           ))} | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className="my-3 text-center text-gray-500 dark:text-gray-400"> | ||||
|         <p className="mx-auto text-sm">{"For search efficiency, only the first 20 results are displayed."}</p> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										12
									
								
								components/utils/PageTitle.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								components/utils/PageTitle.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { twMerge } from "tailwind-merge"; | ||||
|  | ||||
| export const PageTitle = (props: { | ||||
|   children?: React.ReactNode; | ||||
|   classNames?: string; | ||||
| }) => { | ||||
|   return ( | ||||
|     <h2 className={twMerge("caption-font my-5 flex select-none justify-center font-bold text-2xl", props.classNames)}> | ||||
|       {props.children} | ||||
|     </h2> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										63
									
								
								components/utils/Pagination.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								components/utils/Pagination.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { Input } from "@/components/ui/input"; | ||||
| import { type ChangeEvent, type KeyboardEvent, useState } from "react"; | ||||
| import { MdNavigateBefore, MdNavigateNext } from "react-icons/md"; | ||||
|  | ||||
| export const Pagination = (props: { | ||||
|   pageNumber: number; | ||||
|   pageAmount: number; | ||||
|   onGotoNextPage: (nextPage: number) => any; | ||||
|   onGotoPrevPage: (prevPage: number) => any; | ||||
|   onJumpToSpecPage: (pageNum: number) => any; | ||||
| }) => { | ||||
|   const [pageNumberInput, setPageNumberInput] = useState<string>(props.pageNumber.toString()); | ||||
|  | ||||
|   const handleEnterKeyJump = (event: KeyboardEvent<HTMLInputElement>) => { | ||||
|     setPageNumberInput(pageNumberInput.replace(/[^\d]/g, "")); | ||||
|     if (Number.parseInt(pageNumberInput) > 0 && Number.parseInt(pageNumberInput) < props.pageAmount + 1) { | ||||
|       (event.key === "Go" || event.key === "Enter") && props.onJumpToSpecPage(Number.parseInt(pageNumberInput)); | ||||
|       return; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleInputPageNumber = (event: ChangeEvent<HTMLInputElement>) => { | ||||
|     setPageNumberInput(event.target.value); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div className="my-5 flex justify-between font-bold text-base rtl:flex-row-reverse"> | ||||
|       {props.pageNumber !== 1 && ( | ||||
|         <Button | ||||
|           className="rounded-full" | ||||
|           onClick={() => { | ||||
|             setPageNumberInput((props.pageNumber - 1).toString()); | ||||
|             props.onGotoPrevPage(props.pageNumber - 1); | ||||
|           }} | ||||
|         > | ||||
|           <MdNavigateBefore className="text-3xl" /> | ||||
|         </Button> | ||||
|       )} | ||||
|       <div className="my-auto flex justify-center font-bold"> | ||||
|         <Input | ||||
|           className="mx-2 my-auto h-6 w-11" | ||||
|           onChange={handleInputPageNumber} | ||||
|           onKeyDown={handleEnterKeyJump} | ||||
|           title="Type the specified page number and press Enter to jump." | ||||
|           value={pageNumberInput} | ||||
|         /> | ||||
|         <div className="my-auto">{`  /  ${props.pageAmount}`}</div> | ||||
|       </div> | ||||
|       {props.pageNumber !== props.pageAmount && ( | ||||
|         <Button | ||||
|           className="rounded-full" | ||||
|           onClick={() => { | ||||
|             setPageNumberInput((props.pageNumber + 1).toString()); | ||||
|             props.onGotoNextPage(props.pageNumber + 1); | ||||
|           }} | ||||
|         > | ||||
|           <MdNavigateNext className="text-3xl" /> | ||||
|         </Button> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -37,7 +37,7 @@ export const PostList = (props: { data: TPostListItem[] }) => { | ||||
|               </div> | ||||
|               {postItem.frontMatter.tags && ( | ||||
|                 <div className="my-auto flex flex-wrap"> | ||||
|                   {postItem.frontMatter.tags.map((tagName) => ( | ||||
|                   {postItem.frontMatter.tags.map((tagName: string) => ( | ||||
|                     <Badge | ||||
|                       className="my-1 mr-1 text-gray-600 dark:text-gray-300" | ||||
|                       key={`tags-${nanoid()}`} | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
| import { PostFilesDirectory } from "@/consts/consts"; | ||||
| import type { TPostFrontmatter } from "@/types/frontmatter.type"; | ||||
| import type { TPostListItem, TPostsByTag } from "@/types/docs.type"; | ||||
| import type { TPostFrontmatter, TPostListItem, TPostsByTag } from "@/types/docs.type"; | ||||
| import { serialize } from "next-mdx-remote/serialize"; | ||||
| import { titleCase } from "title-case"; | ||||
| import { isEmptyString, nullifyEmptyArray, nullifyEmptyString } from "./utils"; | ||||
| @@ -93,7 +92,7 @@ const sortOutPosts = async (): Promise<{ | ||||
|   }); | ||||
|  | ||||
|   allPostList.forEach((item) => { | ||||
|     item.frontMatter.tags?.forEach((tagName) => { | ||||
|     item.frontMatter.tags?.forEach((tagName: string) => { | ||||
|       if (postsByTag[tagName] == null) { | ||||
|         postsByTag[tagName] = []; | ||||
|       } | ||||
|   | ||||
| @@ -65,7 +65,7 @@ export const generateRSSFeed = async () => { | ||||
|   for (let i = 0; i < Math.min(LatestPostCountInHomePage, sortedPosts.allPostList.length); i++) { | ||||
|     const post = sortedPosts.allPostList[i]; | ||||
|     const postFileContent = `${getPostFileContent(post.id)}${NoticeForRSSReaders(post.id)}`; | ||||
|     const dateNumber = post.frontMatter.time.split("-").map((num) => Number.parseInt(num)); | ||||
|     const dateNumber = post.frontMatter.time.split("-").map((num: string) => Number.parseInt(num)); | ||||
|     const mdxSource = await serialize(postFileContent ?? "", { | ||||
|       parseFrontmatter: true, | ||||
|       mdxOptions: { | ||||
| @@ -89,7 +89,9 @@ export const generateRSSFeed = async () => { | ||||
|           link: `https://${Config.SiteDomain}/about`, | ||||
|         }, | ||||
|       ], | ||||
|       category: post.frontMatter.tags?.map((tagname) => ({ name: tagname })), | ||||
|       category: post.frontMatter.tags?.map((tagname: string) => ({ | ||||
|         name: tagname, | ||||
|       })), | ||||
|       date: new Date(dateNumber[0], dateNumber[1], dateNumber[2]), | ||||
|       image: post.frontMatter.coverURL ?? undefined, | ||||
|     }); | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import type { TTOCItem } from "@/types/docs.type"; | ||||
| import type { TPostTOCItem } from "@/types/docs.type"; | ||||
| import { JSDOM } from "jsdom"; | ||||
|  | ||||
| /** | ||||
| @@ -10,7 +10,7 @@ import { JSDOM } from "jsdom"; | ||||
| export const makeTOCTree = (htmlCode: string) => { | ||||
|   const doc_dom = new JSDOM(htmlCode); | ||||
|   const all_headers = doc_dom.window.document.querySelectorAll("h1,h2,h3,h4,h5,h6"); | ||||
|   const result: TTOCItem[] = []; | ||||
|   const result: TPostTOCItem[] = []; | ||||
|   for (let i = 0; i < all_headers.length; i++) { | ||||
|     const level = Number.parseInt(all_headers[i].tagName.replace("H", "")); | ||||
|     result.push({ | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { Separator } from "@/components/ui/separator"; | ||||
| import { Footer } from "@/components/utils/Footer"; | ||||
| import { ContentContainer, Page } from "@/components/utils/Layout"; | ||||
| import { NavBar } from "@/components/utils/NavBar"; | ||||
| import { PageTitle } from "@/components/utils/PageTitle"; | ||||
| import { TfiFaceSad } from "react-icons/tfi"; | ||||
|  | ||||
| export default function NotFoundPage() { | ||||
| @@ -14,7 +15,7 @@ export default function NotFoundPage() { | ||||
|     <Page> | ||||
|       <NavBar /> | ||||
|       <ContentContainer> | ||||
|         <h2 className={"caption-font my-5 flex justify-center font-bold text-2xl"}>{"404 NOT FOUND"}</h2> | ||||
|         <PageTitle>{"404 NOT FOUND"}</PageTitle> | ||||
|         <Separator /> | ||||
|         <div className="my-5 flex flex-col justify-center"> | ||||
|           <TfiFaceSad className="mx-auto my-4" size={"6em"} /> | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { Separator } from "@/components/ui/separator"; | ||||
| import { Footer } from "@/components/utils/Footer"; | ||||
| import { ContentContainer, Page } from "@/components/utils/Layout"; | ||||
| import { NavBar } from "@/components/utils/NavBar"; | ||||
| import { PageTitle } from "@/components/utils/PageTitle"; | ||||
| import { MdOutlineDangerous } from "react-icons/md"; | ||||
|  | ||||
| export default function ServerErrorPage() { | ||||
| @@ -14,7 +15,7 @@ export default function ServerErrorPage() { | ||||
|     <Page> | ||||
|       <NavBar /> | ||||
|       <ContentContainer> | ||||
|         <h2 className={"caption-font my-5 flex justify-center font-bold text-2xl"}>{"INVALID OPERATION"}</h2> | ||||
|         <PageTitle>{"INVALID OPERATION"}</PageTitle> | ||||
|         <Separator /> | ||||
|         <div className="my-5 flex flex-col justify-center"> | ||||
|           <MdOutlineDangerous className="mx-auto my-4" size={"6em"} /> | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| import { Introduction } from "@/components/about-page/Introduction"; | ||||
| import { Separator } from "@/components/ui/separator"; | ||||
| import { Footer } from "@/components/utils/Footer"; | ||||
| import { ContentContainer, Page } from "@/components/utils/Layout"; | ||||
| import { NavBar } from "@/components/utils/NavBar"; | ||||
| import { PageTitle } from "@/components/utils/PageTitle"; | ||||
| import { SEO } from "@/components/utils/SEO"; | ||||
| import { SocialIcons } from "@/components/utils/SocialIcons"; | ||||
| import { Config } from "@/data/config"; | ||||
| @@ -18,23 +20,9 @@ export default function AboutPage() { | ||||
|       /> | ||||
|       <NavBar /> | ||||
|       <ContentContainer> | ||||
|         <h2 className={"caption-font my-5 flex justify-around font-bold text-2xl"}>{"ABOUT ME"}</h2> | ||||
|         <PageTitle>{"ABOUT ME"}</PageTitle> | ||||
|         <Separator /> | ||||
|         <div className={"my-5 justify-center content-font md:flex md:space-x-10"}> | ||||
|           <div className="my-auto flex md:w-1/3"> | ||||
|             <img alt="my-profile" className="mx-auto my-auto max-h-[23rem] rounded-lg" src="/images/profile.webp" /> | ||||
|           </div> | ||||
|           <div className="my-auto md:w-1/3"> | ||||
|             <div className="mt-5 mb-3 font-bold text-3xl">Hi, there👋</div>I am a student / entrepreneur / engineer | ||||
|             (Your profession) majoring in (Your Research Field) born in XXXX (Your birth year) | ||||
|             <br /> | ||||
|             <br /> | ||||
|             My main research interests includes XXXX | ||||
|             <br /> | ||||
|             <br /> | ||||
|             Additionally, I am also interested in XXXX. | ||||
|           </div> | ||||
|         </div> | ||||
|         <Introduction /> | ||||
|         <Separator /> | ||||
|         <SocialIcons /> | ||||
|         <Separator /> | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import { MDXComponentsSet } from "@/components/mdx"; | ||||
| import { DrawerTOC } from "@/components/reader-page/DrawerTOC"; | ||||
| import { MorePostLinks } from "@/components/reader-page/MorePostLinks"; | ||||
| import { PostComments } from "@/components/reader-page/PostComments"; | ||||
| import { PostCover } from "@/components/reader-page/PostCover"; | ||||
| import { PostRender } from "@/components/reader-page/PostRender"; | ||||
| import { ShareButtons } from "@/components/reader-page/ShareButtons"; | ||||
| import { Separator } from "@/components/ui/separator"; | ||||
| import { Toaster } from "@/components/ui/toaster"; | ||||
| @@ -10,16 +11,12 @@ import { ContentContainer, Page } from "@/components/utils/Layout"; | ||||
| import { NavBar } from "@/components/utils/NavBar"; | ||||
| import { SEO } from "@/components/utils/SEO"; | ||||
| import { Config } from "@/data/config"; | ||||
| import { normalizeDate } from "@/lib/date"; | ||||
| import { getPostFileContent, sortedPosts } from "@/lib/post-process"; | ||||
| import { makeTOCTree } from "@/lib/toc"; | ||||
| import type { TPostFrontmatter } from "@/types/frontmatter.type"; | ||||
| import type { TPostListItem, TTOCItem } from "@/types/docs.type"; | ||||
| import { nanoid } from "nanoid"; | ||||
| import type { TPostFrontmatter, TPostListItem, TPostTOCItem } from "@/types/docs.type"; | ||||
| import type { GetStaticPaths, GetStaticProps } from "next"; | ||||
| import { MDXRemote, type MDXRemoteSerializeResult } from "next-mdx-remote"; | ||||
| import { serialize } from "next-mdx-remote/serialize"; | ||||
| import Link from "next/link"; | ||||
| import { renderToString } from "react-dom/server"; | ||||
| import rehypeAutolinkHeadings from "rehype-autolink-headings"; | ||||
| import rehypeHighlight from "rehype-highlight"; | ||||
| @@ -34,7 +31,7 @@ import { titleCase } from "title-case"; | ||||
|  | ||||
| type ReaderPageProps = { | ||||
|   compiledSource: MDXRemoteSerializeResult; | ||||
|   tocList: TTOCItem[]; | ||||
|   tocList: TPostTOCItem[]; | ||||
|   frontMatter: TPostFrontmatter; | ||||
|   postId: string; | ||||
|   nextPostListItem: TPostListItem | null; | ||||
| @@ -42,9 +39,6 @@ type ReaderPageProps = { | ||||
| }; | ||||
|  | ||||
| const ReaderPage = (props: ReaderPageProps) => { | ||||
|   const compiledSource = props.compiledSource; | ||||
|   // const setIsTOCOpen = useDrawerTOCState((state) => state.changeDrawerTOCOpen); | ||||
|  | ||||
|   // Only the TOC length reaches 3 can be displayed. | ||||
|   // In order to avoid large blank spaces that ruin the visual perception | ||||
|   const isTOCLongEnough = props.tocList.length > 2; | ||||
| @@ -63,97 +57,29 @@ const ReaderPage = (props: ReaderPageProps) => { | ||||
|       <Toaster /> | ||||
|       <NavBar /> | ||||
|       <ContentContainer> | ||||
|         <div className="flex justify-center py-5"> | ||||
|           <div className="typesetting" style={{ width: "min(50rem,100%)" }}> | ||||
|             {props.frontMatter.coverURL && <PostCover coverURL={props.frontMatter.coverURL} />} | ||||
|             <div className="border-black border-b-2 pb-1 dark:border-gray-300"> | ||||
|               <div | ||||
|                 className={ | ||||
|                   "caption-font my-2 flex justify-center whitespace-normal break-words font-bold text-3xl text-black dark:text-white" | ||||
|                 } | ||||
|               > | ||||
|                 {props.frontMatter?.title} | ||||
|               </div> | ||||
|               {props.frontMatter?.subtitle && ( | ||||
|                 <div className={"my-1 flex justify-center font-bold text-xl content-font"}> | ||||
|                   {props.frontMatter.subtitle} | ||||
|                 </div> | ||||
|               )} | ||||
|               <div className="my-1 flex justify-center text-sm italic">{normalizeDate(props.frontMatter?.time)}</div> | ||||
|               {props.frontMatter?.summary && ( | ||||
|                 <p | ||||
|                   className={ | ||||
|                     "my-4 border-gray-400 border-l-4 bg-gray-100 py-5 pr-2 pl-5 text-[16px] text-gray-800 content-font dark:border-gray-600 dark:bg-gray-900 dark:text-gray-300" | ||||
|                   } | ||||
|                 > | ||||
|                   {props.frontMatter?.summary} | ||||
|                 </p> | ||||
|               )} | ||||
|               {props.frontMatter.tags && ( | ||||
|                 <div className={"flex flex-wrap border-black border-t-2 pt-1 dark:border-gray-300"}> | ||||
|                   {props.frontMatter.tags.map((tagName) => ( | ||||
|                     <Link | ||||
|                       className="not-prose my-1 mr-2 border-2 border-black px-2 py-1 font-bold text-gray-700 text-xs hover:text-black dark:border-gray-300 dark:text-gray-300 dark:hover:text-gray-200" | ||||
|                       href={`/tags/${tagName}`} | ||||
|                       key={`tags-${nanoid()}`} | ||||
|                     > | ||||
|                       {tagName} | ||||
|                     </Link> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               )} | ||||
|             </div> | ||||
|             <div | ||||
|               className={`text-wrap border-gray-500 content-font ${!props.frontMatter.allowShare ? "select-none" : ""}`} | ||||
|               // {...handleLeftSwipe} | ||||
|             > | ||||
|               {compiledSource && ( | ||||
|                 <MDXRemote | ||||
|                   compiledSource={compiledSource.compiledSource} | ||||
|                   // @ts-ignore | ||||
|                   components={MDXComponentsSet} | ||||
|                   frontmatter={compiledSource.frontmatter} | ||||
|                   scope={compiledSource.scope} | ||||
|                 /> | ||||
|               )} | ||||
|             </div> | ||||
|             <Separator /> | ||||
|             {/* <BottomCard /> */} | ||||
|             <Separator /> | ||||
|             <ShareButtons | ||||
|               allowShare={props.frontMatter.allowShare} | ||||
|               postId={props.postId} | ||||
|               quote={props.frontMatter.summary} | ||||
|               subtitle={props.frontMatter.subtitle} | ||||
|               title={props.frontMatter.title} | ||||
|             /> | ||||
|             <Separator /> | ||||
|             <ul className="my-5 flex list-disc flex-col justify-center px-5"> | ||||
|               {props.prevPostListItem && ( | ||||
|                 <li className="my-1"> | ||||
|                   <Link | ||||
|                     className=" hover:text-sky-600 dark:hover:text-sky-500" | ||||
|                     href={`/blog/${props.prevPostListItem?.id}`} | ||||
|                   > | ||||
|                     {props.prevPostListItem?.frontMatter.title} | ||||
|                   </Link> | ||||
|                 </li> | ||||
|               )} | ||||
|               {props.nextPostListItem && ( | ||||
|                 <li className="my-1"> | ||||
|                   <Link | ||||
|                     className=" hover:text-sky-600 dark:hover:text-sky-500" | ||||
|                     href={`/blog/${props.nextPostListItem?.id}`} | ||||
|                   > | ||||
|                     {props.nextPostListItem?.frontMatter.title} | ||||
|                   </Link> | ||||
|                 </li> | ||||
|               )} | ||||
|             </ul> | ||||
|             {Config.Giscus?.enabled && <PostComments postId={props.postId} />} | ||||
|           </div> | ||||
|         <div className="mx-auto flex flex-col justify-center py-5" style={{ width: "min(50rem,100%)" }}> | ||||
|           {props.frontMatter.coverURL && <PostCover coverURL={props.frontMatter.coverURL} />} | ||||
|           <PostRender | ||||
|             compiledSource={props.compiledSource} | ||||
|             tocList={props.tocList} | ||||
|             frontMatter={props.frontMatter} | ||||
|             postId={props.postId} | ||||
|             nextPostListItem={props.nextPostListItem} | ||||
|             prevPostListItem={props.prevPostListItem} | ||||
|           /> | ||||
|           <Separator /> | ||||
|           <ShareButtons | ||||
|             allowShare={props.frontMatter.allowShare} | ||||
|             postId={props.postId} | ||||
|             quote={props.frontMatter.summary} | ||||
|             subtitle={props.frontMatter.subtitle} | ||||
|             title={props.frontMatter.title} | ||||
|           /> | ||||
|           <Separator /> | ||||
|           <MorePostLinks prevPostListItem={props.prevPostListItem} nextPostListItem={props.nextPostListItem} /> | ||||
|           {Config.Giscus?.enabled && <PostComments postId={props.postId} />} | ||||
|           {isTOCLongEnough && <DrawerTOC data={props.tocList} />} | ||||
|         </div> | ||||
|         {isTOCLongEnough && <DrawerTOC data={props.tocList} />} | ||||
|       </ContentContainer> | ||||
|       <Footer /> | ||||
|     </Page> | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { FriendLinkList } from "@/components/friend-links-page/FriendLinksList"; | ||||
| import { Separator } from "@/components/ui/separator"; | ||||
| import { Footer } from "@/components/utils/Footer"; | ||||
| import { ContentContainer, Page } from "@/components/utils/Layout"; | ||||
| import { NavBar } from "@/components/utils/NavBar"; | ||||
| import { PageTitle } from "@/components/utils/PageTitle"; | ||||
| import { SEO } from "@/components/utils/SEO"; | ||||
| import { Config } from "@/data/config"; | ||||
| import { FriendsList } from "@/data/friends"; | ||||
| import { nanoid } from "nanoid"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| export default function FriendsPage() { | ||||
|   return ( | ||||
| @@ -14,24 +14,9 @@ export default function FriendsPage() { | ||||
|       <SEO description={"My Friend Links"} title={`${Config.SiteTitle} - Friends`} /> | ||||
|       <NavBar /> | ||||
|       <ContentContainer> | ||||
|         <h2 className={"caption-font my-5 flex justify-center font-bold text-2xl"}>{"FRIENDS"}</h2> | ||||
|         <PageTitle>{"FRIENDS"}</PageTitle> | ||||
|         <Separator /> | ||||
|         <div className={"my-5 flex flex-wrap justify-center text-2xl content-font"}> | ||||
|           {FriendsList.map((item) => ( | ||||
|             <Link className="mx-2 p-2 underline underline-offset-4" href={item.url} key={nanoid()} target="_blank"> | ||||
|               {item.title} | ||||
|             </Link> | ||||
|           ))} | ||||
|         </div> | ||||
|         <Separator /> | ||||
|         <div className="my-2 flex flex-col justify-start text-base"> | ||||
|           <div className="mx-auto"> | ||||
|             {"Welcome to exchange our friend links and every high-quality blog websites are welcomed. "} | ||||
|             <Link className="underline" href={`mailto:${Config.SocialLinks.email}`}> | ||||
|               {"Email me please"} | ||||
|             </Link> | ||||
|           </div> | ||||
|         </div> | ||||
|         <FriendLinkList friends={FriendsList} /> | ||||
|       </ContentContainer> | ||||
|       <Footer /> | ||||
|     </Page> | ||||
|   | ||||
| @@ -1,22 +1,19 @@ | ||||
| import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { Input } from "@/components/ui/input"; | ||||
| import { TagsList } from "@/components/post-list-page/TagsList"; | ||||
| import { Separator } from "@/components/ui/separator"; | ||||
| import { Footer } from "@/components/utils/Footer"; | ||||
| import { ContentContainer, Page } from "@/components/utils/Layout"; | ||||
| import { NavBar } from "@/components/utils/NavBar"; | ||||
| import { PageTitle } from "@/components/utils/PageTitle"; | ||||
| import { Pagination } from "@/components/utils/Pagination"; | ||||
| import { PostList } from "@/components/utils/PostList"; | ||||
| import { SEO } from "@/components/utils/SEO"; | ||||
| import { PostCountPerPagination } from "@/consts/consts"; | ||||
| import { Config } from "@/data/config"; | ||||
| import { sortedPosts } from "@/lib/post-process"; | ||||
| import { isEmptyArray, paginateArray } from "@/lib/utils"; | ||||
| import { paginateArray } from "@/lib/utils"; | ||||
| import type { TPostListItem } from "@/types/docs.type"; | ||||
| import { nanoid } from "nanoid"; | ||||
| import type { GetStaticPaths, GetStaticProps } from "next"; | ||||
| import Link from "next/link"; | ||||
| import { useRouter } from "next/navigation"; | ||||
| import { type ChangeEvent, type KeyboardEvent, useEffect, useState } from "react"; | ||||
| import { LuPenTool } from "react-icons/lu"; | ||||
|  | ||||
| type PostsPageProps = { | ||||
| @@ -28,24 +25,11 @@ type PostsPageProps = { | ||||
|  | ||||
| export default function PostsPage(props: PostsPageProps) { | ||||
|   const router = useRouter(); | ||||
|   const [pageNumber, setPageNumber] = useState<string>(props.pageNumber.toString()); | ||||
|  | ||||
|   const handleEnterKeyJump = (event: KeyboardEvent<HTMLInputElement>) => { | ||||
|     setPageNumber(pageNumber.replace(/[^\d]/g, "")); | ||||
|     if (Number.parseInt(pageNumber) > 0 && Number.parseInt(pageNumber) < props.pageAmount + 1) { | ||||
|       (event.key === "Go" || event.key === "Enter") && router.push(`/posts/${pageNumber}`); | ||||
|       return; | ||||
|     } | ||||
|   const handleChangePage = (pageNumber: number) => { | ||||
|     router.push(`/posts/${pageNumber}`); | ||||
|   }; | ||||
|  | ||||
|   const handleInputPageNumber = (event: ChangeEvent<HTMLInputElement>) => { | ||||
|     setPageNumber(event.target.value); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setPageNumber(props.pageNumber.toString().replace(/[^\d]/g, "")); | ||||
|   }, [props.pageNumber]); | ||||
|  | ||||
|   return ( | ||||
|     <Page> | ||||
|       <SEO | ||||
| @@ -55,60 +39,21 @@ export default function PostsPage(props: PostsPageProps) { | ||||
|       /> | ||||
|       <NavBar /> | ||||
|       <ContentContainer> | ||||
|         <h2 className={"caption-font my-5 flex justify-center font-bold text-2xl"}> | ||||
|         <PageTitle> | ||||
|           <LuPenTool className="mx-2 my-auto" /> | ||||
|           {"ALL POSTS"} | ||||
|         </h2> | ||||
|         {!isEmptyArray(props.tagList) && ( | ||||
|           <Accordion collapsible type="single"> | ||||
|             <AccordionItem className="border-t" value="item-1"> | ||||
|               <AccordionTrigger className="font-bold hover:no-underline">{"TAG FILTER"}</AccordionTrigger> | ||||
|               <AccordionContent> | ||||
|                 <Separator /> | ||||
|                 <div className={"my-5 flex flex-wrap justify-center text-wrap px-2 text-sm"}> | ||||
|                   {props.tagList.map((item) => ( | ||||
|                     <Link | ||||
|                       className="m-1 my-auto p-1 font-bold text-gray-700 underline decoration-2 underline-offset-[5px] hover:text-black dark:text-gray-300 dark:hover:text-white" | ||||
|                       href={`/tags/${item.name}`} | ||||
|                       key={`tags-${nanoid()}`} | ||||
|                     > | ||||
|                       {`${item.name} (${item.count})`} | ||||
|                     </Link> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </AccordionContent> | ||||
|             </AccordionItem> | ||||
|           </Accordion> | ||||
|         )} | ||||
|         </PageTitle> | ||||
|         <TagsList tagsList={props.tagList} /> | ||||
|         <Separator /> | ||||
|         <PostList data={props.postList} /> | ||||
|         <Separator /> | ||||
|         <div className="my-5 flex justify-between font-bold text-base"> | ||||
|           {props.pageNumber !== 1 && ( | ||||
|             <Button asChild> | ||||
|               <Link className="font-bold" href={`/posts/${props.pageNumber - 1}/`}> | ||||
|                 {"< PREV"} | ||||
|               </Link> | ||||
|             </Button> | ||||
|           )} | ||||
|           <div className="my-auto flex justify-center font-bold"> | ||||
|             <Input | ||||
|               className="mx-2 my-auto h-6 w-11" | ||||
|               onChange={handleInputPageNumber} | ||||
|               onKeyDown={handleEnterKeyJump} | ||||
|               title="Type the specified page number and press Enter to jump." | ||||
|               value={pageNumber} | ||||
|             /> | ||||
|             <div className="my-auto">{`  /  ${props.pageAmount}`}</div> | ||||
|           </div> | ||||
|           {props.pageNumber !== props.pageAmount && ( | ||||
|             <Button asChild> | ||||
|               <Link className="font-bold" href={`/posts/${props.pageNumber + 1}/`}> | ||||
|                 {"NEXT >"} | ||||
|               </Link> | ||||
|             </Button> | ||||
|           )} | ||||
|         </div> | ||||
|         <Pagination | ||||
|           onGotoNextPage={(nextPage) => handleChangePage(nextPage)} | ||||
|           onGotoPrevPage={(prevPage) => handleChangePage(prevPage)} | ||||
|           onJumpToSpecPage={(pageNum) => handleChangePage(pageNum)} | ||||
|           pageNumber={props.pageNumber} | ||||
|           pageAmount={props.pageAmount} | ||||
|         /> | ||||
|       </ContentContainer> | ||||
|       <Footer /> | ||||
|     </Page> | ||||
|   | ||||
							
								
								
									
										108
									
								
								pages/search.tsx
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								pages/search.tsx
									
									
									
									
									
								
							| @@ -1,70 +1,48 @@ | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { Input } from "@/components/ui/input"; | ||||
| import { SearchInput } from "@/components/search-page/SearchInput"; | ||||
| import { SearchResultList } from "@/components/search-page/SearchResultList"; | ||||
| import { Toaster } from "@/components/ui/toaster"; | ||||
| import { useToast } from "@/components/ui/use-toast"; | ||||
| import { Footer } from "@/components/utils/Footer"; | ||||
| import { ContentContainer, Page } from "@/components/utils/Layout"; | ||||
| import { NavBar } from "@/components/utils/NavBar"; | ||||
| import { PageTitle } from "@/components/utils/PageTitle"; | ||||
| import { SEO } from "@/components/utils/SEO"; | ||||
| import { Config } from "@/data/config"; | ||||
| import { isEmptyString } from "@/lib/utils"; | ||||
| import type { TSearchResultItem } from "@/types/docs.type"; | ||||
| import axios from "axios"; | ||||
| import { isArray } from "lodash"; | ||||
| import { nanoid } from "nanoid"; | ||||
| import type { GetServerSideProps } from "next"; | ||||
| import Link from "next/link"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { type ChangeEvent, type KeyboardEvent, useEffect, useState } from "react"; | ||||
| import { SiteLinksSearchBoxJsonLd } from "next-seo"; | ||||
| import { useState } from "react"; | ||||
|  | ||||
| type SearchPageProps = { query: string | null }; | ||||
|  | ||||
| export default function SearchPage(props: SearchPageProps) { | ||||
|   const [searchText, setSearchText] = useState<string>(props.query ?? ""); | ||||
|   const [searchResult, setSearchResult] = useState<TSearchResultItem[]>([]); | ||||
|   const [isLoading, setIsLoading] = useState<boolean>(false); | ||||
|   const { toast } = useToast(); | ||||
|   const router = useRouter(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!isEmptyString(searchText)) { | ||||
|       handleMakeSearch(); | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   const fetchSearchAPI = (param: string): Promise<TSearchResultItem[]> => { | ||||
|     return axios.get<TSearchResultItem[]>(`/api/search/${param}`).then((response) => response.data); | ||||
|   }; | ||||
|  | ||||
|   const handleInputSearchText = (event: ChangeEvent<HTMLInputElement>) => { | ||||
|     setSearchText(event.target.value); | ||||
|   }; | ||||
|  | ||||
|   const handleEnterKeySearch = (event: KeyboardEvent<HTMLInputElement>) => { | ||||
|     if (event.key === "Go" || event.key === "Enter") { | ||||
|       handleMakeSearch(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleMakeSearch = () => { | ||||
|     const searchQuery = searchText; | ||||
|  | ||||
|     if (isEmptyString(searchQuery)) { | ||||
|       toast({ title: "Enter a Keyword", description: "Please enter one keyword at least." }); | ||||
|   const handleSearch = (word: string) => { | ||||
|     if (isEmptyString(word)) { | ||||
|       toast({ | ||||
|         title: "Enter a Keyword", | ||||
|         description: "Please enter one keyword at least.", | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     if (searchQuery && searchQuery.length < 4) { | ||||
|       toast({ title: "Keywords too short", description: "Keyword length must be at least 5." }); | ||||
|     if (word && word.length < 4) { | ||||
|       toast({ | ||||
|         title: "Keywords too short", | ||||
|         description: "Keyword length must be at least 4.", | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     router.push({ | ||||
|       pathname: router.pathname, | ||||
|       query: { ...router.query, q: searchQuery }, | ||||
|     }); | ||||
|     setIsLoading(true); | ||||
|  | ||||
|     fetchSearchAPI(searchQuery) | ||||
|     fetchSearchAPI(word) | ||||
|       .then((data) => { | ||||
|         setSearchResult(data); | ||||
|         if (data.length === 0) { | ||||
| @@ -87,47 +65,19 @@ export default function SearchPage(props: SearchPageProps) { | ||||
|       <SEO description={"Search the posts on your demand."} title={`${Config.SiteTitle} - Search`} /> | ||||
|       <Toaster /> | ||||
|       <NavBar /> | ||||
|       <SiteLinksSearchBoxJsonLd | ||||
|         potentialActions={[ | ||||
|           { | ||||
|             target: `https://${Config.SiteDomain}/search?q={search_term_string}`, | ||||
|             queryInput: "search_term_string", | ||||
|           }, | ||||
|         ]} | ||||
|         url={`https://${Config.SiteDomain}/`} | ||||
|       /> | ||||
|       <ContentContainer> | ||||
|         <h2 className={"caption-font my-10 flex justify-center font-bold text-2xl"}>{"SEARCH POSTS"}</h2> | ||||
|         <div className="my-10 flex"> | ||||
|           <Input | ||||
|             className="my-auto py-0" | ||||
|             onChange={handleInputSearchText} | ||||
|             onKeyDown={handleEnterKeySearch} | ||||
|             placeholder="Input the keyword" | ||||
|             value={searchText} | ||||
|           /> | ||||
|           <Button className="mx-3 my-auto w-32" disabled={isLoading} onClick={handleMakeSearch}> | ||||
|             {isLoading ? "Loading" : "Search"} | ||||
|           </Button> | ||||
|         </div> | ||||
|         <div className="flex flex-col justify-center"> | ||||
|           <div className={"flex min-h-full flex-col content-font"}> | ||||
|             {searchResult.map((item, index) => ( | ||||
|               <Link | ||||
|                 className={`border-t p-2 ${index === searchResult.length - 1 && "border-b"} flex flex-col hover:bg-gray-50 dark:hover:bg-gray-900`} | ||||
|                 href={`/blog/${item.id}`} | ||||
|                 key={nanoid()} | ||||
|                 target="_blank" | ||||
|               > | ||||
|                 <div className="my-1"> | ||||
|                   <div className="post-list-caption-font font-bold text-md capitalize">{item.title}</div> | ||||
|                   {item.summary && <div>{item.summary}</div>} | ||||
|                 </div> | ||||
|                 <div className="flex flex-wrap space-x-2"> | ||||
|                   {item.tags?.map((tagitem) => ( | ||||
|                     <div className="text-gray-500 text-sm dark:text-gray-400" key={nanoid()}> | ||||
|                       {tagitem} | ||||
|                     </div> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </Link> | ||||
|             ))} | ||||
|           </div> | ||||
|         </div> | ||||
|         <div className="my-3 text-center text-gray-500 dark:text-gray-400"> | ||||
|           <p className="mx-auto text-sm">{"For search efficiency, only the first 20 results are displayed."}</p> | ||||
|         </div> | ||||
|         <PageTitle>{"SEARCH POSTS"}</PageTitle> | ||||
|         <SearchInput isLoading={isLoading} handleSearch={handleSearch} word={props.query} /> | ||||
|         <SearchResultList searchResult={searchResult} /> | ||||
|       </ContentContainer> | ||||
|       <Footer /> | ||||
|     </Page> | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { Toaster } from "@/components/ui/toaster"; | ||||
| import { Footer } from "@/components/utils/Footer"; | ||||
| import { ContentContainer, Page } from "@/components/utils/Layout"; | ||||
| import { NavBar } from "@/components/utils/NavBar"; | ||||
| import { PageTitle } from "@/components/utils/PageTitle"; | ||||
| import { SEO } from "@/components/utils/SEO"; | ||||
| import { Config } from "@/data/config"; | ||||
| import { GoHeartFill } from "react-icons/go"; | ||||
| @@ -22,10 +23,10 @@ export default function SponsorPage() { | ||||
|       <ContentContainer> | ||||
|         <div className="mt-10 md:flex"> | ||||
|           <div className="flex flex-col justify-center md:w-1/2"> | ||||
|             <h2 className={"caption-font my-5 flex justify-center font-bold text-2xl text-red-500"}> | ||||
|             <PageTitle classNames="text-red-500"> | ||||
|               <GoHeartFill className="mx-2 my-auto" /> | ||||
|               {"SPONSOR"} | ||||
|             </h2> | ||||
|             </PageTitle> | ||||
|             <SponsorDescription /> | ||||
|           </div> | ||||
|           <div className="md:w-1/2 md:px-15"> | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { Input } from "@/components/ui/input"; | ||||
| import { Separator } from "@/components/ui/separator"; | ||||
| import { Footer } from "@/components/utils/Footer"; | ||||
| import { ContentContainer, Page } from "@/components/utils/Layout"; | ||||
| import { NavBar } from "@/components/utils/NavBar"; | ||||
| import { PageTitle } from "@/components/utils/PageTitle"; | ||||
| import { Pagination } from "@/components/utils/Pagination"; | ||||
| import { PostList } from "@/components/utils/PostList"; | ||||
| import { SEO } from "@/components/utils/SEO"; | ||||
| import { PostCountPerPagination } from "@/consts/consts"; | ||||
| @@ -12,9 +12,7 @@ import { sortedPosts } from "@/lib/post-process"; | ||||
| import { paginateArray } from "@/lib/utils"; | ||||
| import type { TPostListItem } from "@/types/docs.type"; | ||||
| import type { GetStaticPaths, GetStaticProps } from "next"; | ||||
| import Link from "next/link"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { type ChangeEvent, type KeyboardEvent, useEffect, useState } from "react"; | ||||
|  | ||||
| type TagsContentPageProps = { | ||||
|   tagName: string | null; | ||||
| @@ -25,24 +23,10 @@ type TagsContentPageProps = { | ||||
|  | ||||
| export default function TagsContentPage(props: TagsContentPageProps) { | ||||
|   const router = useRouter(); | ||||
|   const [pageNumber, setPageNumber] = useState<string>(props.pageNumber.toString()); | ||||
|  | ||||
|   const handleEnterKeyJump = (event: KeyboardEvent<HTMLInputElement>) => { | ||||
|     setPageNumber(pageNumber.replace(/[^\d]/g, "")); | ||||
|     if (Number.parseInt(pageNumber) > 0 && Number.parseInt(pageNumber) < props.pageAmount + 1) { | ||||
|       (event.key === "Go" || event.key === "Enter") && router.push(`/tags/${props.tagName}/${pageNumber}`); | ||||
|       return; | ||||
|     } | ||||
|   const handleChangePage = (pageNumber: number) => { | ||||
|     router.push(`/tags/${props.tagName}/${pageNumber}/`); | ||||
|   }; | ||||
|  | ||||
|   const handleInputPageNumber = (event: ChangeEvent<HTMLInputElement>) => { | ||||
|     setPageNumber(event.target.value); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setPageNumber(props.pageNumber.toString()); | ||||
|   }, [props.pageNumber]); | ||||
|  | ||||
|   return ( | ||||
|     <Page> | ||||
|       <SEO | ||||
| @@ -52,38 +36,17 @@ export default function TagsContentPage(props: TagsContentPageProps) { | ||||
|       /> | ||||
|       <NavBar /> | ||||
|       <ContentContainer> | ||||
|         <h2 className={"caption-font my-5 flex flex-col justify-center text-center font-bold"}> | ||||
|           <div className="mx-auto text-2xl">{`Posts of ${props.tagName}`}</div> | ||||
|         </h2> | ||||
|         <PageTitle>{`Posts of ${props.tagName}`}</PageTitle> | ||||
|         <Separator /> | ||||
|         <PostList data={props.postList} /> | ||||
|         <Separator /> | ||||
|         <div className="my-5 flex justify-between font-bold text-base"> | ||||
|           {props.pageNumber !== 1 && ( | ||||
|             <Button asChild> | ||||
|               <Link className="font-bold" href={`/tags/${props.tagName}/${props.pageNumber - 1}/`}> | ||||
|                 {"< PREV"} | ||||
|               </Link> | ||||
|             </Button> | ||||
|           )} | ||||
|           <div className="my-auto flex justify-center font-bold"> | ||||
|             <Input | ||||
|               className="mx-2 my-auto h-6 w-11" | ||||
|               onChange={handleInputPageNumber} | ||||
|               onKeyDown={handleEnterKeyJump} | ||||
|               title="Type the specified page number and press Enter to jump." | ||||
|               value={pageNumber} | ||||
|             /> | ||||
|             <div className="my-auto">{`  /  ${props.pageAmount}`}</div> | ||||
|           </div> | ||||
|           {props.pageNumber !== props.pageAmount && ( | ||||
|             <Button asChild> | ||||
|               <Link className="font-bold" href={`/tags/${props.tagName}/${props.pageNumber + 1}/`}> | ||||
|                 {"NEXT >"} | ||||
|               </Link> | ||||
|             </Button> | ||||
|           )} | ||||
|         </div> | ||||
|         <Pagination | ||||
|           onGotoNextPage={(nextPage) => handleChangePage(nextPage)} | ||||
|           onGotoPrevPage={(prevPage) => handleChangePage(prevPage)} | ||||
|           onJumpToSpecPage={(pageNum) => handleChangePage(pageNum)} | ||||
|           pageNumber={props.pageNumber} | ||||
|           pageAmount={props.pageAmount} | ||||
|         /> | ||||
|       </ContentContainer> | ||||
|       <Footer /> | ||||
|     </Page> | ||||
| @@ -101,7 +64,9 @@ export const getStaticPaths: GetStaticPaths = () => { | ||||
|   for (let i = 0; i < allTags.length; i++) { | ||||
|     allPaths.push({ params: { slug: [allTags[i].name] } }); | ||||
|     for (let j = 0; j < allTags[i].count; j++) { | ||||
|       allPaths.push({ params: { slug: [allTags[i].name, (j + 1).toString()] } }); | ||||
|       allPaths.push({ | ||||
|         params: { slug: [allTags[i].name, (j + 1).toString()] }, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { PostFilesDirectory } from "@/consts/consts"; | ||||
| import { getCurrentTime } from "@/lib/date"; | ||||
| import { stringifyFrontmatter } from "@/lib/frontmatter"; | ||||
| import type { TPostFrontmatter } from "@/types/frontmatter.type"; | ||||
| import type { TPostFrontmatter } from "@/types/docs.type"; | ||||
| import { type ChildProcessWithoutNullStreams, type SpawnSyncReturns, spawn, spawnSync } from "child_process"; | ||||
| import colors from "colors"; | ||||
| import fs from "fs"; | ||||
|   | ||||
| @@ -16,8 +16,21 @@ export type TSearchResultItem = { | ||||
|   tags: string[] | null; | ||||
| }; | ||||
|  | ||||
| export type TTOCItem = { | ||||
| export type TPostTOCItem = { | ||||
|   level: number; | ||||
|   title: string; | ||||
|   anchorId: string; | ||||
| }; | ||||
|  | ||||
| export type TPostFrontmatter = { | ||||
|   title: string; | ||||
|   time: string; | ||||
|   tags: string[] | null; | ||||
|   subtitle: string | null; | ||||
|   summary: string | null; | ||||
|   coverURL: string | null; | ||||
|   pin: boolean | null; | ||||
|   noPrompt: boolean | null; | ||||
|   allowShare: boolean | null; | ||||
|   closed: boolean | null; | ||||
| }; | ||||
|   | ||||
| @@ -1,12 +0,0 @@ | ||||
| export type TPostFrontmatter = { | ||||
|   title: string; | ||||
|   time: string; | ||||
|   tags: string[] | null; | ||||
|   subtitle: string | null; | ||||
|   summary: string | null; | ||||
|   coverURL: string | null; | ||||
|   pin: boolean | null; | ||||
|   noPrompt: boolean | null; | ||||
|   allowShare: boolean | null; | ||||
|   closed: boolean | null; | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user
	 PrinOrange
					PrinOrange