search impl
All checks were successful
Deploy / deploy (ubuntu-latest, 2.44.0) (push) Successful in 1m16s

Took 13 minutes
This commit is contained in:
Braydon 2024-10-06 16:44:03 -04:00
parent 80425ee7c8
commit 8298e33f44
9 changed files with 468 additions and 74 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -14,12 +14,15 @@
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.1.5",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"framer-motion": "^11.11.1",
"lucide-react": "^0.447.0",
"next": "^15.0.0-canary.179",

View File

@ -2,7 +2,7 @@ import type { Metadata, Viewport } from "next";
import "./styles/globals.css";
import { ReactElement, ReactNode } from "react";
import { ThemeProvider } from "@/components/theme-provider";
import Navbar from "@/components/navbar";
import Navbar from "@/components/navbar/navbar";
import Sidebar from "@/components/sidebar/sidebar";
/**
@ -49,7 +49,7 @@ const RootLayout = ({
}}
>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
<div className="px-10 max-w-[90rem] mx-auto min-h-screen flex flex-col">
<div className="px-7 max-w-[90rem] mx-auto min-h-screen flex flex-col">
<Navbar />
<div className="w-full h-full flex flex-grow gap-5">
<Sidebar />

View File

@ -1,71 +0,0 @@
import { ReactElement } from "react";
import Link from "next/link";
import Image from "next/image";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
const Navbar = (): ReactElement => (
<nav
className={cn(
"py-4 flex justify-between items-center",
"after:absolute after:inset-x-0 after:top-[4.2rem] after:h-0.5 after:bg-muted/55"
)}
>
{/* Branding */}
<Link
className="flex gap-1 items-end hover:opacity-75 transition-all transform-gpu select-none"
href="/"
draggable={false}
>
<h1 className="text-lg font-semibold">docs.</h1>
<Image
src="/media/logo.png"
alt="Pulse App Logo"
width={36}
height={36}
/>
</Link>
{/* Right */}
<div className="flex gap-7 items-center">
{/* Search */}
<Input
className="hidden xs:flex rounded-lg"
placeholder="Search the docs..."
disabled
/>
{/* Social */}
<div className="flex gap-5 items-center">
<SocialLink
name="GitHub"
link="https://github.com/PulseAppCC"
icon="/media/github.svg"
/>
<SocialLink
name="Discord"
link="https://discord.pulseapp.cc"
icon="/media/discord.svg"
/>
</div>
</div>
</nav>
);
const SocialLink = ({
name,
link,
icon,
}: {
name: string;
link: string;
icon: string;
}): ReactElement => (
<div className="relative w-6 h-6 hover:opacity-75 transition-all transform-gpu select-none">
<Link href={link} target="_blank" draggable={false}>
<Image src={icon} alt={`${name} Logo`} fill draggable={false} />
</Link>
</div>
);
export default Navbar;

View File

@ -0,0 +1,74 @@
import { ReactElement } from "react";
import Link from "next/link";
import Image from "next/image";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import QuickSearchDialog from "@/components/navbar/search-dialog";
import { getDocsContent } from "@/lib/mdx";
const Navbar = (): ReactElement => {
const pages: DocsContentMetadata[] = getDocsContent();
return (
<nav
className={cn(
"py-4 flex justify-between items-center",
"after:absolute after:inset-x-0 after:top-[4.2rem] after:h-0.5 after:bg-muted/55"
)}
>
{/* Branding */}
<Link
className="flex gap-1 items-end hover:opacity-75 transition-all transform-gpu select-none"
href="/public"
draggable={false}
>
<h1 className="text-lg font-semibold">docs.</h1>
<Image
src="/media/logo.png"
alt="Pulse App Logo"
width={36}
height={36}
/>
</Link>
{/* Right */}
<div className="flex gap-5 sm:gap-7 items-center transition-all transform-gpu">
{/* Search */}
<div className="hidden xs:flex">
<QuickSearchDialog pages={pages} />
</div>
{/* Social */}
<div className="flex gap-5 items-center">
<SocialLink
name="GitHub"
link="https://github.com/PulseAppCC"
icon="/media/github.svg"
/>
<SocialLink
name="Discord"
link="https://discord.pulseapp.cc"
icon="/media/discord.svg"
/>
</div>
</div>
</nav>
);
};
const SocialLink = ({
name,
link,
icon,
}: {
name: string;
link: string;
icon: string;
}): ReactElement => (
<div className="relative w-6 h-6 hover:opacity-75 transition-all transform-gpu select-none">
<Link href={link} target="_blank" draggable={false}>
<Image src={icon} alt={`${name} Logo`} fill draggable={false} />
</Link>
</div>
);
export default Navbar;

View File

@ -0,0 +1,110 @@
"use client";
import { ReactElement, useEffect, useState } from "react";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Input } from "@/components/ui/input";
import { useRouter } from "next/navigation";
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
/**
* The dialog for quickly searching the docs.
*
* @return the content jsx
*/
const QuickSearchDialog = ({
pages,
}: {
pages: DocsContentMetadata[];
}): ReactElement => {
const [open, setOpen] = useState<boolean>(false);
const router: AppRouterInstance = useRouter();
// Listen for CTRL + K keybinds to open this dialog
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent): void => {
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
event.preventDefault();
setOpen((open: boolean) => !open);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
// Render the contents
return (
<>
{/* Button to open */}
<div
className="hover:opacity-85 transition-all transform-gpu"
onClick={() => setOpen(true)}
>
<div className="absolute top-2.5 left-3 z-10">
<MagnifyingGlassIcon className="w-[1.15rem] h-[1.15rem]" />
</div>
<Input
className="pl-10 rounded-lg cursor-pointer"
type="search"
name="search"
placeholder="Search the docs..."
readOnly
/>
<div className="absolute top-1.5 right-3">
<kbd className="h-5 px-1.5 inline-flex gap-1 items-center bg-muted font-medium text-muted-foreground rounded select-none pointer-events-none">
<span></span>K
</kbd>
</div>
</div>
{/* 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">
{pages?.map(
(
result: DocsContentMetadata,
index: number
): ReactElement => (
<CommandItem
key={index}
className="flex flex-col gap-1 items-start"
onSelect={() => {
setOpen(false);
router.push(`/${result.slug}`);
}}
>
<h1 className="text-primary font-bold">
{result.title}
</h1>
<p className="opacity-60">
{result.summary}
</p>
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
};
export default QuickSearchDialog;

View File

@ -12,6 +12,7 @@ import { ChevronRight } from "lucide-react";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
const SidebarLinks = ({
pages,
@ -84,7 +85,7 @@ const CategoryItem = ({
animate={{ rotate: isOpen ? 90 : 180 }}
transition={{ duration: 0.2 }}
>
<ChevronRight className="h-4 w-4" />
<ChevronRightIcon className="w-4 h-4" />
</motion.div>
)}
</Button>

View File

@ -0,0 +1,155 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { MagnifyingGlassIcon } from "@radix-ui/react-icons"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/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">
<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="">
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 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 data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground 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,
}

View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}