diff --git a/src/components/layout/components-layout/ComponentSearchbar.tsx b/src/components/layout/components-layout/ComponentSearchbar.tsx new file mode 100644 index 0000000..62944ab --- /dev/null +++ b/src/components/layout/components-layout/ComponentSearchbar.tsx @@ -0,0 +1,197 @@ +"use client"; + +import { useComponentSearch } from "@/hooks/useComponentSearch"; +import { AnimatePresence, motion } from "motion/react"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { FiCommand, FiSearch } from "react-icons/fi"; + +const ComponentSearchbar = () => { + const { isOpen, query, setQuery, filteredResults, toggleOpen, closeModal } = + useComponentSearch(); + const [selectedIndex, setSelectedIndex] = useState(0); + const [mounted, setMounted] = useState(false); + const inputRef = useRef(null); + const listRef = useRef(null); + const router = useRouter(); + + useEffect(() => setMounted(true), []); + + useEffect(() => setSelectedIndex(0), [filteredResults]); + + useEffect(() => { + if (isOpen && inputRef.current) inputRef.current.focus(); + }, [isOpen]); + + useEffect(() => { + if (selectedIndex >= 0 && listRef.current) { + const activeItem = listRef.current.children[selectedIndex] as HTMLElement; + if (activeItem) { + activeItem.scrollIntoView({ block: "nearest", behavior: "auto" }); + } + } + }, [selectedIndex]); + + const handleNavigate = useCallback( + (href: string) => { + router.push(href); + closeModal(); + }, + [router, closeModal] + ); + + useEffect(() => { + if (!isOpen) return; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setSelectedIndex((prev) => + prev < filteredResults.length - 1 ? prev + 1 : 0 + ); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setSelectedIndex((prev) => + prev > 0 ? prev - 1 : filteredResults.length - 1 + ); + } else if (e.key === "Enter" && filteredResults[selectedIndex]) { + e.preventDefault(); + handleNavigate(filteredResults[selectedIndex].href); + } + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [isOpen, filteredResults, selectedIndex, handleNavigate]); + + useEffect(() => { + document.body.style.overflow = isOpen ? "hidden" : ""; + return () => { + document.body.style.overflow = ""; + }; + }, [isOpen]); + + return ( + <> + + + {mounted && + createPortal( + + {isOpen && ( + + e.stopPropagation()} + className="w-[calc(100%-2rem)] max-w-lg bg-[var(--background-color)] border border-[var(--primary-color)] dark:border-white/10 rounded-xl shadow-2xl shadow-black/20 overflow-hidden" + > +
+ + setQuery(e.target.value)} + placeholder="Search components..." + className="flex-1 bg-transparent text-[var(--black-color)] dark:text-white placeholder:text-[var(--black-color)]/40 dark:placeholder:text-white/40 focus:outline-none text-sm" + /> + + ESC + +
+ +
+ {filteredResults.length === 0 ? ( +
+ No results found for “{query}” +
+ ) : ( +
+ {filteredResults.map((item, index) => ( + + ))} +
+ )} +
+ +
+
+ + + + Navigate + + + + Open + +
+ + {filteredResults.length} result{filteredResults.length !== 1 ? "s" : ""} + +
+
+
+ )} +
, + document.body + )} + + ); +}; + +export default ComponentSearchbar; diff --git a/src/components/layout/components-layout/ComponentsNavbar.tsx b/src/components/layout/components-layout/ComponentsNavbar.tsx index d3542cd..a9ecea8 100644 --- a/src/components/layout/components-layout/ComponentsNavbar.tsx +++ b/src/components/layout/components-layout/ComponentsNavbar.tsx @@ -6,6 +6,7 @@ import { useAppContext } from "@/context/AppContext"; import { useEffect, useState } from "react"; import { FaGithub, FaXTwitter } from "react-icons/fa6"; import { GoSidebarCollapse, GoSidebarExpand } from "react-icons/go"; +import ComponentSearchbar from "./ComponentSearchbar"; const ComponentsNavbar = () => { const { sideBar, setSideBar } = useAppContext(); @@ -22,6 +23,7 @@ const ComponentsNavbar = () => { fetchStars(); }, []); + return (
{ : "w-full xl:w-[calc(100vw-5rem)]" }`} > -