diff --git a/Frontend/bun.lockb b/Frontend/bun.lockb index f936f11..c881f87 100644 Binary files a/Frontend/bun.lockb and b/Frontend/bun.lockb differ diff --git a/Frontend/package.json b/Frontend/package.json index 57beb3f..0d664cf 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -37,7 +37,7 @@ "class-variance-authority": "^0.7.0", "clipboard-copy": "^4.0.1", "clsx": "^2.1.0", - "fuse.js": "^7.0.0", + "cmdk": "^1.0.0", "lucide-react": "^0.372.0", "moment": "^2.30.1", "next": "14.2.2", diff --git a/Frontend/src/app/api/docs/search/route.ts b/Frontend/src/app/api/docs/search/route.ts index f33b9a4..8743fe0 100644 --- a/Frontend/src/app/api/docs/search/route.ts +++ b/Frontend/src/app/api/docs/search/route.ts @@ -1,30 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { DOCS_SEARCH_INDEX } from "@/lib/search"; -import { FuseResult } from "fuse.js"; +import { getDocsContent } from "@/lib/mdxUtils"; export const GET = async (request: NextRequest): Promise => { - const query: string | null = request.nextUrl.searchParams.get("query"); // The query to search for - - // Ensure the query is valid - if (!query || query.length < 3 || query.length > 64) { - return new NextResponse( - JSON.stringify({ error: "Invalid query given" }), - { status: 400 } - ); - } - - // Return the results of the search - return new NextResponse( - JSON.stringify( - DOCS_SEARCH_INDEX.search(query, { limit: 5 }).map( - (result: FuseResult) => { - return { - slug: result.item.slug, - title: result.item.title, - summary: result.item.summary, - }; - } - ) - ) - ); + return new NextResponse(JSON.stringify(getDocsContent())); }; diff --git a/Frontend/src/app/common/search.ts b/Frontend/src/app/common/search.ts deleted file mode 100644 index 180abec..0000000 --- a/Frontend/src/app/common/search.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Fuse from "fuse.js"; -import { getDocsContent } from "@/lib/mdxUtils"; - -/** - * The fuse index for searching the docs. - */ -export const DOCS_SEARCH_INDEX: Fuse = new Fuse( - getDocsContent(), - { - keys: ["title", "summary"], - includeScore: true, - threshold: 0.5, - } -); diff --git a/Frontend/src/app/components/docs/search/search-dialog.tsx b/Frontend/src/app/components/docs/search/search-dialog.tsx index 1fe3c58..3c97ee1 100644 --- a/Frontend/src/app/components/docs/search/search-dialog.tsx +++ b/Frontend/src/app/components/docs/search/search-dialog.tsx @@ -1,146 +1,103 @@ "use client"; +import { ReactElement, useEffect, useState } from "react"; import { - AnchorHTMLAttributes, - ChangeEvent, - HTMLAttributes, - ReactElement, - useState, -} from "react"; -import { - DialogClose, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"; /** - * Content for the search dialog. + * The dialog for quickly searching the docs. * * @return the content jsx */ -const SearchDialogContent = (): ReactElement => { - const [label, setLabel] = useState(undefined); +const QuickSearchDialog = (): ReactElement => { + const [open, setOpen] = useState(false); const [results, setResults] = useState( undefined ); // The search results + const router: AppRouterInstance = useRouter(); - /** - * Search the docs with the given query. - */ - const search = async ( - event: ChangeEvent - ): Promise => { - const query: string = event.target.value; // Get the query to search for - const tooShort: boolean = query.length < 3; - - // No query or too short - if (!query || tooShort || query.length > 64) { - // Display warning - if (query) { - setLabel( - tooShort - ? "Please enter at least 3 characters" - : "Your input is too long" - ); - } - setResults(undefined); - return; + // Fetch the default results when the page loads + useEffect((): void => { + if (!results) { + const fetchDefaults = async () => { + const response: Response = await fetch("/api/docs/search"); // Search the docs + // setLabel(undefined); // Clear the label + setResults((await response.json()) as DocsContentMetadata[]); + }; + fetchDefaults(); } - const response: Response = await fetch( - `/api/docs/search?query=${query}` - ); // Search the docs - setLabel(undefined); // Clear the label - setResults((await response.json()) as DocsContentMetadata[]); - }; + }, [results]); // Render the contents return ( <> - {/* Header */} - - Quick Search - - Quickly find the documentation you're looking for by - typing in a few terms. - - - {/* Query Input */} -
- - setOpen(true)}> +
+
- - {/* Results */} -
- {results?.length === 0 && ( -

No Results

- )} - {results?.map( - ( - result: DocsContentMetadata, - index: number - ): ReactElement => ( - - ) - )} +
- {/* Footer */} - - - - - + {/* Dialog */} + + {/* Input */} + + + {/* Results */} + + + No results were found. + + + + {results?.map( + ( + result: DocsContentMetadata, + index: number + ): ReactElement => ( + + router.push(`/docs/${result.slug}`) + } + > +

+ {result.title} +

+

+ {result.summary} +

+
+ ) + )} +
+
+
); }; -/** - * The props for a search result entry. - */ -type SearchResultEntryProps = { - /** - * The search result to display. - */ - result: DocsContentMetadata; -}; - -/** - * A search result entry. - * - * @param result the result to display - * @param props the additional props - * @return the result jsx - */ -const SearchResultEntry = ({ - result, - ...props -}: AnchorHTMLAttributes & - SearchResultEntryProps): ReactElement => ( - -

{result.title}

-

{result.summary}

- -); - -export default SearchDialogContent; +export default QuickSearchDialog; diff --git a/Frontend/src/app/components/docs/search/search-input.tsx b/Frontend/src/app/components/docs/search/search-input.tsx deleted file mode 100644 index 9257cff..0000000 --- a/Frontend/src/app/components/docs/search/search-input.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { ReactElement } from "react"; -import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; -import SearchDialogContent from "@/components/docs/search/search-dialog"; - -/** - * The quick search component. - * - * @return the search jsx - */ -const QuickSearch = (): ReactElement => ( - - - {/* Button to open search */} -
- -
- - -
- - - -
-); -export default QuickSearch; diff --git a/Frontend/src/app/components/docs/sidebar.tsx b/Frontend/src/app/components/docs/sidebar.tsx index 88a194f..1fd7d56 100644 --- a/Frontend/src/app/components/docs/sidebar.tsx +++ b/Frontend/src/app/components/docs/sidebar.tsx @@ -4,7 +4,7 @@ import { cn } from "@/lib/utils"; import { capitalize } from "@/lib/stringUtils"; import { minecrafter } from "@/font/fonts"; import { getDocsContent } from "@/lib/mdxUtils"; -import QuickSearch from "@/components/docs/search/search-input"; +import QuickSearchDialog from "@/components/docs/search/search-dialog"; /** * The sidebar for the docs page. @@ -24,7 +24,7 @@ const Sidebar = ({ activeSlug }: { activeSlug: string }): ReactElement => {
{/* Quick Search */} - + {/* Links */}
diff --git a/Frontend/src/app/components/ui/command.tsx b/Frontend/src/app/components/ui/command.tsx new file mode 100644 index 0000000..e4ad011 --- /dev/null +++ b/Frontend/src/app/components/ui/command.tsx @@ -0,0 +1,158 @@ +"use client"; + +import * as React from "react"; +import { type DialogProps } from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Dialog, DialogContent } from "@/app/components/ui/dialog"; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +};