impl command menu and change theme colors
All checks were successful
Deploy App / docker (ubuntu-latest) (push) Successful in 3m26s

This commit is contained in:
Lee 2024-04-22 01:21:04 +01:00
parent a200fa045c
commit 49daf6f1a4
13 changed files with 330 additions and 137 deletions

@ -29,6 +29,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",
"cmdk": "^1.0.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"lucide-react": "^0.372.0", "lucide-react": "^0.372.0",
"mcutils-library": "^1.2.6", "mcutils-library": "^1.2.6",

18
pnpm-lock.yaml generated

@ -71,6 +71,9 @@ dependencies:
clsx: clsx:
specifier: ^2.1.0 specifier: ^2.1.0
version: 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: fuse.js:
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.0.0 version: 7.0.0
@ -3016,6 +3019,21 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: false 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: /co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}

@ -13,6 +13,7 @@ import {
import { capitalizeFirstLetter } from "@/app/common/string-utils"; import { capitalizeFirstLetter } from "@/app/common/string-utils";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { GithubLink } from "@/app/components/docs/github-link"; import { GithubLink } from "@/app/components/docs/github-link";
import { CommandMenu } from "@/app/components/command-menu";
type DocumentationPageParams = { type DocumentationPageParams = {
params: { params: {
@ -87,8 +88,10 @@ export default function Page({ params: { slug } }: DocumentationPageParams) {
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
<div className="flex flex-row gap-2 items-center">
<GithubLink page={page} /> <GithubLink page={page} />
</div> </div>
</div>
{/* The documentation page title and description */} {/* The documentation page title and description */}
<div className="text-center mb-4"> <div className="text-center mb-4">

@ -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 (
<div className="w-full flex flex-col items-center gap-2 h-full md:flex-row md:items-start">
<Sidebar />
{children}
</div>
);
}

@ -119,7 +119,7 @@ export function getDocContent(path?: string[]): DocsContentMetadata | undefined
* @param limit the maximum number of results * @param limit the maximum number of results
*/ */
export function searchDocs( export function searchDocs(
query: string, query?: string,
limit?: number, limit?: number,
): { ): {
title: string; title: string;
@ -129,7 +129,7 @@ export function searchDocs(
if (!limit) { if (!limit) {
limit = 5; // Default to 5 results limit = 5; // Default to 5 results
} }
return fuseIndex.search(query, { limit }).map(result => { return fuseIndex.search(query || "", { limit }).map(result => {
return { return {
title: result.item.title, title: result.item.title,
summary: result.item.summary, summary: result.item.summary,

@ -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<boolean>(false);
/**
* The pages that were found
*/
const [pages, setPages] = useState<DocsContentMetadata[] | undefined>(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<void> {
// 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 (
<>
<Button
variant="outline"
className={cn(
"relative h-8 w-full justify-start rounded-[0.5rem] bg-background text-sm font-normal text-muted-foreground shadow-none sm:pr-12 md:w-40 lg:w-64",
props.className,
)}
onClick={() => setOpen(true)}
>
Search
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput
placeholder="Query..."
onValueChange={async search => {
await searchDocs(search);
}}
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{pages && (
<CommandGroup heading="Suggestions">
{pages.map(page => {
return (
<CommandItem
key={page.slug}
onSelect={() => {
router.push(`/docs/${page.slug}`);
setOpen(false);
}}
>
{page.title}
</CommandItem>
);
})}
</CommandGroup>
)}
</CommandList>
</CommandDialog>
</>
);
}

@ -16,7 +16,7 @@ export function GithubLink({ page }: GithubLink): ReactElement {
href={`https://git.fascinated.cc/MinecraftUtilities/Frontend/src/branch/master/documentation/${page.slug}.md`} href={`https://git.fascinated.cc/MinecraftUtilities/Frontend/src/branch/master/documentation/${page.slug}.md`}
target="_blank" target="_blank"
> >
<Image src="/media/github.png" alt="The GitHub logo" width={24} height={24} className="filter dark:invert" /> <Image src="/media/github.png" alt="The GitHub logo" width={32} height={32} className="filter dark:invert" />
</Link> </Link>
); );
} }

@ -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<DocsContentMetadata[] | undefined>(undefined);
const [continueTyping, setContinueTyping] = useState<boolean>(false);
/**
* Search the documentation
* for the given query.
*
* @param query the query to search for
*/
async function searchDocs(query: string): Promise<void> {
// 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 (
<div className="w-full md:w-[250px] min-h-fit h-fit bg-card rounded-lg p-3">
<div className="flex flex-col w-full">
<Dialog>
<DialogTrigger asChild>
<Button className="flex items-center gap-1">
<SearchIcon width={16} height={16} />
<p>Search</p>
</Button>
</DialogTrigger>
<DialogContent className="w-screen md:w-[600px]">
<DialogTitle>Search Documentation</DialogTitle>
<input
type="text"
placeholder="Query..."
className="w-full p-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:border-ring"
onChange={event => searchDocs(event.target.value)}
/>
{!pages && continueTyping && <p>Continue typing...</p>}
<DocumentationPages pages={pages} />
</DialogContent>
</Dialog>
</div>
</div>
);
}

@ -1,10 +0,0 @@
import { Search } from "@/app/components/docs/search";
import { ReactElement } from "react";
export function Sidebar(): ReactElement {
return (
<div className="flex flex-row gap-2 h-full md:flex-col">
<Search />
</div>
);
}

@ -3,8 +3,16 @@
import { ReactElement, useEffect, useState } from "react"; import { ReactElement, useEffect, useState } from "react";
import { Star } from "lucide-react"; import { Star } from "lucide-react";
import Link from "next/link"; 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 [starCount, setStarCount] = useState(0);
const getStarCount = async () => { const getStarCount = async () => {
@ -19,7 +27,10 @@ export function GithubStar(): ReactElement {
return ( return (
<Link <Link
className="bg-github-green px-2 py-1 rounded-lg items-center gap-1 hover:opacity-85 transform-gpu transition-all hidden md:flex" className={cn(
"bg-github-green px-2 py-1 rounded-lg items-center gap-1 hover:opacity-85 transform-gpu transition-all hidden md:flex",
className,
)}
href="https://github.com/RealFascinated/MinecraftUtilities" href="https://github.com/RealFascinated/MinecraftUtilities"
target="_blank" target="_blank"
> >

@ -2,13 +2,14 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { ReactElement } from "react"; import { ReactElement, useEffect, useState } from "react";
import { HrefButton } from "./href-button"; import { HrefButton } from "./href-button";
import Logo from "./logo"; import Logo from "./logo";
import { ToggleThemeButton } from "./theme-toggle-button"; import { ToggleThemeButton } from "./theme-toggle-button";
import { GithubStar } from "@/app/components/github-star"; import { GithubStar } from "@/app/components/github-star";
import { Card } from "@/app/components/card"; import { Card } from "@/app/components/card";
import { cn } from "@/app/common/utils"; import { cn } from "@/app/common/utils";
import { CommandMenu } from "@/app/components/command-menu";
type Page = { type Page = {
/** /**
@ -42,7 +43,8 @@ const pages: Page[] = [
]; ];
export default function NavBar(): ReactElement { export default function NavBar(): ReactElement {
const path = usePathname(); const path: string = usePathname();
const isDocs: boolean = path ? path.includes("/docs") : false;
return ( return (
<Card <Card
@ -50,15 +52,17 @@ export default function NavBar(): ReactElement {
classNameContent="p-0 relative rounded-lg flex justify-between items-center gap-3 px-3 bg-opacity-85 h-12" classNameContent="p-0 relative rounded-lg flex justify-between items-center gap-3 px-3 bg-opacity-85 h-12"
> >
{/* Left */} {/* Left */}
<div className="z-50"> <div className={cn("flex flex-row items-center gap-2 z-50", isDocs ? "w-full md:w-fit" : "w-fit")}>
<Link href="/" className="flex items-center gap-2"> <Link href="/" className="flex items-center gap-2">
<Logo /> <Logo />
<p className="hidden md:block text-lg font-semibold">Minecraft Utilities</p>
</Link> </Link>
{/* Command Menu */}
<CommandMenu className={cn(isDocs ? "" : "hidden md:inline-flex")} />
</div> </div>
{/* Links */} {/* Links */}
<div className="absolute inset-x-0 flex justify-center"> <div className={cn("absolute inset-x-0 justify-center", isDocs ? "hidden md:flex" : "flex")}>
<div className="flex gap-4"> <div className="flex gap-4">
{pages.map((page, index) => { {pages.map((page, index) => {
const isActive: boolean = path ? path.includes(page.url) : false; const isActive: boolean = path ? path.includes(page.url) : false;
@ -79,7 +83,7 @@ export default function NavBar(): ReactElement {
{/* Right */} {/* Right */}
<div className="flex gap-4 items-center z-50"> <div className="flex gap-4 items-center z-50">
<ToggleThemeButton /> <ToggleThemeButton />
<GithubStar /> <GithubStar className={isDocs ? "hidden md:flex" : "hidden"} />
</div> </div>
</Card> </Card>
); );

@ -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<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,
)}
shouldFilter={false}
{...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,
};

@ -2,51 +2,51 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 240 10% 3.9%; --foreground: 0 0% 3.9%;
--card: 0 0% 100%; --card: 0 0% 95%;
--card-foreground: 240 10% 3.9%; --card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%; --popover-foreground: 0 0% 3.9%;
--primary: 221.2 83.2% 53.3%; --primary: 0 0% 9%;
--primary-foreground: 355.7 100% 97.3%; --primary-foreground: 0 0% 98%;
--secondary: 5 5% 95%; --secondary: 0 0% 96.1%;
--secondary-foreground: 240 5.9% 10%; --secondary-foreground: 0 0% 9%;
--muted: 240 4.8% 95.9%; --muted: 0 0% 96.1%;
--muted-foreground: 240 3.8% 46.1%; --muted-foreground: 0 0% 45.1%;
--accent: 240 4.8% 95.9%; --accent: 0 0% 96.1%;
--accent-foreground: 240 5.9% 10%; --accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%; --border: 0 0% 89.8%;
--input: 240 5.9% 90%; --input: 0 0% 89.8%;
--ring: 221.2 83.2% 53.3%; --ring: 0 0% 3.9%;
--radius: 0.5rem; --radius: 0.3rem;
} }
.dark { .dark {
--background: 20 14.3% 6.5%; --background: 0 0% 3.9%;
--background-accent: 20 14.3% 8.5%; --foreground: 0 0% 98%;
--foreground: 0 0% 95%; --card: 0 0% 8%;
--card: 24 9.8% 10%; --card-foreground: 0 0% 98%;
--card-foreground: 0 0% 95%; --popover: 0 0% 3.9%;
--popover: 0 0% 9%; --popover-foreground: 0 0% 98%;
--popover-foreground: 0 0% 95%; --primary: 0 0% 98%;
--primary: 217.2 91.2% 59.8%; --primary-foreground: 0 0% 9%;
--primary-foreground: 144.9 80.4% 10%; --secondary: 0 0% 14.9%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 0 0% 98%;
--muted: 0 0% 15%; --muted: 0 0% 14.9%;
--muted-foreground: 240 5% 64.9%; --muted-foreground: 0 0% 63.9%;
--accent: 12 6.5% 75%; --accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%; --destructive-foreground: 0 0% 98%;
--border: 240 5.9% 30%; --border: 0 0% 14.9%;
--input: 240 3.7% 15.9%; --input: 0 0% 14.9%;
--ring: 224.3 76.3% 48%; --ring: 0 0% 83.1%;
} }
} }