From 575dee0580a9ac153f75b9304f9da4d5080e76b0 Mon Sep 17 00:00:00 2001 From: Bjorn Lammers Date: Sun, 27 Apr 2025 22:57:56 +0200 Subject: [PATCH 1/2] feat(icons/[id]): Refine related icons relevance, display limits, and styling --- web/src/components/icon-details.tsx | 78 +++++++++++++++++++---------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/web/src/components/icon-details.tsx b/web/src/components/icon-details.tsx index 59a4a121..f0dae881 100644 --- a/web/src/components/icon-details.tsx +++ b/web/src/components/icon-details.tsx @@ -10,7 +10,7 @@ import { formatIconName } from "@/lib/utils" import type { AuthorData, Icon, IconFile } from "@/types/icons" import confetti from "canvas-confetti" import { motion } from "framer-motion" -import { Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun } from "lucide-react" +import { Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun, ArrowRight } from "lucide-react" import dynamic from "next/dynamic" import Image from "next/image" import Link from "next/link" @@ -479,31 +479,57 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail - {iconData.categories && iconData.categories.length > 0 && ( -
- - - - - - - Other icons from {iconData.categories.map((cat) => cat.replace(/-/g, " ")).join(", ")} categories - - - - { - if (name === icon) return false - return data.categories?.some((cat) => iconData.categories?.includes(cat)) - }) - .map(([name, data]) => ({ name, data }))} - matchedAliases={{}} - /> - - -
- )} + {iconData.categories && iconData.categories.length > 0 && (() => { + const MAX_RELATED_ICONS = 16 + const currentCategories = iconData.categories || [] + + const relatedIconsWithScore = Object.entries(allIcons) + .map(([name, data]) => { + if (name === icon) return null // Exclude the current icon + + const otherCategories = data.categories || [] + const commonCategories = currentCategories.filter((cat) => otherCategories.includes(cat)) + const score = commonCategories.length + + return score > 0 ? { name, data, score } : null + }) + .filter((item): item is { name: string; data: Icon; score: number } => item !== null) // Type guard + .sort((a, b) => b.score - a.score) // Sort by score DESC + + const topRelatedIcons = relatedIconsWithScore.slice(0, MAX_RELATED_ICONS) + + const viewMoreUrl = `/icons?${currentCategories.map((cat) => `category=${encodeURIComponent(cat)}`).join("&")}` + + if (topRelatedIcons.length === 0) return null + + return ( +
+ + + + + + + Other icons from {currentCategories.map((cat) => cat.replace(/-/g, " ")).join(", ")} categories + + + + + {relatedIconsWithScore.length > MAX_RELATED_ICONS && ( +
+ +
+ )} +
+
+
+ ) + })()} ) } From 50c3a92b294448859905f26fca1b398b9d9c3be4 Mon Sep 17 00:00:00 2001 From: Bjorn Lammers Date: Sun, 27 Apr 2025 22:59:33 +0200 Subject: [PATCH 2/2] fix(web): Run Biome checks and apply fixes --- web/src/components/command-menu.tsx | 44 +++++-------- web/src/components/icon-details.tsx | 96 +++++++++++++++-------------- web/src/components/icon-search.tsx | 2 +- web/src/lib/utils.ts | 53 ++++++++-------- 4 files changed, 95 insertions(+), 100 deletions(-) diff --git a/web/src/components/command-menu.tsx b/web/src/components/command-menu.tsx index e954f530..d2a1d4fe 100644 --- a/web/src/components/command-menu.tsx +++ b/web/src/components/command-menu.tsx @@ -1,13 +1,13 @@ "use client" +import { Badge } from "@/components/ui/badge" import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command" import { useMediaQuery } from "@/hooks/use-media-query" -import { formatIconName, fuzzySearch, filterAndSortIcons } from "@/lib/utils" -import { useRouter } from "next/navigation" -import { useCallback, useEffect, useState, useMemo } from "react" +import { filterAndSortIcons, formatIconName, fuzzySearch } from "@/lib/utils" import type { IconWithName } from "@/types/icons" -import { Tag, Search as SearchIcon, Info } from "lucide-react" -import { Badge } from "@/components/ui/badge" +import { Info, Search as SearchIcon, Tag } from "lucide-react" +import { useRouter } from "next/navigation" +import { useCallback, useEffect, useMemo, useState } from "react" interface CommandMenuProps { icons: IconWithName[] @@ -37,10 +37,7 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO [externalOnOpenChange], ) - const filteredIcons = useMemo(() => - filterAndSortIcons({ icons, query, limit: 20 }), - [icons, query] - ) + const filteredIcons = useMemo(() => filterAndSortIcons({ icons, query, limit: 20 }), [icons, query]) const totalIcons = icons.length @@ -70,11 +67,7 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO } return ( - + {/* Icon Results */} - {filteredIcons.length > 0 && ( + {filteredIcons.length > 0 && filteredIcons.map(({ name, data }) => { const formatedIconName = formatIconName(name) const hasCategories = data.categories && data.categories.length > 0 @@ -97,7 +90,9 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO >
- {name.substring(0, 2).toUpperCase()} + + {name.substring(0, 2).toUpperCase()} +
{formatedIconName} @@ -110,9 +105,7 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO className="text-xs font-normal inline-flex items-center gap-1 whitespace-nowrap max-w-[120px] overflow-hidden" > - - {data.categories[0].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} - + {data.categories[0].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} {/* "+N" badge if more than one category */} {data.categories.length > 1 && ( @@ -124,8 +117,7 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO )} ) - }) - )} + })}
{/* Minimal empty state */} @@ -138,12 +130,10 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO {/* Separator and Browse section - Styled div outside CommandList */}
-
{ if (e.key === 'Enter' || e.key === ' ') handleBrowseAll() }} >
@@ -151,7 +141,7 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
Browse all icons – {totalIcons} available -
+
) diff --git a/web/src/components/icon-details.tsx b/web/src/components/icon-details.tsx index f0dae881..3a5cd507 100644 --- a/web/src/components/icon-details.tsx +++ b/web/src/components/icon-details.tsx @@ -10,7 +10,7 @@ import { formatIconName } from "@/lib/utils" import type { AuthorData, Icon, IconFile } from "@/types/icons" import confetti from "canvas-confetti" import { motion } from "framer-motion" -import { Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun, ArrowRight } from "lucide-react" +import { ArrowRight, Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun } from "lucide-react" import dynamic from "next/dynamic" import Image from "next/image" import Link from "next/link" @@ -479,57 +479,63 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail - {iconData.categories && iconData.categories.length > 0 && (() => { - const MAX_RELATED_ICONS = 16 - const currentCategories = iconData.categories || [] + {iconData.categories && + iconData.categories.length > 0 && + (() => { + const MAX_RELATED_ICONS = 16 + const currentCategories = iconData.categories || [] - const relatedIconsWithScore = Object.entries(allIcons) - .map(([name, data]) => { - if (name === icon) return null // Exclude the current icon + const relatedIconsWithScore = Object.entries(allIcons) + .map(([name, data]) => { + if (name === icon) return null // Exclude the current icon - const otherCategories = data.categories || [] - const commonCategories = currentCategories.filter((cat) => otherCategories.includes(cat)) - const score = commonCategories.length + const otherCategories = data.categories || [] + const commonCategories = currentCategories.filter((cat) => otherCategories.includes(cat)) + const score = commonCategories.length - return score > 0 ? { name, data, score } : null - }) - .filter((item): item is { name: string; data: Icon; score: number } => item !== null) // Type guard - .sort((a, b) => b.score - a.score) // Sort by score DESC + return score > 0 ? { name, data, score } : null + }) + .filter((item): item is { name: string; data: Icon; score: number } => item !== null) // Type guard + .sort((a, b) => b.score - a.score) // Sort by score DESC - const topRelatedIcons = relatedIconsWithScore.slice(0, MAX_RELATED_ICONS) + const topRelatedIcons = relatedIconsWithScore.slice(0, MAX_RELATED_ICONS) - const viewMoreUrl = `/icons?${currentCategories.map((cat) => `category=${encodeURIComponent(cat)}`).join("&")}` + const viewMoreUrl = `/icons?${currentCategories.map((cat) => `category=${encodeURIComponent(cat)}`).join("&")}` - if (topRelatedIcons.length === 0) return null + if (topRelatedIcons.length === 0) return null - return ( -
- - - - - - - Other icons from {currentCategories.map((cat) => cat.replace(/-/g, " ")).join(", ")} categories - - - - - {relatedIconsWithScore.length > MAX_RELATED_ICONS && ( -
- -
- )} -
-
-
- ) - })()} + return ( +
+ + + + + + + Other icons from {currentCategories.map((cat) => cat.replace(/-/g, " ")).join(", ")} categories + + + + + {relatedIconsWithScore.length > MAX_RELATED_ICONS && ( +
+ +
+ )} +
+
+
+ ) + })()} ) } diff --git a/web/src/components/icon-search.tsx b/web/src/components/icon-search.tsx index 94c19bba..d95e5224 100644 --- a/web/src/components/icon-search.tsx +++ b/web/src/components/icon-search.tsx @@ -17,6 +17,7 @@ import { } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Separator } from "@/components/ui/separator" +import { type SortOption, filterAndSortIcons } from "@/lib/utils" import type { IconSearchProps } from "@/types/icons" import { ArrowDownAZ, ArrowUpZA, Calendar, Filter, Search, SortAsc, X } from "lucide-react" import { useTheme } from "next-themes" @@ -24,7 +25,6 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation" import posthog from "posthog-js" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { toast } from "sonner" -import { filterAndSortIcons, SortOption } from "@/lib/utils" export function IconSearch({ icons }: IconSearchProps) { const searchParams = useSearchParams() diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 52394f7a..2724465e 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -1,6 +1,6 @@ +import type { IconWithName } from "@/types/icons" import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" -import type { IconWithName } from "@/types/icons" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -173,41 +173,40 @@ export function filterAndSortIcons({ // Filter by categories if any are selected if (categories.length > 0) { filtered = filtered.filter(({ data }) => - data.categories.some((cat) => - categories.some((selectedCat) => cat.toLowerCase() === selectedCat.toLowerCase()), - ), + data.categories.some((cat) => categories.some((selectedCat) => cat.toLowerCase() === selectedCat.toLowerCase())), ) } if (query.trim()) { const queryWords = query.toLowerCase().split(/\s+/) - const scored = filtered.map((icon) => { - const nameScore = fuzzySearch(icon.name, query) * NAME_WEIGHT - const aliasScore = - icon.data.aliases && icon.data.aliases.length > 0 - ? Math.max(...icon.data.aliases.map((alias) => fuzzySearch(alias, query))) * ALIAS_WEIGHT - : 0 - const categoryScore = - icon.data.categories && icon.data.categories.length > 0 - ? Math.max(...icon.data.categories.map((category) => fuzzySearch(category, query))) * CATEGORY_WEIGHT - : 0 + const scored = filtered + .map((icon) => { + const nameScore = fuzzySearch(icon.name, query) * NAME_WEIGHT + const aliasScore = + icon.data.aliases && icon.data.aliases.length > 0 + ? Math.max(...icon.data.aliases.map((alias) => fuzzySearch(alias, query))) * ALIAS_WEIGHT + : 0 + const categoryScore = + icon.data.categories && icon.data.categories.length > 0 + ? Math.max(...icon.data.categories.map((category) => fuzzySearch(category, query))) * CATEGORY_WEIGHT + : 0 - const maxScore = Math.max(nameScore, aliasScore, categoryScore) + const maxScore = Math.max(nameScore, aliasScore, categoryScore) - // Penalize if only category matches - const onlyCategoryMatch = - categoryScore > 0.7 && nameScore < 0.5 && aliasScore < 0.5 - const finalScore = onlyCategoryMatch ? maxScore * CATEGORY_PENALTY : maxScore + // Penalize if only category matches + const onlyCategoryMatch = categoryScore > 0.7 && nameScore < 0.5 && aliasScore < 0.5 + const finalScore = onlyCategoryMatch ? maxScore * CATEGORY_PENALTY : maxScore - // Require all query words to be present in at least one field - const allWordsPresent = queryWords.every((word) => - icon.name.toLowerCase().includes(word) || - icon.data.aliases.some((alias) => alias.toLowerCase().includes(word)) || - icon.data.categories.some((cat) => cat.toLowerCase().includes(word)) - ) + // Require all query words to be present in at least one field + const allWordsPresent = queryWords.every( + (word) => + icon.name.toLowerCase().includes(word) || + icon.data.aliases.some((alias) => alias.toLowerCase().includes(word)) || + icon.data.categories.some((cat) => cat.toLowerCase().includes(word)), + ) - return { icon, score: allWordsPresent ? finalScore : finalScore * 0.4 } - }) + return { icon, score: allWordsPresent ? finalScore : finalScore * 0.4 } + }) .filter((item) => item.score > 0.7) .sort((a, b) => { if (b.score !== a.score) return b.score - a.score