diff --git a/package.json b/package.json index e353843..a9b3d96 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "class-variance-authority": "^0.7.0", "clipboard-copy": "^4.0.1", "clsx": "^2.1.0", + "cmdk": "^1.0.0", "fuse.js": "^7.0.0", "lucide-react": "^0.372.0", "mcutils-library": "^1.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c99f73..89292e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ dependencies: clsx: specifier: ^2.1.0 version: 2.1.0 + cmdk: + specifier: ^1.0.0 + version: 1.0.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) fuse.js: specifier: ^7.0.0 version: 7.0.0 @@ -3016,6 +3019,21 @@ packages: engines: {node: '>=6'} dev: false + /cmdk@1.0.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + dev: false + /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} diff --git a/src/app/(pages)/docs/[[...slug]]/page.tsx b/src/app/(pages)/docs/[[...slug]]/page.tsx index d471d52..5d8e66b 100644 --- a/src/app/(pages)/docs/[[...slug]]/page.tsx +++ b/src/app/(pages)/docs/[[...slug]]/page.tsx @@ -13,6 +13,7 @@ import { import { capitalizeFirstLetter } from "@/app/common/string-utils"; import { notFound } from "next/navigation"; import { GithubLink } from "@/app/components/docs/github-link"; +import { CommandMenu } from "@/app/components/command-menu"; type DocumentationPageParams = { params: { @@ -87,7 +88,9 @@ export default function Page({ params: { slug } }: DocumentationPageParams) { - +
+ +
{/* The documentation page title and description */} diff --git a/src/app/(pages)/docs/layout.tsx b/src/app/(pages)/docs/layout.tsx deleted file mode 100644 index 339cdb0..0000000 --- a/src/app/(pages)/docs/layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React, { ReactElement } from "react"; -import { Sidebar } from "@/app/components/docs/sidebar"; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>): ReactElement { - return ( -
- - {children} -
- ); -} diff --git a/src/app/common/documentation.ts b/src/app/common/documentation.ts index cb993d8..b7a67fc 100644 --- a/src/app/common/documentation.ts +++ b/src/app/common/documentation.ts @@ -119,7 +119,7 @@ export function getDocContent(path?: string[]): DocsContentMetadata | undefined * @param limit the maximum number of results */ export function searchDocs( - query: string, + query?: string, limit?: number, ): { title: string; @@ -129,7 +129,7 @@ export function searchDocs( if (!limit) { limit = 5; // Default to 5 results } - return fuseIndex.search(query, { limit }).map(result => { + return fuseIndex.search(query || "", { limit }).map(result => { return { title: result.item.title, summary: result.item.summary, diff --git a/src/app/components/command-menu.tsx b/src/app/components/command-menu.tsx new file mode 100644 index 0000000..71587db --- /dev/null +++ b/src/app/components/command-menu.tsx @@ -0,0 +1,113 @@ +"use client"; + +import React, { ReactElement, useState } from "react"; +import { DocsContentMetadata } from "@/app/common/documentation"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/app/components/ui/command"; +import { Button, ButtonProps } from "@/app/components/ui/button"; +import { useRouter } from "next/navigation"; +import { cn } from "@/app/common/utils"; + +export function CommandMenu({ ...props }: ButtonProps): ReactElement { + const router = useRouter(); + + /** + * Whether to show the search + */ + const [open, setOpen] = useState(false); + + /** + * The pages that were found + */ + const [pages, setPages] = useState(undefined); + + // Handle keyboard shortcuts + React.useEffect(() => { + const down = (e: KeyboardEvent) => { + if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || e.key === "/") { + if ( + (e.target instanceof HTMLElement && e.target.isContentEditable) || + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLSelectElement + ) { + return; + } + + e.preventDefault(); + setOpen(open => !open); + } + }; + + return () => document.removeEventListener("keydown", down); + }, []); + + /** + * Search the documentation + * for the given query. + * + * @param query the query to search for + */ + async function searchDocs(query: string): Promise { + // Don't bother searching if the query is less than 3 characters + if (query.length < 3) { + setPages(undefined); + return; + } + + // Attempt to search for the query + const response = await fetch(`/api/docs/search?query=${query}`); + const pages: DocsContentMetadata[] = await response.json(); + setPages(pages); + } + + return ( + <> + + + + { + await searchDocs(search); + }} + /> + + No results found. + {pages && ( + + {pages.map(page => { + return ( + { + router.push(`/docs/${page.slug}`); + setOpen(false); + }} + > + {page.title} + + ); + })} + + )} + + + + ); +} diff --git a/src/app/components/docs/github-link.tsx b/src/app/components/docs/github-link.tsx index 78a7fb6..abe5ed3 100644 --- a/src/app/components/docs/github-link.tsx +++ b/src/app/components/docs/github-link.tsx @@ -16,7 +16,7 @@ export function GithubLink({ page }: GithubLink): ReactElement { href={`https://git.fascinated.cc/MinecraftUtilities/Frontend/src/branch/master/documentation/${page.slug}.md`} target="_blank" > - The GitHub logo + The GitHub logo ); } diff --git a/src/app/components/docs/search.tsx b/src/app/components/docs/search.tsx deleted file mode 100644 index 1105650..0000000 --- a/src/app/components/docs/search.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import React, { ReactElement, useState } from "react"; -import { Button } from "@/app/components/ui/button"; -import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/app/components/ui/dialog"; -import { DocsContentMetadata } from "@/app/common/documentation"; -import { DocumentationPages } from "@/app/components/docs/documentation-pages"; -import { SearchIcon } from "lucide-react"; - -export function Search(): ReactElement { - /** - * The pages that were found - */ - const [pages, setPages] = useState(undefined); - const [continueTyping, setContinueTyping] = useState(false); - - /** - * Search the documentation - * for the given query. - * - * @param query the query to search for - */ - async function searchDocs(query: string): Promise { - // Don't bother searching if the query is less than 3 characters - if (query.length < 3) { - if (query.length > 0) { - setContinueTyping(true); - } else { - setContinueTyping(false); - } - return setPages(undefined); - } - - // Attempt to search for the query - const response = await fetch(`/api/docs/search?query=${query}`); - const pages: DocsContentMetadata[] = await response.json(); - setPages(pages); - } - - return ( -
-
- - - - - - Search Documentation - searchDocs(event.target.value)} - /> - - {!pages && continueTyping &&

Continue typing...

} - - -
-
-
-
- ); -} diff --git a/src/app/components/docs/sidebar.tsx b/src/app/components/docs/sidebar.tsx deleted file mode 100644 index aeb5f46..0000000 --- a/src/app/components/docs/sidebar.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Search } from "@/app/components/docs/search"; -import { ReactElement } from "react"; - -export function Sidebar(): ReactElement { - return ( -
- -
- ); -} diff --git a/src/app/components/github-star.tsx b/src/app/components/github-star.tsx index e5ce233..d68de6a 100644 --- a/src/app/components/github-star.tsx +++ b/src/app/components/github-star.tsx @@ -3,8 +3,16 @@ import { ReactElement, useEffect, useState } from "react"; import { Star } from "lucide-react"; import Link from "next/link"; +import { cn } from "@/app/common/utils"; -export function GithubStar(): ReactElement { +type GithubStarProps = { + /** + * The class name for this component. + */ + className?: string; +}; + +export function GithubStar({ className }: GithubStarProps): ReactElement { const [starCount, setStarCount] = useState(0); const getStarCount = async () => { @@ -19,7 +27,10 @@ export function GithubStar(): ReactElement { return ( diff --git a/src/app/components/navbar.tsx b/src/app/components/navbar.tsx index 9e24def..1989391 100644 --- a/src/app/components/navbar.tsx +++ b/src/app/components/navbar.tsx @@ -2,13 +2,14 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { ReactElement } from "react"; +import { ReactElement, useEffect, useState } from "react"; import { HrefButton } from "./href-button"; import Logo from "./logo"; import { ToggleThemeButton } from "./theme-toggle-button"; import { GithubStar } from "@/app/components/github-star"; import { Card } from "@/app/components/card"; import { cn } from "@/app/common/utils"; +import { CommandMenu } from "@/app/components/command-menu"; type Page = { /** @@ -42,7 +43,8 @@ const pages: Page[] = [ ]; export default function NavBar(): ReactElement { - const path = usePathname(); + const path: string = usePathname(); + const isDocs: boolean = path ? path.includes("/docs") : false; return ( {/* Left */} -
+
-

Minecraft Utilities

+ + {/* Command Menu */} +
{/* Links */} -
+
{pages.map((page, index) => { const isActive: boolean = path ? path.includes(page.url) : false; @@ -79,7 +83,7 @@ export default function NavBar(): ReactElement { {/* Right */}
- +
); diff --git a/src/app/components/ui/command.tsx b/src/app/components/ui/command.tsx new file mode 100644 index 0000000..ad3d6d8 --- /dev/null +++ b/src/app/components/ui/command.tsx @@ -0,0 +1,135 @@ +"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 "@/app/common/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, +}; diff --git a/src/app/globals.css b/src/app/globals.css index 99825d5..05edfe4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,51 +2,51 @@ @tailwind components; @tailwind utilities; + @layer base { :root { --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; + --foreground: 0 0% 3.9%; + --card: 0 0% 95%; + --card-foreground: 0 0% 3.9%; --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 355.7 100% 97.3%; - --secondary: 5 5% 95%; - --secondary-foreground: 240 5.9% 10%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 221.2 83.2% 53.3%; - --radius: 0.5rem; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --radius: 0.3rem; } .dark { - --background: 20 14.3% 6.5%; - --background-accent: 20 14.3% 8.5%; - --foreground: 0 0% 95%; - --card: 24 9.8% 10%; - --card-foreground: 0 0% 95%; - --popover: 0 0% 9%; - --popover-foreground: 0 0% 95%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 144.9 80.4% 10%; - --secondary: 240 3.7% 15.9%; + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 8%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; --secondary-foreground: 0 0% 98%; - --muted: 0 0% 15%; - --muted-foreground: 240 5% 64.9%; - --accent: 12 6.5% 75%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 85.7% 97.3%; - --border: 240 5.9% 30%; - --input: 240 3.7% 15.9%; - --ring: 224.3 76.3% 48%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; } }