From f995c844781b52202b25c2ae45f435f0bcbe65e5 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 22 Apr 2025 14:21:48 +0200 Subject: [PATCH 1/2] feat: integrate PostHog for tracking when no icons are found in search --- web/src/app/icons/components/icon-search.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/src/app/icons/components/icon-search.tsx b/web/src/app/icons/components/icon-search.tsx index 7502878b..b89901a9 100644 --- a/web/src/app/icons/components/icon-search.tsx +++ b/web/src/app/icons/components/icon-search.tsx @@ -24,6 +24,7 @@ import { useTheme } from "next-themes" import Image from "next/image" import Link from "next/link" import { usePathname, useRouter, useSearchParams } from "next/navigation" +import posthog from "posthog-js" import { useCallback, useEffect, useMemo, useRef, useState } from "react" type SortOption = "relevance" | "alphabetical-asc" | "alphabetical-desc" | "newest" @@ -139,6 +140,8 @@ export function IconSearch({ icons }: IconSearchProps) { [pathname, router, initialSort], ) + + const handleSearch = useCallback( (query: string) => { setSearchQuery(query) @@ -193,6 +196,14 @@ export function IconSearch({ icons }: IconSearchProps) { } }, []) + useEffect(() => { + if (filteredIcons.length === 0) { + posthog.capture("no icons found", { + query: searchQuery, + }) + } + }, [filteredIcons, searchQuery]) + if (!searchParams) return null const getSortLabel = (sort: SortOption) => { From b3b88414e71e2806e1ee766f396745243f03524f Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 22 Apr 2025 15:28:23 +0200 Subject: [PATCH 2/2] feat: implement debounced search query and normalize filtering --- web/src/app/icons/components/icon-search.tsx | 37 ++++++++++++++------ web/src/components/PostHogProvider.tsx | 2 +- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/web/src/app/icons/components/icon-search.tsx b/web/src/app/icons/components/icon-search.tsx index b89901a9..b9a515bc 100644 --- a/web/src/app/icons/components/icon-search.tsx +++ b/web/src/app/icons/components/icon-search.tsx @@ -37,11 +37,20 @@ export function IconSearch({ icons }: IconSearchProps) { 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() + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedQuery(searchQuery) + }, 200) + + return () => clearTimeout(timer) + }, [searchQuery]) + // Extract all unique categories const allCategories = useMemo(() => { const categories = new Set() @@ -66,11 +75,17 @@ export function IconSearch({ icons }: IconSearchProps) { // Then filter by search query if (query.trim()) { - const q = query.toLowerCase() + // 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 }) => { - if (name.toLowerCase().includes(q)) return true - if (data.aliases.some((alias) => alias.toLowerCase().includes(q))) return true - if (data.categories.some((category) => category.toLowerCase().includes(q))) return true + // 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 }) } @@ -89,6 +104,7 @@ export function IconSearch({ icons }: IconSearchProps) { } // Default sort (relevance or fallback to alphabetical) + // TODO: Implement actual relevance sorting return filtered.sort((a, b) => a.name.localeCompare(b.name)) }, [icons], @@ -114,10 +130,10 @@ export function IconSearch({ icons }: IconSearchProps) { return matches }, [icons, searchQuery]) - // Use useMemo for filtered icons + // Use useMemo for filtered icons with debounced query const filteredIcons = useMemo(() => { - return filterIcons(searchQuery, selectedCategories, sortOption) - }, [filterIcons, searchQuery, selectedCategories, sortOption]) + return filterIcons(debouncedQuery, selectedCategories, sortOption) + }, [filterIcons, debouncedQuery, selectedCategories, sortOption]) const updateResults = useCallback( (query: string, categories: string[], sort: SortOption) => { @@ -140,8 +156,6 @@ export function IconSearch({ icons }: IconSearchProps) { [pathname, router, initialSort], ) - - const handleSearch = useCallback( (query: string) => { setSearchQuery(query) @@ -197,7 +211,10 @@ export function IconSearch({ icons }: IconSearchProps) { }, []) useEffect(() => { - if (filteredIcons.length === 0) { + if (filteredIcons.length === 0 && searchQuery && searchQuery.length > 3) { + console.log("no icons found", { + query: searchQuery, + }) posthog.capture("no icons found", { query: searchQuery, }) diff --git a/web/src/components/PostHogProvider.tsx b/web/src/components/PostHogProvider.tsx index 203ed29c..cf8d2383 100644 --- a/web/src/components/PostHogProvider.tsx +++ b/web/src/components/PostHogProvider.tsx @@ -7,7 +7,7 @@ import { Suspense, useEffect } from "react" export function PostHogProvider({ children }: { children: React.ReactNode }) { useEffect(() => { - if (process.env.NODE_ENV === "development" || process.env.DISABLE_POSTHOG === "true") return + if (process.env.DISABLE_POSTHOG === "true") return // biome-ignore lint/style/noNonNullAssertion: The NEXT_PUBLIC_POSTHOG_KEY environment variable is guaranteed to be set in production. posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { ui_host: "https://eu.posthog.com",