Compare commits

..

2 Commits

Author SHA1 Message Date
Bjorn Lammers
4ada974942 fix(icons): trim empty spacing
Signed-off-by: Bjorn Lammers <bjorn@lammers.media>
2025-04-28 19:37:15 +02:00
Dashboard Icons Manager
cfc4b1f4ca feat(icons): add cup 2025-04-27 15:11:32 +00:00
30 changed files with 83 additions and 344 deletions

View File

@@ -1,14 +0,0 @@
{
"base": "svg",
"aliases": [],
"categories": [
"Development"
],
"update": {
"timestamp": "2025-04-27T15:11:19.986638",
"author": {
"id": 10742906,
"login": "zackad"
}
}
}

View File

@@ -1,12 +0,0 @@
{
"base": "png",
"aliases": [],
"categories": [],
"update": {
"timestamp": "2025-04-27T15:13:05.869706",
"author": {
"id": 39389502,
"login": "TeHtloTs"
}
}
}

View File

@@ -1,16 +0,0 @@
{
"base": "svg",
"aliases": [
"react-js"
],
"categories": [
"Development"
],
"update": {
"timestamp": "2025-04-26T23:03:30.306984",
"author": {
"id": 61716607,
"login": "lesolski"
}
}
}

View File

@@ -1,17 +0,0 @@
{
"base": "svg",
"aliases": [
"viber-app",
"viber-messaging"
],
"categories": [
"Communication"
],
"update": {
"timestamp": "2025-04-27T15:12:16.037327",
"author": {
"id": 61716607,
"login": "lesolski"
}
}
}

View File

@@ -1,18 +0,0 @@
{
"base": "svg",
"aliases": [
"zen"
],
"categories": [],
"update": {
"timestamp": "2025-04-28T17:40:35.244653",
"author": {
"id": 120695243,
"login": "theoneand33"
}
},
"colors": {
"light": "zen-browser",
"dark": "zen-browser-dark"
}
}

View File

@@ -198,20 +198,6 @@
}
}
},
"mailpit": {
"base": "svg",
"aliases": [],
"categories": [
"Development"
],
"update": {
"timestamp": "2025-04-27T15:11:19.986638",
"author": {
"id": 10742906,
"login": "zackad"
}
}
},
"phoneinfoga": {
"base": "svg",
"aliases": [],
@@ -3137,18 +3123,6 @@
"light": "libreddit-light"
}
},
"nzbgeek": {
"base": "png",
"aliases": [],
"categories": [],
"update": {
"timestamp": "2025-04-27T15:13:05.869706",
"author": {
"id": 39389502,
"login": "TeHtloTs"
}
}
},
"seelf": {
"base": "svg",
"aliases": [
@@ -3940,24 +3914,6 @@
}
}
},
"zen-browser": {
"base": "svg",
"aliases": [
"zen"
],
"categories": [],
"update": {
"timestamp": "2025-04-28T17:40:35.244653",
"author": {
"id": 120695243,
"login": "theoneand33"
}
},
"colors": {
"light": "zen-browser",
"dark": "zen-browser-dark"
}
},
"zipline": {
"base": "svg",
"aliases": [
@@ -11423,22 +11379,6 @@
}
}
},
"reactjs": {
"base": "svg",
"aliases": [
"react-js"
],
"categories": [
"Development"
],
"update": {
"timestamp": "2025-04-26T23:03:30.306984",
"author": {
"id": 61716607,
"login": "lesolski"
}
}
},
"hotio": {
"base": "svg",
"aliases": [],
@@ -19539,23 +19479,6 @@
}
}
},
"viber": {
"base": "svg",
"aliases": [
"viber-app",
"viber-messaging"
],
"categories": [
"Communication"
],
"update": {
"timestamp": "2025-04-27T15:12:16.037327",
"author": {
"id": 61716607,
"login": "lesolski"
}
}
},
"docker-amd": {
"base": "png",
"aliases": [],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="500" height="460" viewBox="0 0 132.292 121.708" version="1.1" id="svg6" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs10"/>
<path d="M12.321 0l53.861 53.918L120.365 0zM5.155 9.025l60.842 59.673 61.211-59.489-.185 36.835L66.921 70.54l15.164 12.616-8.137 5.986-41.609.184c-4.838-.022-25.877-18.34-27.185-41.255z" fill-opacity=".941" fill="#2d4a5f" id="path2" style="fill:#ffffff;fill-opacity:1"/>
<path d="M78.385 72.049l53.907-21.679-8.031 57.318-11.845-9.132c-21.727 23.171-45.255 26.289-67.997 20.837S12.281 98.39 5.155 83.8-.67 53.116 2.843 38.769c1.13 10.511-1.313 16.316 6.38 33.612 6.31 11.399 14.413 20.417 25.89 24.956 13.9 6.195 32.247 3.357 41.701-3.039l14.24-12.156z" fill="#00b786" id="path4"/>
</svg>

Before

Width:  |  Height:  |  Size: 828 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.23174 23 20.46348">
<title>React Logo</title>
<circle cx="0" cy="0" r="2.05" fill="#61dafb"/>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"/>
<ellipse rx="11" ry="4.2" transform="rotate(60)"/>
<ellipse rx="11" ry="4.2" transform="rotate(120)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 631.99 666.43"><defs><style>.cls-1{fill:#7360f2;}.cls-2{fill:none;stroke:#7360f2;stroke-linecap:round;stroke-linejoin:round;stroke-width:16.86px;}</style></defs><title>Artboard 6</title><path class="cls-1" d="M560.65,65C544.09,49.72,477.17,1.14,328.11.48c0,0-175.78-10.6-261.47,68C18.94,116.19,2.16,186,.39,272.55S-3.67,521.3,152.68,565.28l.15,0-.1,67.11s-1,27.17,16.89,32.71c21.64,6.72,34.34-13.93,55-36.19,11.34-12.22,27-30.17,38.8-43.89,106.93,9,189.17-11.57,198.51-14.61,21.59-7,143.76-22.66,163.63-184.84C646.07,218.4,615.64,112.66,560.65,65Zm18.12,308.58C562,509,462.91,517.51,444.64,523.37c-7.77,2.5-80,20.47-170.83,14.54,0,0-67.68,81.65-88.82,102.88-3.3,3.32-7.18,4.66-9.77,4-3.64-.89-4.64-5.2-4.6-11.5.06-9,.58-111.52.58-111.52s-.08,0,0,0C38.94,485.05,46.65,347,48.15,274.71S63.23,143.2,103.57,103.37c72.48-65.65,221.79-55.84,221.79-55.84,126.09.55,186.51,38.52,200.52,51.24C572.4,138.6,596.1,233.91,578.77,373.54Z"/><path class="cls-2" d="M389.47,268.77q-2.46-49.59-50.38-52.09"/><path class="cls-2" d="M432.72,283.27q1-46.2-27.37-77.2c-19-20.74-45.3-32.16-79.05-34.63"/><path class="cls-2" d="M477,300.59q-.61-80.17-47.91-126.28t-117.65-46.6"/><path class="cls-1" d="M340.76,381.68s11.85,1,18.23-6.86l12.44-15.65c6-7.76,20.48-12.71,34.66-4.81A366.67,366.67,0,0,1,437,374.1c9.41,6.92,28.68,23,28.74,23,9.18,7.75,11.3,19.13,5.05,31.13,0,.07-.05.19-.05.25a129.81,129.81,0,0,1-25.89,31.88c-.12.06-.12.12-.23.18q-13.38,11.18-26.29,12.71a17.39,17.39,0,0,1-3.84.24,35,35,0,0,1-11.18-1.72l-.28-.41c-13.26-3.74-35.4-13.1-72.27-33.44a430.39,430.39,0,0,1-60.72-40.11,318.31,318.31,0,0,1-27.31-24.22l-.92-.92-.92-.92h0l-.92-.92c-.31-.3-.61-.61-.92-.92a318.31,318.31,0,0,1-24.22-27.31,430.83,430.83,0,0,1-40.11-60.71c-20.34-36.88-29.7-59-33.44-72.28l-.41-.28a35,35,0,0,1-1.71-11.18,16.87,16.87,0,0,1,.23-3.84Q141,181.42,152.12,168c.06-.11.12-.11.18-.23a129.53,129.53,0,0,1,31.88-25.88c.06,0,.18-.06.25-.06,12-6.25,23.38-4.13,31.12,5,.06.06,16.11,19.33,23,28.74a366.67,366.67,0,0,1,19.74,30.94c7.9,14.17,2.95,28.68-4.81,34.66l-15.65,12.44c-7.9,6.38-6.86,18.23-6.86,18.23S254.15,359.57,340.76,381.68Z"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,27 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="82 82 860 860">
<g filter="url(#filter0_dd_637_5954)">
<rect x="82" y="82" width="860" height="860" rx="155.371" fill="#202020"/>
</g>
<circle cx="512.439" cy="512.439" r="113.02" stroke="#F2F0E3" stroke-width="25.1953"/>
<circle cx="512.439" cy="512.439" r="197.737" stroke="#F2F0E3" stroke-width="41.9922"/>
<circle cx="512.439" cy="512.439" r="298.266" stroke="#F2F0E3" stroke-width="58.7891"/>
<defs>
<filter id="filter0_dd_637_5954" x="74.4414" y="80.3203" width="875.117" height="878.477" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="0.839844" operator="dilate" in="SourceAlpha" result="effect1_dropShadow_637_5954"/>
<feOffset dy="5.87891"/>
<feGaussianBlur stdDeviation="3.35938"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_637_5954"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="10.0781"/>
<feGaussianBlur stdDeviation="3.35938"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_637_5954" result="effect2_dropShadow_637_5954"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_637_5954" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,27 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="82 82 860 860">
<g filter="url(#filter0_dd_637_6627)">
<rect x="82" y="82" width="860" height="860" rx="155.371" fill="#F2F0E3"/>
</g>
<circle cx="512.439" cy="512.439" r="113.02" stroke="#202020" stroke-width="25.1953"/>
<circle cx="512.439" cy="512.439" r="197.737" stroke="#202020" stroke-width="41.9922"/>
<circle cx="512.439" cy="512.439" r="298.266" stroke="#202020" stroke-width="58.7891"/>
<defs>
<filter id="filter0_dd_637_6627" x="74.4414" y="80.3203" width="875.117" height="878.477" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="0.839844" operator="dilate" in="SourceAlpha" result="effect1_dropShadow_637_6627"/>
<feOffset dy="5.87891"/>
<feGaussianBlur stdDeviation="3.35938"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_637_6627"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="10.0781"/>
<feGaussianBlur stdDeviation="3.35938"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_637_6627" result="effect2_dropShadow_637_6627"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_637_6627" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1066,7 +1066,6 @@
"mailgun.png",
"mailhog.png",
"mailjet.png",
"mailpit.png",
"mailu.png",
"mainsail.png",
"maintainerr.png",
@@ -1308,7 +1307,6 @@
"nvidia.png",
"nxfilter.png",
"nxlog.png",
"nzbgeek.png",
"nzbget.png",
"nzbhydra.png",
"nzbhydra2-light.png",
@@ -1644,7 +1642,6 @@
"rdt-client.png",
"reactive-resume-light.png",
"reactive-resume.png",
"reactjs.png",
"readarr.png",
"readeck.png",
"readthedocs-light.png",
@@ -2066,7 +2063,6 @@
"vercel.png",
"verizon.png",
"vi.png",
"viber.png",
"victoriametrics-light.png",
"victoriametrics.png",
"vidzy.png",
@@ -2224,8 +2220,6 @@
"zabbix.png",
"zabka.png",
"zammad.png",
"zen-browser-dark.png",
"zen-browser.png",
"zenarmor.png",
"zendesk.png",
"zerotier-light.png",
@@ -3119,7 +3113,6 @@
"mailfence.svg",
"mailgun.svg",
"mailjet.svg",
"mailpit.svg",
"mainsail.svg",
"maintainerr.svg",
"manjaro-linux.svg",
@@ -3594,7 +3587,6 @@
"rdt-client.svg",
"reactive-resume-light.svg",
"reactive-resume.svg",
"reactjs.svg",
"readarr.svg",
"readeck.svg",
"readthedocs-light.svg",
@@ -3911,7 +3903,6 @@
"vercel.svg",
"verizon.svg",
"vi.svg",
"viber.svg",
"victoriametrics-light.svg",
"victoriametrics.svg",
"vidzy.svg",
@@ -4031,8 +4022,6 @@
"zabbix.svg",
"zabka.svg",
"zammad.svg",
"zen-browser-dark.svg",
"zen-browser.svg",
"zenarmor.svg",
"zendesk.svg",
"zerotier-light.svg",
@@ -5121,7 +5110,6 @@
"mailgun.webp",
"mailhog.webp",
"mailjet.webp",
"mailpit.webp",
"mailu.webp",
"mainsail.webp",
"maintainerr.webp",
@@ -5363,7 +5351,6 @@
"nvidia.webp",
"nxfilter.webp",
"nxlog.webp",
"nzbgeek.webp",
"nzbget.webp",
"nzbhydra.webp",
"nzbhydra2-light.webp",
@@ -5699,7 +5686,6 @@
"rdt-client.webp",
"reactive-resume-light.webp",
"reactive-resume.webp",
"reactjs.webp",
"readarr.webp",
"readeck.webp",
"readthedocs-light.webp",
@@ -6121,7 +6107,6 @@
"vercel.webp",
"verizon.webp",
"vi.webp",
"viber.webp",
"victoriametrics-light.webp",
"victoriametrics.webp",
"vidzy.webp",
@@ -6279,8 +6264,6 @@
"zabbix.webp",
"zabka.webp",
"zammad.webp",
"zen-browser-dark.webp",
"zen-browser.webp",
"zenarmor.webp",
"zendesk.webp",
"zerotier-light.webp",

View File

@@ -85,6 +85,7 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
description: `Download the ${formattedIconName} icon in SVG, PNG, and WEBP formats for FREE. Part of a collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`,
},
alternates: {
canonical: pageUrl,
media: {
png: `${BASE_URL}/png/${icon}.png`,
svg: `${BASE_URL}/svg/${icon}.svg`,

View File

@@ -7,7 +7,7 @@ import { Suspense, useEffect } from "react"
export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (process.env.NEXT_PUBLIC_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",

View File

@@ -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 { filterAndSortIcons, formatIconName, fuzzySearch } from "@/lib/utils"
import type { IconWithName } from "@/types/icons"
import { Info, Search as SearchIcon, Tag } from "lucide-react"
import { formatIconName, fuzzySearch, filterAndSortIcons } from "@/lib/utils"
import { useRouter } from "next/navigation"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useCallback, useEffect, useState, useMemo } from "react"
import type { IconWithName } from "@/types/icons"
import { Tag, Search as SearchIcon, Info } from "lucide-react"
import { Badge } from "@/components/ui/badge"
interface CommandMenuProps {
icons: IconWithName[]
@@ -37,7 +37,10 @@ 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
@@ -67,7 +70,11 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
}
return (
<CommandDialog open={isOpen} onOpenChange={setIsOpen} contentClassName="bg-background/90 backdrop-blur-sm border border-border/60">
<CommandDialog
open={isOpen}
onOpenChange={setIsOpen}
contentClassName="bg-background/90 backdrop-blur-sm border border-border/60"
>
<CommandInput
placeholder={`Search our collection of ${totalIcons} icons by name or category...`}
value={query}
@@ -76,7 +83,7 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
<CommandList className="max-h-[300px]">
{/* Icon Results */}
<CommandGroup heading="Icons">
{filteredIcons.length > 0 &&
{filteredIcons.length > 0 && (
filteredIcons.map(({ name, data }) => {
const formatedIconName = formatIconName(name)
const hasCategories = data.categories && data.categories.length > 0
@@ -90,9 +97,7 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
>
<div className="flex-shrink-0 h-5 w-5 relative">
<div className="h-full w-full bg-primary/10 dark:bg-primary/20 rounded-md flex items-center justify-center">
<span className="text-[9px] font-medium text-primary dark:text-primary-foreground">
{name.substring(0, 2).toUpperCase()}
</span>
<span className="text-[9px] font-medium text-primary dark:text-primary-foreground">{name.substring(0, 2).toUpperCase()}</span>
</div>
</div>
<span className="flex-grow capitalize font-medium text-sm">{formatedIconName}</span>
@@ -105,7 +110,9 @@ 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"
>
<Tag size={8} className="mr-1 flex-shrink-0" />
<span className="truncate">{data.categories[0].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}</span>
<span className="truncate">
{data.categories[0].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}
</span>
</Badge>
{/* "+N" badge if more than one category */}
{data.categories.length > 1 && (
@@ -117,7 +124,8 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
)}
</CommandItem>
)
})}
})
)}
</CommandGroup>
<CommandEmpty>
{/* Minimal empty state */}
@@ -130,10 +138,12 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
{/* Separator and Browse section - Styled div outside CommandList */}
<div className="border-t border-border/40 pt-1 mt-1 px-1 pb-1">
<button
type="button"
className="flex items-center gap-2 cursor-pointer rounded-sm px-2 py-1 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground w-full"
<div
role="button"
tabIndex={0}
className="flex items-center gap-2 cursor-pointer rounded-sm px-2 py-1 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground"
onClick={handleBrowseAll}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleBrowseAll() }}
>
<div className="flex-shrink-0 h-5 w-5 relative">
<div className="h-full w-full bg-primary/80 dark:bg-primary/40 rounded-md flex items-center justify-center">
@@ -141,7 +151,7 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
</div>
</div>
<span className="flex-grow text-sm">Browse all icons {totalIcons} available</span>
</button>
</div>
</div>
</CommandDialog>
)

View File

@@ -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 { ArrowRight, Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun } from "lucide-react"
import { 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,63 +479,31 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
</Card>
</div>
</div>
{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 (
<section className="container mx-auto mt-12" aria-labelledby="related-icons-title">
<Card className="bg-background/50 border shadow-lg">
<CardHeader>
<CardTitle>
<h2 id="related-icons-title">Related Icons</h2>
</CardTitle>
<CardDescription>
Other icons from {currentCategories.map((cat) => cat.replace(/-/g, " ")).join(", ")} categories
</CardDescription>
</CardHeader>
<CardContent>
<IconsGrid filteredIcons={topRelatedIcons} matchedAliases={{}} />
{relatedIconsWithScore.length > MAX_RELATED_ICONS && (
<div className="mt-6 text-center">
<Button
asChild
variant="link"
className="text-muted-foreground hover:text-primary transition-colors duration-200 hover:no-underline"
>
<Link href={viewMoreUrl} className="no-underline">
View all related icons
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
)}
</CardContent>
</Card>
</section>
)
})()}
{iconData.categories && iconData.categories.length > 0 && (
<section className="container mx-auto mt-12" aria-labelledby="related-icons-title">
<Card className="bg-background/50 border shadow-lg">
<CardHeader>
<CardTitle>
<h2 id="related-icons-title">Related Icons</h2>
</CardTitle>
<CardDescription>
Other icons from {iconData.categories.map((cat) => cat.replace(/-/g, " ")).join(", ")} categories
</CardDescription>
</CardHeader>
<CardContent>
<IconsGrid
filteredIcons={Object.entries(allIcons)
.filter(([name, data]) => {
if (name === icon) return false
return data.categories?.some((cat) => iconData.categories?.includes(cat))
})
.map(([name, data]) => ({ name, data }))}
matchedAliases={{}}
/>
</CardContent>
</Card>
</section>
)}
</main>
)
}

View File

@@ -17,7 +17,6 @@ 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"
@@ -25,6 +24,7 @@ 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()

View File

@@ -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,40 +173,41 @@ 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB