Update search ui
All checks were successful
Deploy Frontend / docker (17, 3.8.5) (push) Successful in 1m52s

This commit is contained in:
Braydon 2024-04-21 19:51:30 -04:00
parent 349747d555
commit 6963085295
8 changed files with 239 additions and 199 deletions

Binary file not shown.

@ -37,7 +37,7 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clipboard-copy": "^4.0.1", "clipboard-copy": "^4.0.1",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"fuse.js": "^7.0.0", "cmdk": "^1.0.0",
"lucide-react": "^0.372.0", "lucide-react": "^0.372.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "14.2.2", "next": "14.2.2",

@ -1,30 +1,6 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { DOCS_SEARCH_INDEX } from "@/lib/search"; import { getDocsContent } from "@/lib/mdxUtils";
import { FuseResult } from "fuse.js";
export const GET = async (request: NextRequest): Promise<NextResponse> => { export const GET = async (request: NextRequest): Promise<NextResponse> => {
const query: string | null = request.nextUrl.searchParams.get("query"); // The query to search for return new NextResponse(JSON.stringify(getDocsContent()));
// 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<DocsContentMetadata>) => {
return {
slug: result.item.slug,
title: result.item.title,
summary: result.item.summary,
};
}
)
)
);
}; };

@ -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<DocsContentMetadata> = new Fuse(
getDocsContent(),
{
keys: ["title", "summary"],
includeScore: true,
threshold: 0.5,
}
);

@ -1,146 +1,103 @@
"use client"; "use client";
import { ReactElement, useEffect, useState } from "react";
import { import {
AnchorHTMLAttributes, CommandDialog,
ChangeEvent, CommandEmpty,
HTMLAttributes, CommandGroup,
ReactElement, CommandInput,
useState, CommandItem,
} from "react"; CommandList,
import { } from "@/components/ui/command";
DialogClose, import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { useRouter } from "next/navigation";
import { Label } from "@/components/ui/label"; import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import Link from "next/link";
/** /**
* Content for the search dialog. * The dialog for quickly searching the docs.
* *
* @return the content jsx * @return the content jsx
*/ */
const SearchDialogContent = (): ReactElement => { const QuickSearchDialog = (): ReactElement => {
const [label, setLabel] = useState<string | undefined>(undefined); const [open, setOpen] = useState<boolean>(false);
const [results, setResults] = useState<DocsContentMetadata[] | undefined>( const [results, setResults] = useState<DocsContentMetadata[] | undefined>(
undefined undefined
); // The search results ); // The search results
const router: AppRouterInstance = useRouter();
/** // Fetch the default results when the page loads
* Search the docs with the given query. useEffect((): void => {
*/ if (!results) {
const search = async ( const fetchDefaults = async () => {
event: ChangeEvent<HTMLInputElement> const response: Response = await fetch("/api/docs/search"); // Search the docs
): Promise<void> => { // setLabel(undefined); // Clear the label
const query: string = event.target.value; // Get the query to search for setResults((await response.json()) as DocsContentMetadata[]);
const tooShort: boolean = query.length < 3; };
fetchDefaults();
// 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;
} }
const response: Response = await fetch( }, [results]);
`/api/docs/search?query=${query}`
); // Search the docs
setLabel(undefined); // Clear the label
setResults((await response.json()) as DocsContentMetadata[]);
};
// Render the contents // Render the contents
return ( return (
<> <>
{/* Header */} {/* Button to open */}
<DialogHeader className="flex flex-col gap-2"> <div onClick={() => setOpen(true)}>
<DialogTitle>Quick Search</DialogTitle> <div className="absolute top-2.5 left-3 z-10">
<DialogDescription> <MagnifyingGlassIcon
Quickly find the documentation you&apos;re looking for by className="absolute"
typing in a few terms. width={22}
</DialogDescription> height={22}
{/* Query Input */}
<div className="space-y-1.5">
<Label htmlFor="search">
{label || "Start typing to get started"}
</Label>
<Input
type="search"
name="search"
placeholder="Query..."
onChange={search}
/> />
</div> </div>
</DialogHeader>
{/* Results */} <Input
<div className="flex flex-col gap-2"> className="pl-10"
{results?.length === 0 && ( type="search"
<p className="text-red-500">No Results</p> name="search"
)} placeholder="Quick search..."
{results?.map( readOnly
( />
result: DocsContentMetadata,
index: number
): ReactElement => (
<SearchResultEntry key={index} result={result} />
)
)}
</div> </div>
{/* Footer */} {/* Dialog */}
<DialogFooter className="sm:justify-start"> <CommandDialog open={open} onOpenChange={setOpen}>
<DialogClose asChild> {/* Input */}
<Button type="button" variant="secondary"> <CommandInput placeholder="Start typing to get started..." />
Close
</Button> {/* Results */}
</DialogClose> <CommandList>
</DialogFooter> <CommandEmpty className="text-center text-red-500">
No results were found.
</CommandEmpty>
<CommandGroup heading="Results">
{results?.map(
(
result: DocsContentMetadata,
index: number
): ReactElement => (
<CommandItem
key={index}
className="flex flex-col gap-1 items-start"
onSelect={() =>
router.push(`/docs/${result.slug}`)
}
>
<h1 className="text-minecraft-green-3 font-bold">
{result.title}
</h1>
<p className="text-zinc-300/85">
{result.summary}
</p>
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</CommandDialog>
</> </>
); );
}; };
/** export default QuickSearchDialog;
* 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<HTMLAnchorElement> &
SearchResultEntryProps): ReactElement => (
<Link
className="p-3 flex flex-col gap-1.5 bg-muted rounded-xl"
href={`/docs/${result.slug}`}
{...props}
>
<h1 className="font-semibold text-minecraft-green-4">{result.title}</h1>
<p className="font-light text-zinc-200">{result.summary}</p>
</Link>
);
export default SearchDialogContent;

@ -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 => (
<Dialog>
<DialogTrigger>
{/* Button to open search */}
<div className="absolute top-2.5 left-3 z-10">
<MagnifyingGlassIcon
className="absolute"
width={22}
height={22}
/>
</div>
<Input
className="pl-10"
type="search"
name="search"
placeholder="Quick search..."
readOnly
/>
</DialogTrigger>
<DialogContent>
<SearchDialogContent />
</DialogContent>
</Dialog>
);
export default QuickSearch;

@ -4,7 +4,7 @@ import { cn } from "@/lib/utils";
import { capitalize } from "@/lib/stringUtils"; import { capitalize } from "@/lib/stringUtils";
import { minecrafter } from "@/font/fonts"; import { minecrafter } from "@/font/fonts";
import { getDocsContent } from "@/lib/mdxUtils"; 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. * The sidebar for the docs page.
@ -24,7 +24,7 @@ const Sidebar = ({ activeSlug }: { activeSlug: string }): ReactElement => {
<div className="hidden h-full px-3 py-5 xl:flex flex-col items-center"> <div className="hidden h-full px-3 py-5 xl:flex flex-col items-center">
<div className="fixed w-56 flex flex-col gap-5"> <div className="fixed w-56 flex flex-col gap-5">
{/* Quick Search */} {/* Quick Search */}
<QuickSearch /> <QuickSearchDialog />
{/* Links */} {/* Links */}
<div className="flex flex-col gap-7"> <div className="flex flex-col gap-7">

@ -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<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn(
"max-h-[300px] overflow-y-auto overflow-x-hidden",
className
)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50",
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};