AutoLoop / components /global-search.tsx
shubhjn's picture
Deploy Clean V1
8dd52b2
"use client";
import * as React from "react";
import { useRouter } from "next/navigation";
import { Search, Loader2, Building2, Mail, Workflow, FileText } from "lucide-react";
import { useDebounce } from "@/hooks/use-debounce";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { useApi } from "@/hooks/use-api";
interface SearchResult {
type: 'business' | 'template' | 'workflow';
id: string;
title: string;
subtitle: string;
url: string;
}
export function GlobalSearch() {
const router = useRouter();
const [open, setOpen] = React.useState(false);
const [query, setQuery] = React.useState("");
const debouncedQuery = useDebounce(query, 300);
const [results, setResults] = React.useState<SearchResult[]>([]);
const { get, loading } = useApi<{ results: SearchResult[] }>();
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
React.useEffect(() => {
if (!debouncedQuery || debouncedQuery.length < 2) {
setResults([]);
return;
}
const fetchResults = async () => {
const data = await get(`/api/search?q=${encodeURIComponent(debouncedQuery)}`);
if (data && data.results) {
setResults(data.results);
}
};
fetchResults();
}, [debouncedQuery, get]);
const handleSelect = (url: string) => {
setOpen(false);
router.push(url);
};
const getIcon = (type: string) => {
switch (type) {
case 'business': return <Building2 className="mr-2 h-4 w-4" />;
case 'template': return <Mail className="mr-2 h-4 w-4" />;
case 'workflow': return <Workflow className="mr-2 h-4 w-4" />;
default: return <FileText className="mr-2 h-4 w-4" />;
}
};
return (
<>
<Button
variant="outline"
className={cn(
"relative w-full justify-start text-sm text-muted-foreground sm:pr-12 md:w-40 lg:w-64"
)}
onClick={() => setOpen(true)}
>
<Search className="mr-2 h-4 w-4" />
<span className="hidden lg:inline-flex">Search...</span>
<span className="inline-flex lg:hidden">Search...</span>
<kbd className="pointer-events-none absolute right-1.5 top-1.5 hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
<span className="text-xs"></span>K
</kbd>
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="p-4 overflow-hidden shadow-2xl">
<DialogTitle className="sr-only">Global Search</DialogTitle>
<Command className="**:[cmdk-group-heading]:px-4 **:[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">
<CommandInput
placeholder="Type to search..."
value={query}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>
{loading ? (
<div className="flex items-center justify-center py-6">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : query.length > 0 && query.length < 2 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
Type at least 2 characters...
</div>
) : (
"No results found."
)}
</CommandEmpty>
{results.length > 0 && (
<CommandGroup heading="Results">
{results.map((result) => (
<CommandItem
key={`${result.type}-${result.id}`}
onSelect={() => handleSelect(result.url)}
className="cursor-pointer"
>
{getIcon(result.type)}
<span>{result.title}</span>
{result.subtitle && (
<span className="ml-2 text-xs text-muted-foreground">
- {result.subtitle}
</span>
)}
</CommandItem>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</DialogContent>
</Dialog>
</>
);
}