From 008af7c530559fd736974d911cb9cf1b1356e436 Mon Sep 17 00:00:00 2001 From: Kevin Bravo Date: Tue, 12 May 2026 09:53:41 -0400 Subject: [PATCH] Implement global search --- .../src/components/Common/GlobalSearch.tsx | 201 ++++++++++++++++++ frontend/src/components/Common/Navbar.tsx | 7 - frontend/src/routes/_layout.tsx | 16 +- 3 files changed, 216 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/Common/GlobalSearch.tsx diff --git a/frontend/src/components/Common/GlobalSearch.tsx b/frontend/src/components/Common/GlobalSearch.tsx new file mode 100644 index 0000000..211c375 --- /dev/null +++ b/frontend/src/components/Common/GlobalSearch.tsx @@ -0,0 +1,201 @@ +import { + Badge, + Box, + Flex, + Icon, + Input, + InputGroup, + InputLeftElement, + Text, + useColorModeValue, +} from "@chakra-ui/react" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { Link } from "@tanstack/react-router" +import { useMemo, useState } from "react" +import { FaSearch } from "react-icons/fa" + +import { + type ItemPublic, + ItemsService, + type UserPublic, + UsersService, +} from "../../client" + +type SearchResult = { + id: string + label: string + description?: string | null + type: "Page" | "Item" | "User" + path: string +} + +const pages: SearchResult[] = [ + { + id: "page-dashboard", + label: "Dashboard", + description: "Overview", + type: "Page", + path: "/", + }, + { + id: "page-items", + label: "Items", + description: "Items management", + type: "Page", + path: "/items", + }, + { + id: "page-settings", + label: "User Settings", + description: "Account settings", + type: "Page", + path: "/settings", + }, +] + +const normalize = (value?: string | null) => value?.toLowerCase().trim() ?? "" + +const itemToResult = (item: ItemPublic): SearchResult => ({ + id: `item-${item.id}`, + label: item.title, + description: item.description, + type: "Item", + path: "/items", +}) + +const userToResult = (user: UserPublic): SearchResult => ({ + id: `user-${user.id}`, + label: user.full_name || user.email, + description: user.full_name ? user.email : "User account", + type: "User", + path: "/admin", +}) + +const GlobalSearch = () => { + const [query, setQuery] = useState("") + const [isFocused, setIsFocused] = useState(false) + const queryClient = useQueryClient() + const currentUser = queryClient.getQueryData(["currentUser"]) + const bgColor = useColorModeValue("white", "ui.darkSlate") + const borderColor = useColorModeValue("gray.200", "whiteAlpha.300") + const hoverBg = useColorModeValue("gray.50", "whiteAlpha.100") + const mutedColor = useColorModeValue("gray.600", "gray.400") + const searchTerm = normalize(query) + + const { data: items } = useQuery({ + queryKey: ["items"], + queryFn: () => ItemsService.readItems({}), + enabled: isFocused, + }) + + const { data: users } = useQuery({ + queryKey: ["users"], + queryFn: () => UsersService.readUsers({}), + enabled: isFocused && Boolean(currentUser?.is_superuser), + }) + + const results = useMemo(() => { + if (!searchTerm) { + return [] + } + + const searchablePages = currentUser?.is_superuser + ? [ + ...pages, + { + id: "page-admin", + label: "Admin", + description: "User management", + type: "Page" as const, + path: "/admin", + }, + ] + : pages + + return [ + ...searchablePages, + ...(items?.data.map(itemToResult) ?? []), + ...(users?.data.map(userToResult) ?? []), + ] + .filter((result) => + [result.label, result.description, result.type] + .map(normalize) + .some((value) => value.includes(searchTerm)), + ) + .slice(0, 8) + }, [currentUser?.is_superuser, items?.data, searchTerm, users?.data]) + + const showResults = isFocused && query.length > 0 + + return ( + + + + + + setQuery(event.target.value)} + onFocus={() => setIsFocused(true)} + onBlur={() => window.setTimeout(() => setIsFocused(false), 120)} + placeholder="Search" + fontSize={{ base: "sm", md: "inherit" }} + borderRadius="8px" + /> + + + {showResults && ( + + {results.length > 0 ? ( + results.map((result) => ( + setQuery("")} + > + + + {result.label} + + {result.description && ( + + {result.description} + + )} + + + {result.type} + + + )) + ) : ( + + No results found + + )} + + )} + + ) +} + +export default GlobalSearch diff --git a/frontend/src/components/Common/Navbar.tsx b/frontend/src/components/Common/Navbar.tsx index edcb3d0..0ee2a04 100644 --- a/frontend/src/components/Common/Navbar.tsx +++ b/frontend/src/components/Common/Navbar.tsx @@ -15,13 +15,6 @@ const Navbar = ({ type }: NavbarProps) => { return ( <> - {/* TODO: Complete search functionality */} - {/* - - - - - */}