Compare commits

...

11 Commits

Author SHA1 Message Date
Dashboard Icons Manager
09531d11d5 feat(icons): add uptimerobot 2025-04-28 18:02:52 +00:00
dashboard-icons-manager[bot]
d0f45e6bbb feat(icons): add cup (#1316)
* feat(icons): add cup

* fix(icons): trim empty spacing

Signed-off-by: Bjorn Lammers <bjorn@lammers.media>

---------

Signed-off-by: Bjorn Lammers <bjorn@lammers.media>
Co-authored-by: Dashboard Icons Manager <193821040+dashboard-icons-manager[bot]@users.noreply.github.com>
Co-authored-by: Bjorn Lammers <bjorn@lammers.media>
2025-04-28 19:37:31 +02:00
dashboard-icons-manager[bot]
4138e10265 feat(icons): add nzbgeek (#1318)
* feat(icons): add nzbgeek

* fix(icons): trim empty spacing

Signed-off-by: Bjorn Lammers <bjorn@lammers.media>

---------

Signed-off-by: Bjorn Lammers <bjorn@lammers.media>
Co-authored-by: Dashboard Icons Manager <193821040+dashboard-icons-manager[bot]@users.noreply.github.com>
Co-authored-by: Bjorn Lammers <bjorn@lammers.media>
2025-04-28 19:34:56 +02:00
Thomas Camlong
321e969f6c Merge pull request #1320 from homarr-labs/feat/related-refine 2025-04-28 16:21:21 +02:00
Thomas Camlong
ea9b96ad6d fix(icons): Remove non-existing alternate URL (#1324) 2025-04-28 16:19:23 +02:00
Thomas Camlong
59ad9344b7 feat: make DISABLE_POSTHOG public (#1323)
For client side availabilty
2025-04-28 15:46:42 +02:00
Bjorn Lammers
50c3a92b29 fix(web): Run Biome checks and apply fixes 2025-04-27 22:59:33 +02:00
Bjorn Lammers
575dee0580 feat(icons/[id]): Refine related icons relevance, display limits, and styling 2025-04-27 22:57:56 +02:00
dashboard-icons-manager[bot]
23462d2980 feat(icons): add reactjs (#1300)
Co-authored-by: Dashboard Icons Manager <193821040+dashboard-icons-manager[bot]@users.noreply.github.com>
2025-04-27 17:29:48 +02:00
dashboard-icons-manager[bot]
832a4b76ae feat(icons): add viber (#1317)
Co-authored-by: Dashboard Icons Manager <193821040+dashboard-icons-manager[bot]@users.noreply.github.com>
2025-04-27 17:12:48 +02:00
dashboard-icons-manager[bot]
267b6d4400 feat(icons): add mailpit (#1315)
Co-authored-by: Dashboard Icons Manager <193821040+dashboard-icons-manager[bot]@users.noreply.github.com>
2025-04-27 17:12:30 +02:00
33 changed files with 340 additions and 83 deletions

12
meta/cup.json Normal file
View File

@@ -0,0 +1,12 @@
{
"base": "svg",
"aliases": [],
"categories": [],
"update": {
"timestamp": "2025-04-27T15:11:25.174121",
"author": {
"id": 77530549,
"login": "sergi0g"
}
}
}

14
meta/mailpit.json Normal file
View File

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

12
meta/nzbgeek.json Normal file
View File

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

16
meta/reactjs.json Normal file
View File

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

16
meta/uptimerobot.json Normal file
View File

@@ -0,0 +1,16 @@
{
"base": "svg",
"aliases": [
"uptime-robot"
],
"categories": [
"Cloud"
],
"update": {
"timestamp": "2025-04-28T18:02:44.073299",
"author": {
"id": 69894187,
"login": "bunnypranav"
}
}
}

17
meta/viber.json Normal file
View File

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

View File

@@ -198,6 +198,20 @@
}
}
},
"mailpit": {
"base": "svg",
"aliases": [],
"categories": [
"Development"
],
"update": {
"timestamp": "2025-04-27T15:11:19.986638",
"author": {
"id": 10742906,
"login": "zackad"
}
}
},
"phoneinfoga": {
"base": "svg",
"aliases": [],
@@ -3123,6 +3137,18 @@
"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": [
@@ -11031,6 +11057,18 @@
}
}
},
"cup": {
"base": "svg",
"aliases": [],
"categories": [],
"update": {
"timestamp": "2025-04-27T15:11:25.174121",
"author": {
"id": 77530549,
"login": "sergi0g"
}
}
},
"private-internet-access": {
"base": "svg",
"aliases": [
@@ -11367,6 +11405,22 @@
}
}
},
"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": [],
@@ -17528,6 +17582,22 @@
}
}
},
"uptimerobot": {
"base": "svg",
"aliases": [
"uptime-robot"
],
"categories": [
"Cloud"
],
"update": {
"timestamp": "2025-04-28T18:02:44.073299",
"author": {
"id": 69894187,
"login": "bunnypranav"
}
}
},
"infoblox": {
"base": "svg",
"aliases": [],
@@ -19467,6 +19537,23 @@
}
}
},
"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": [],

BIN
png/cup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
png/mailpit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
png/nzbgeek.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
png/reactjs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
png/uptimerobot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
png/viber.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

29
svg/cup.svg Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<path style="fill:#A6CFD6;" d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
C97.82,30.66,94.2,18.43,65.12,17.55z"/>
<path style="fill:#DCEDF6;" d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"/>
<path style="fill:#6CA4AE;" d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"/>
<path style="fill:#DC0D27;" d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
C110.47,3.47,86.08,11.74,84.85,13.1z"/>
<path style="fill:#8A1F0F;" d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"/>
<g>
<path style="fill:#8A1F0F;" d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"/>
</g>
<g>
<path style="fill:#8A1F0F;" d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"/>
</g>
<g>
<path style="fill:#8A1F0F;" d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

6
svg/mailpit.svg Normal file
View File

@@ -0,0 +1,6 @@
<?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>

After

Width:  |  Height:  |  Size: 828 B

9
svg/reactjs.svg Normal file
View File

@@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 366 B

1
svg/uptimerobot.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="298" height="298"><g fill="#3BD771" transform="translate(.9 .9)"><circle cx="148.1" cy="148.1" r="148.1" opacity=".3"/><circle cx="148.1" cy="148.1" r="98.9"/></g></svg>

After

Width:  |  Height:  |  Size: 216 B

1
svg/viber.svg Normal file
View File

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -357,6 +357,7 @@
"css-light.png",
"css.png",
"ctfreak.png",
"cup.png",
"cups-light.png",
"cups.png",
"cura.png",
@@ -1065,6 +1066,7 @@
"mailgun.png",
"mailhog.png",
"mailjet.png",
"mailpit.png",
"mailu.png",
"mainsail.png",
"maintainerr.png",
@@ -1306,6 +1308,7 @@
"nvidia.png",
"nxfilter.png",
"nxlog.png",
"nzbgeek.png",
"nzbget.png",
"nzbhydra.png",
"nzbhydra2-light.png",
@@ -1641,6 +1644,7 @@
"rdt-client.png",
"reactive-resume-light.png",
"reactive-resume.png",
"reactjs.png",
"readarr.png",
"readeck.png",
"readthedocs-light.png",
@@ -2047,6 +2051,7 @@
"ups.png",
"upsnap.png",
"uptime-kuma.png",
"uptimerobot.png",
"upvote-rss.png",
"urbackup-server.png",
"urbackup.png",
@@ -2062,6 +2067,7 @@
"vercel.png",
"verizon.png",
"vi.png",
"viber.png",
"victoriametrics-light.png",
"victoriametrics.png",
"vidzy.png",
@@ -2519,6 +2525,7 @@
"css-light.svg",
"css.svg",
"ctfreak.svg",
"cup.svg",
"cups-light.svg",
"cups.svg",
"cura.svg",
@@ -3111,6 +3118,7 @@
"mailfence.svg",
"mailgun.svg",
"mailjet.svg",
"mailpit.svg",
"mainsail.svg",
"maintainerr.svg",
"manjaro-linux.svg",
@@ -3585,6 +3593,7 @@
"rdt-client.svg",
"reactive-resume-light.svg",
"reactive-resume.svg",
"reactjs.svg",
"readarr.svg",
"readeck.svg",
"readthedocs-light.svg",
@@ -3888,6 +3897,7 @@
"ups.svg",
"upsnap.svg",
"uptime-kuma.svg",
"uptimerobot.svg",
"upvote-rss.svg",
"valetudo.svg",
"valkey.svg",
@@ -3901,6 +3911,7 @@
"vercel.svg",
"verizon.svg",
"vi.svg",
"viber.svg",
"victoriametrics-light.svg",
"victoriametrics.svg",
"vidzy.svg",
@@ -4399,6 +4410,7 @@
"css-light.webp",
"css.webp",
"ctfreak.webp",
"cup.webp",
"cups-light.webp",
"cups.webp",
"cura.webp",
@@ -5107,6 +5119,7 @@
"mailgun.webp",
"mailhog.webp",
"mailjet.webp",
"mailpit.webp",
"mailu.webp",
"mainsail.webp",
"maintainerr.webp",
@@ -5348,6 +5361,7 @@
"nvidia.webp",
"nxfilter.webp",
"nxlog.webp",
"nzbgeek.webp",
"nzbget.webp",
"nzbhydra.webp",
"nzbhydra2-light.webp",
@@ -5683,6 +5697,7 @@
"rdt-client.webp",
"reactive-resume-light.webp",
"reactive-resume.webp",
"reactjs.webp",
"readarr.webp",
"readeck.webp",
"readthedocs-light.webp",
@@ -6089,6 +6104,7 @@
"ups.webp",
"upsnap.webp",
"uptime-kuma.webp",
"uptimerobot.webp",
"upvote-rss.webp",
"urbackup-server.webp",
"urbackup.webp",
@@ -6104,6 +6120,7 @@
"vercel.webp",
"verizon.webp",
"vi.webp",
"viber.webp",
"victoriametrics-light.webp",
"victoriametrics.webp",
"vidzy.webp",

View File

@@ -85,7 +85,6 @@ 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.DISABLE_POSTHOG === "true") return
if (process.env.NEXT_PUBLIC_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 { 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 (
<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}
@@ -83,7 +76,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
@@ -97,7 +90,9 @@ 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>
@@ -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"
>
<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 && (
@@ -124,8 +117,7 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
)}
</CommandItem>
)
})
)}
})}
</CommandGroup>
<CommandEmpty>
{/* Minimal empty state */}
@@ -138,12 +130,10 @@ 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">
<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"
<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"
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">
@@ -151,7 +141,7 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
</div>
</div>
<span className="flex-grow text-sm">Browse all icons {totalIcons} available</span>
</div>
</button>
</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 { Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun } 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,31 +479,63 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
</Card>
</div>
</div>
{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>
)}
{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>
)
})()}
</main>
)
}

View File

@@ -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()

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,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

BIN
webp/cup.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
webp/mailpit.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
webp/nzbgeek.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
webp/reactjs.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
webp/uptimerobot.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
webp/viber.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB