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",
"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",

@ -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<NextResponse> => {
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<DocsContentMetadata>) => {
return {
slug: result.item.slug,
title: result.item.title,
summary: result.item.summary,
};
}
)
)
);
return new NextResponse(JSON.stringify(getDocsContent()));
};

@ -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";
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<string | undefined>(undefined);
const QuickSearchDialog = (): ReactElement => {
const [open, setOpen] = useState<boolean>(false);
const [results, setResults] = useState<DocsContentMetadata[] | undefined>(
undefined
); // The search results
const router: AppRouterInstance = useRouter();
/**
* Search the docs with the given query.
*/
const search = async (
event: ChangeEvent<HTMLInputElement>
): Promise<void> => {
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 */}
<DialogHeader className="flex flex-col gap-2">
<DialogTitle>Quick Search</DialogTitle>
<DialogDescription>
Quickly find the documentation you&apos;re looking for by
typing in a few terms.
</DialogDescription>
{/* 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}
{/* Button to open */}
<div onClick={() => setOpen(true)}>
<div className="absolute top-2.5 left-3 z-10">
<MagnifyingGlassIcon
className="absolute"
width={22}
height={22}
/>
</div>
</DialogHeader>
{/* Results */}
<div className="flex flex-col gap-2">
{results?.length === 0 && (
<p className="text-red-500">No Results</p>
)}
{results?.map(
(
result: DocsContentMetadata,
index: number
): ReactElement => (
<SearchResultEntry key={index} result={result} />
)
)}
<Input
className="pl-10"
type="search"
name="search"
placeholder="Quick search..."
readOnly
/>
</div>
{/* Footer */}
<DialogFooter className="sm:justify-start">
<DialogClose asChild>
<Button type="button" variant="secondary">
Close
</Button>
</DialogClose>
</DialogFooter>
{/* Dialog */}
<CommandDialog open={open} onOpenChange={setOpen}>
{/* Input */}
<CommandInput placeholder="Start typing to get started..." />
{/* Results */}
<CommandList>
<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>
</>
);
};
/**
* 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;
export default QuickSearchDialog;

@ -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 { 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 => {
<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">
{/* Quick Search */}
<QuickSearch />
<QuickSearchDialog />
{/* Links */}
<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,
};