import { useState, useEffect, useCallback, useRef } from "react"; import { createRoute, useNavigate } from "@tanstack/react-router"; import { Route as appRoute } from "../_app"; import { useQuery } from "@tanstack/react-query"; import { api, useApiQuery } from "@/lib/api"; import { Search as SearchIcon, X, Clock, ArrowRight } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import type { SearchResult } from "@/lib/types"; const SEARCH_TYPES = [ { id: "task", label: "Tasks", color: "bg-blue-500" }, { id: "habit", label: "Habits", color: "bg-green-500" }, { id: "project", label: "Projects", color: "bg-purple-500" }, { id: "note", label: "Notes", color: "bg-amber-500" }, { id: "domain", label: "Domains", color: "bg-indigo-500" }, ]; function SearchPage() { const navigate = useNavigate(); const [query, setQuery] = useState(""); const [debouncedQuery, setDebouncedQuery] = useState(""); const [selectedTypes, setSelectedTypes] = useState>(new Set(SEARCH_TYPES.map((t) => t.id))); const [recentSearches, setRecentSearches] = useState(() => { try { return JSON.parse(localStorage.getItem("recentSearches") || "[]"); } catch { return []; } }); const inputRef = useRef(null); // Debounce search useEffect(() => { const timer = setTimeout(() => setDebouncedQuery(query), 300); return () => clearTimeout(timer); }, [query]); useEffect(() => { if (inputRef.current) inputRef.current.focus(); }, []); const { data: searchData, isLoading } = useApiQuery<{ results: SearchResult[]; totalCount: number }>( ["search", debouncedQuery, ...Array.from(selectedTypes)], "/search?q=" + encodeURIComponent(debouncedQuery) + "&types=" + Array.from(selectedTypes).join(",") + "&limit=50" ); const results = searchData?.results || []; const toggleType = (typeId: string) => { const next = new Set(selectedTypes); if (next.has(typeId)) next.delete(typeId); else next.add(typeId); setSelectedTypes(next); }; const handleSearch = (q: string) => { setQuery(q); if (q.trim() && q.trim().length > 2) { setRecentSearches((prev) => { const next = [q.trim(), ...prev.filter((s) => s !== q.trim())].slice(0, 10); localStorage.setItem("recentSearches", JSON.stringify(next)); return next; }); } }; const groupedResults = results.reduce((acc, r) => { if (!acc[r.type]) acc[r.type] = []; acc[r.type].push(r); return acc; }, {} as Record); return (
{/* Search bar */}
handleSearch(e.target.value)} placeholder="Search tasks, notes, projects, habits..." className="pl-10 pr-10 h-12 text-lg" /> {query && ( )}
{/* Type filters */}
{SEARCH_TYPES.map((type) => ( ))}
{/* Recent searches */} {!debouncedQuery && recentSearches.length > 0 && (

Recent Searches

{recentSearches.map((s, i) => ( ))}
)} {/* Results */} {debouncedQuery && (
{isLoading ? (
Searching...
) : results.length === 0 ? (
No results found for "{debouncedQuery}"
) : ( Object.entries(groupedResults).map(([type, typeResults]) => { const typeDef = SEARCH_TYPES.find((t) => t.id === type); return (

{typeDef?.label || type} {typeResults.length}

{typeResults.map((result) => (
navigate({ to: result.link as any })} >

{result.title}

{result.snippet && (

)}

))}
); }) )}
)}
); } export const Route = createRoute({ getParentRoute: () => appRoute, path: "/search", component: SearchPage, });