"use client" import { VirtualizedIconsGrid } from "@/components/icon-grid" import { IconSubmissionContent } from "@/components/icon-submission-form" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { MagicCard } from "@/components/magicui/magic-card" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Separator } from "@/components/ui/separator" import { BASE_URL } from "@/constants" import type { Icon, IconSearchProps } from "@/types/icons" import { ArrowDownAZ, ArrowUpZA, Calendar, Filter, Search, SortAsc, X } from "lucide-react" import { useTheme } from "next-themes" import { usePathname, useRouter, useSearchParams } from "next/navigation" import Image from "next/image" import Link from "next/link" import posthog from "posthog-js" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { toast } from "sonner" type SortOption = "relevance" | "alphabetical-asc" | "alphabetical-desc" | "newest" export function IconSearch({ icons }: IconSearchProps) { const searchParams = useSearchParams() const initialQuery = searchParams.get("q") const initialCategories = searchParams.getAll("category") const initialSort = (searchParams.get("sort") as SortOption) || "relevance" const router = useRouter() const pathname = usePathname() const [searchQuery, setSearchQuery] = useState(initialQuery ?? "") const [debouncedQuery, setDebouncedQuery] = useState(initialQuery ?? "") const [selectedCategories, setSelectedCategories] = useState(initialCategories ?? []) const [sortOption, setSortOption] = useState(initialSort) const timeoutRef = useRef(null) const { resolvedTheme } = useTheme() const [isLazyRequestSubmitted, setIsLazyRequestSubmitted] = useState(false) useEffect(() => { const timer = setTimeout(() => { setDebouncedQuery(searchQuery) }, 200) return () => clearTimeout(timer) }, [searchQuery]) // Extract all unique categories const allCategories = useMemo(() => { const categories = new Set() for (const icon of icons) { for (const category of icon.data.categories) { categories.add(category) } } return Array.from(categories).sort() }, [icons]) // Simple filter function using substring matching const filterIcons = useCallback( (query: string, categories: string[], sort: SortOption) => { // First filter by categories if any are selected let filtered = icons if (categories.length > 0) { filtered = filtered.filter(({ data }) => data.categories.some((cat) => categories.some((selectedCat) => cat.toLowerCase() === selectedCat.toLowerCase())), ) } // Then filter by search query if (query.trim()) { // Normalization function: lowercase, remove spaces and hyphens const normalizeString = (str: string) => str.toLowerCase().replace(/[-\s]/g, "") const normalizedQuery = normalizeString(query) filtered = filtered.filter(({ name, data }) => { // Check normalized name if (normalizeString(name).includes(normalizedQuery)) return true // Check normalized aliases if (data.aliases.some((alias) => normalizeString(alias).includes(normalizedQuery))) return true // Check normalized categories if (data.categories.some((category) => normalizeString(category).includes(normalizedQuery))) return true return false }) } // Apply sorting if (sort === "alphabetical-asc") { return filtered.sort((a, b) => a.name.localeCompare(b.name)) } if (sort === "alphabetical-desc") { return filtered.sort((a, b) => b.name.localeCompare(a.name)) } if (sort === "newest") { return filtered.sort((a, b) => { return new Date(b.data.update.timestamp).getTime() - new Date(a.data.update.timestamp).getTime() }) } // Default sort (relevance or fallback to alphabetical) // TODO: Implement actual relevance sorting return filtered.sort((a, b) => a.name.localeCompare(b.name)) }, [icons], ) // Find matched aliases for display purposes const matchedAliases = useMemo(() => { if (!searchQuery.trim()) return {} const q = searchQuery.toLowerCase() const matches: Record = {} for (const { name, data } of icons) { // If name doesn't match but an alias does, store the first matching alias if (!name.toLowerCase().includes(q)) { const matchingAlias = data.aliases.find((alias) => alias.toLowerCase().includes(q)) if (matchingAlias) { matches[name] = matchingAlias } } } return matches }, [icons, searchQuery]) // Use useMemo for filtered icons with debounced query const filteredIcons = useMemo(() => { return filterIcons(debouncedQuery, selectedCategories, sortOption) }, [filterIcons, debouncedQuery, selectedCategories, sortOption]) const updateResults = useCallback( (query: string, categories: string[], sort: SortOption) => { const params = new URLSearchParams() if (query) params.set("q", query) // Clear existing category params and add new ones for (const category of categories) { params.append("category", category) } // Add sort parameter if not default if (sort !== "relevance" || initialSort !== "relevance") { params.set("sort", sort) } const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname router.push(newUrl, { scroll: false }) }, [pathname, router, initialSort], ) const handleSearch = useCallback( (query: string) => { setSearchQuery(query) if (timeoutRef.current) { clearTimeout(timeoutRef.current) } timeoutRef.current = setTimeout(() => { updateResults(query, selectedCategories, sortOption) }, 200) // Changed from 100ms to 200ms }, [updateResults, selectedCategories, sortOption], ) const handleCategoryChange = useCallback( (category: string) => { let newCategories: string[] if (selectedCategories.includes(category)) { // Remove the category if it's already selected newCategories = selectedCategories.filter((c) => c !== category) } else { // Add the category if it's not selected newCategories = [...selectedCategories, category] } setSelectedCategories(newCategories) updateResults(searchQuery, newCategories, sortOption) }, [updateResults, searchQuery, selectedCategories, sortOption], ) const handleSortChange = useCallback( (sort: SortOption) => { setSortOption(sort) updateResults(searchQuery, selectedCategories, sort) }, [updateResults, searchQuery, selectedCategories], ) const clearFilters = useCallback(() => { setSearchQuery("") setSelectedCategories([]) setSortOption("relevance") updateResults("", [], "relevance") }, [updateResults]) useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current) } } }, []) useEffect(() => { if (filteredIcons.length === 0 && searchQuery) { console.log("no icons found", { query: searchQuery, }) posthog.capture("no icons found", { query: searchQuery, }) } }, [filteredIcons, searchQuery]) if (!searchParams) return null const getSortLabel = (sort: SortOption) => { switch (sort) { case "relevance": return "Relevance" case "alphabetical-asc": return "Name (A-Z)" case "alphabetical-desc": return "Name (Z-A)" case "newest": return "Newest first" default: return "Sort" } } const getSortIcon = (sort: SortOption) => { switch (sort) { case "relevance": return case "alphabetical-asc": return case "alphabetical-desc": return case "newest": return default: return } } return ( <>
{/* Search input */}
handleSearch(e.target.value)} />
{/* Filter and sort controls */}
{/* Filter dropdown */} Select Categories
{allCategories.map((category) => ( handleCategoryChange(category)} className="cursor-pointer capitalize" > {category.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} ))}
{selectedCategories.length > 0 && ( <> { setSelectedCategories([]) updateResults(searchQuery, [], sortOption) }} className="cursor-pointer focus: focus:bg-rose-50 dark:focus:bg-rose-950/20" > Clear categories )}
{/* Sort dropdown */} Sort Icons handleSortChange(value as SortOption)}> Relevance Name (A-Z) Name (Z-A) Newest first {/* Clear all button */} {(searchQuery || selectedCategories.length > 0 || sortOption !== "relevance") && ( )}
{/* Active filter badges */} {selectedCategories.length > 0 && (
Selected:
{selectedCategories.map((category) => ( {category.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} ))}
)}
{filteredIcons.length === 0 ? (

Icon not found

Help us expand our collection

Can't submit it yourself?
) : ( <>

Found {filteredIcons.length} icon {filteredIcons.length !== 1 ? "s" : ""}.

{getSortIcon(sortOption)} {getSortLabel(sortOption)}
)} ) } function IconCard({ name, data: iconData, matchedAlias, }: { name: string data: Icon matchedAlias?: string | null }) { return (
{`${name}
{name.replace(/-/g, " ")} {matchedAlias && Alias: {matchedAlias}}
) } interface IconsGridProps { filteredIcons: { name: string; data: Icon }[] matchedAliases: Record } function IconsGrid({ filteredIcons, matchedAliases }: IconsGridProps) { return ( <>
{filteredIcons.slice(0, 120).map(({ name, data }) => ( ))}
{filteredIcons.length > 120 &&

And {filteredIcons.length - 120} more...

} ) }