From d0f8f8ced934d6b5f067369ee57c165437815350 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sat, 26 Apr 2025 13:07:02 +0200 Subject: [PATCH] revert: revert changes --- web/package.json | 1 + web/pnpm-lock.yaml | 21 ++ web/src/app/icons/[icon]/page.tsx | 16 +- web/src/app/icons/opengraph-image.tsx | 22 +- web/src/app/icons/page.tsx | 10 - web/src/app/layout.tsx | 3 +- web/src/components/command-menu.tsx | 138 ++++++++++ web/src/components/header.tsx | 66 ++++- web/src/components/hero.tsx | 58 +--- web/src/components/icon-card.tsx | 61 +---- web/src/components/icon-details.tsx | 284 +++++++------------- web/src/components/icon-grid.tsx | 2 +- web/src/components/icon-search.tsx | 152 +++-------- web/src/components/recently-added-icons.tsx | 84 ++---- web/src/components/ui/command.tsx | 177 ++++++++++++ web/src/hooks/use-media-query.ts | 25 ++ web/src/lib/utils.ts | 4 + 17 files changed, 624 insertions(+), 500 deletions(-) create mode 100644 web/src/components/command-menu.tsx create mode 100644 web/src/components/ui/command.tsx create mode 100644 web/src/hooks/use-media-query.ts diff --git a/web/package.json b/web/package.json index 0a3e2367..0c33be2d 100644 --- a/web/package.json +++ b/web/package.json @@ -42,6 +42,7 @@ "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.7.3", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 9d90ed44..94060bfc 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -1549,6 +1552,12 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -3424,6 +3433,18 @@ snapshots: clsx@2.1.1: {} + cmdk@1.1.1(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-dialog': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + color-convert@2.0.1: dependencies: color-name: 1.1.4 diff --git a/web/src/app/icons/[icon]/page.tsx b/web/src/app/icons/[icon]/page.tsx index e143f5ff..eab2ce81 100644 --- a/web/src/app/icons/[icon]/page.tsx +++ b/web/src/app/icons/[icon]/page.tsx @@ -1,13 +1,22 @@ import { IconDetails } from "@/components/icon-details" import { BASE_URL, WEB_URL } from "@/constants" import { getAllIcons, getAuthorData } from "@/lib/api" +import { formatIconName } from "@/lib/utils" import type { Metadata, ResolvingMetadata } from "next" +import { default as dynamicImport } from "next/dynamic" import { notFound } from "next/navigation" - export const dynamicParams = false export async function generateStaticParams() { const iconsData = await getAllIcons() + if (process.env.CI_MODE === "false") { + // This is meant to speed up the build process in local development + return Object.keys(iconsData) + .slice(0, 5) + .map((icon) => ({ + icon, + })) + } return Object.keys(iconsData).map((icon) => ({ icon, })) @@ -33,7 +42,6 @@ export async function generateMetadata({ params, searchParams }: Props, parent: console.debug(`Generated metadata for ${icon} by ${authorName} (${authorData.html_url}) updated at ${updateDate.toLocaleString()}`) - const iconPreviewImageUrl = `${BASE_URL}/webp/${icon}.webp` const pageUrl = `${WEB_URL}/icons/${icon}` const formattedIconName = icon .split("-") @@ -61,7 +69,7 @@ export async function generateMetadata({ params, searchParams }: Props, parent: "app directory", ], icons: { - icon: iconPreviewImageUrl, + icon: `${BASE_URL}/webp/${icon}.webp`, }, abstract: `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.`, openGraph: { @@ -74,13 +82,11 @@ export async function generateMetadata({ params, searchParams }: Props, parent: modifiedTime: updateDate.toISOString(), section: "Icons", tags: [formattedIconName, "dashboard icon", "service icon", "application icon", "tool icon", "web dashboard", "app directory"], - images: [iconPreviewImageUrl], }, twitter: { card: "summary_large_image", title: `${formattedIconName} Icon | Dashboard Icons`, 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.`, - images: [iconPreviewImageUrl], }, alternates: { canonical: pageUrl, diff --git a/web/src/app/icons/opengraph-image.tsx b/web/src/app/icons/opengraph-image.tsx index 71462c2d..5513c80b 100644 --- a/web/src/app/icons/opengraph-image.tsx +++ b/web/src/app/icons/opengraph-image.tsx @@ -10,28 +10,28 @@ export const size = { // Define a fixed list of representative icons const representativeIcons = [ - "github", - "discord", - "slack", - "docker", - "kubernetes", - "grafana", - "prometheus", - "nextcloud", - "homeassistant", + "homarr", + "sonarr", + "radarr", + "lidarr", + "readarr", + "prowlarr", + "qbittorrent", + "home-assistant", "cloudflare", - "nginx", + "github", "traefik", "portainer", "plex", "jellyfin", + "overseerr", ] export default async function Image() { const iconsData = await getAllIcons() const totalIcons = Object.keys(iconsData).length // Round down to the nearest 100 - const roundedTotalIcons = Math.floor(totalIcons / 100) * 100 + const roundedTotalIcons = Math.round(totalIcons / 100) * 100 const iconImages = representativeIcons.map((icon) => ({ name: icon diff --git a/web/src/app/icons/page.tsx b/web/src/app/icons/page.tsx index 0b12ce33..78669d9a 100644 --- a/web/src/app/icons/page.tsx +++ b/web/src/app/icons/page.tsx @@ -25,21 +25,11 @@ export async function generateMetadata(): Promise { description: `Search and browse through our collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`, type: "website", url: `${BASE_URL}/icons`, - images: [ - { - url: "/og-image.png", - width: 1200, - height: 630, - alt: "Browse Dashboard Icons Collection", - type: "image/png", - }, - ], }, twitter: { card: "summary_large_image", title: "Browse Icons | Free Dashboard Icons", description: `Search and browse through our collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`, - images: ["/og-image-browse.png"], }, alternates: { canonical: `${BASE_URL}/icons`, diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index b792f1d1..55c212e4 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -2,12 +2,12 @@ import { PostHogProvider } from "@/components/PostHogProvider" import { Footer } from "@/components/footer" import { HeaderWrapper } from "@/components/header-wrapper" import { LicenseNotice } from "@/components/license-notice" +import { BASE_URL, WEB_URL, getDescription, websiteTitle } from "@/constants" import { getTotalIcons } from "@/lib/api" import type { Metadata, Viewport } from "next" import { Inter } from "next/font/google" import { Toaster } from "sonner" import "./globals.css" -import { BASE_URL, WEB_URL, getDescription, websiteTitle } from "@/constants" import { ThemeProvider } from "./theme-provider" const inter = Inter({ @@ -82,6 +82,7 @@ export async function generateMetadata(): Promise { } export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( diff --git a/web/src/components/command-menu.tsx b/web/src/components/command-menu.tsx new file mode 100644 index 00000000..dc955062 --- /dev/null +++ b/web/src/components/command-menu.tsx @@ -0,0 +1,138 @@ +"use client" + +import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command" +import { useMediaQuery } from "@/hooks/use-media-query" +import { formatIconName, fuzzySearch } from "@/lib/utils" +import { useRouter } from "next/navigation" +import { useCallback, useEffect, useState } from "react" + +interface CommandMenuProps { + icons: { + name: string + data: { + categories: string[] + aliases: string[] + [key: string]: unknown + } + }[] + triggerButtonId?: string + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalOnOpenChange }: CommandMenuProps) { + const router = useRouter() + const [internalOpen, setInternalOpen] = useState(false) + const [query, setQuery] = useState("") + const isDesktop = useMediaQuery("(min-width: 768px)") + + // Use either external or internal state for controlling open state + const isOpen = externalOpen !== undefined ? externalOpen : internalOpen + + // Wrap setIsOpen in useCallback to fix dependency issue + const setIsOpen = useCallback( + (value: boolean) => { + if (externalOnOpenChange) { + externalOnOpenChange(value) + } else { + setInternalOpen(value) + } + }, + [externalOnOpenChange], + ) + + const filteredIcons = getFilteredIcons(icons, query) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ( + (e.key === "k" && (e.metaKey || e.ctrlKey)) || + (e.key === "/" && document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA") + ) { + e.preventDefault() + setIsOpen(!isOpen) + } + } + + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, [isOpen, setIsOpen]) + + function getFilteredIcons(iconList: CommandMenuProps["icons"], query: string) { + if (!query) { + // Return a limited number of icons when no query is provided + return iconList.slice(0, 8) + } + + // Calculate scores for each icon + const scoredIcons = iconList.map((icon) => { + // Calculate scores for different fields + const nameScore = fuzzySearch(icon.name, query) * 2.0 // Give more weight to name matches + + // Get max score from aliases + const aliasScore = + icon.data.aliases && icon.data.aliases.length > 0 + ? Math.max(...icon.data.aliases.map((alias) => fuzzySearch(alias, query))) * 1.8 // Increased weight for aliases + : 0 + + // Get max score from categories + const categoryScore = + icon.data.categories && icon.data.categories.length > 0 + ? Math.max(...icon.data.categories.map((category) => fuzzySearch(category, query))) + : 0 + + // Use the highest score + const score = Math.max(nameScore, aliasScore, categoryScore) + + return { icon, score, matchedField: score === nameScore ? "name" : score === aliasScore ? "alias" : "category" } + }) + + // Filter icons with a minimum score and sort by highest score + return scoredIcons + .filter((item) => item.score > 0.3) // Higher threshold for more accurate results + .sort((a, b) => b.score - a.score) + .slice(0, 20) // Limit the number of results + .map((item) => item.icon) + } + + const handleSelect = (name: string) => { + setIsOpen(false) + router.push(`/icons/${name}`) + } + + return ( + + + + No matching icons found. Try a different search term or browse all icons. + + {filteredIcons.map(({ name, data }) => { + // Find matched alias for display if available + const matchedAlias = + query && data.aliases && data.aliases.length > 0 + ? data.aliases.find((alias) => alias.toLowerCase().includes(query.toLowerCase())) + : null + const formatedIconName = formatIconName(name) + + return ( + handleSelect(name)} className="flex items-center gap-2 cursor-pointer"> +
+
+ {name.substring(0, 2).toUpperCase()} +
+
+ {formatedIconName} + {matchedAlias && alias: {matchedAlias}} + {!matchedAlias && data.categories && data.categories.length > 0 && ( + + {data.categories[0].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} + + )} +
+ ) + })} +
+
+
+ ) +} diff --git a/web/src/components/header.tsx b/web/src/components/header.tsx index 2551d556..b431b564 100644 --- a/web/src/components/header.tsx +++ b/web/src/components/header.tsx @@ -3,16 +3,45 @@ import { IconSubmissionForm } from "@/components/icon-submission-form" import { ThemeSwitcher } from "@/components/theme-switcher" import { REPO_PATH } from "@/constants" -import { motion } from "framer-motion" -import { Github } from "lucide-react" +import { getIconsArray } from "@/lib/api" +import type { IconWithName } from "@/types/icons" +import { Github, Search } from "lucide-react" import Link from "next/link" +import { useEffect, useState } from "react" +import { CommandMenu } from "./command-menu" import { HeaderNav } from "./header-nav" import { Button } from "./ui/button" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip" export function Header() { + const [iconsData, setIconsData] = useState([]) + const [isLoaded, setIsLoaded] = useState(false) + const [commandMenuOpen, setCommandMenuOpen] = useState(false) + + useEffect(() => { + async function loadIcons() { + try { + const icons = await getIconsArray() + setIconsData(icons) + setIsLoaded(true) + } catch (error) { + console.error("Failed to load icons:", error) + setIsLoaded(true) + } + } + + loadIcons() + }, []) + + // Function to open the command menu + const openCommandMenu = () => { + setCommandMenuOpen(true) + } + return ( -
+
@@ -23,6 +52,30 @@ export function Header() {
+ {/* Desktop search button */} +
+ +
+ + {/* Mobile search button */} +
+ +
+
@@ -49,6 +102,9 @@ export function Header() {
-
+ + {/* Single instance of CommandMenu */} + {isLoaded && } + ) -} +} \ No newline at end of file diff --git a/web/src/components/hero.tsx b/web/src/components/hero.tsx index babc6434..97e1cd17 100644 --- a/web/src/components/hero.tsx +++ b/web/src/components/hero.tsx @@ -205,61 +205,13 @@ export function HeroSection({ totalIcons, stars }: { totalIcons: number; stars: /> -
+

Your definitive source for - - - - - +
- - - - - + dashboard icons

@@ -272,7 +224,7 @@ export function HeroSection({ totalIcons, stars }: { totalIcons: number; stars:
- Browse icons + Explore icons @@ -526,7 +478,7 @@ function SearchInput({ searchQuery, setSearchQuery, totalIcons }: SearchInputPro name="q" autoFocus type="search" - placeholder={`Search our collection of ${totalIcons} icons by name or category...`} + placeholder={`Find any of ${totalIcons} icons by name or category...`} className="pl-10 h-10 md:h-12 rounded-lg w-full border-border focus:border-primary/20 text-sm md:text-base" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} diff --git a/web/src/components/icon-card.tsx b/web/src/components/icon-card.tsx index 49372f26..7ce8cd7b 100644 --- a/web/src/components/icon-card.tsx +++ b/web/src/components/icon-card.tsx @@ -1,11 +1,10 @@ import { MagicCard } from "@/components/magicui/magic-card" import { BASE_URL } from "@/constants" +import { formatIconName } from "@/lib/utils" import type { Icon } from "@/types/icons" import Image from "next/image" import Link from "next/link" -import { useState } from "react" -import { AlertTriangle } from "lucide-react" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { preload } from "react-dom" export function IconCard({ name, @@ -16,58 +15,20 @@ export function IconCard({ data: Icon matchedAlias?: string }) { - const [isLoading, setIsLoading] = useState(true) - const [hasError, setHasError] = useState(false) - - // Construct URLs for both WebP and the original format - const webpSrc = `${BASE_URL}/webp/${name}.webp` - const originalSrc = `${BASE_URL}/${iconData.base}/${name}.${iconData.base}` - - const handleLoadingComplete = () => { - setIsLoading(false) - setHasError(false) - } - - const handleError = () => { - setIsLoading(false) - setHasError(true) - } - + const formatedIconName = formatIconName(name) return ( -
- {isLoading && !hasError && ( -
- )} - {hasError ? ( - - - - - - -

Image failed to load, likely due to size limits. Please raise an issue on GitHub.

-
-
-
- ) : ( - - - - {`${name} - - )} +
+ {`${name}
- {name.replace(/-/g, " ")} + {formatedIconName} {matchedAlias && Alias: {matchedAlias}} diff --git a/web/src/components/icon-details.tsx b/web/src/components/icon-details.tsx index f1cdb37f..253c0b4d 100644 --- a/web/src/components/icon-details.tsx +++ b/web/src/components/icon-details.tsx @@ -9,7 +9,8 @@ import { BASE_URL, REPO_PATH } from "@/constants" 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, AlertTriangle } 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" import { useCallback, useState } from "react" @@ -17,6 +18,7 @@ import { toast } from "sonner" import { Carbon } from "./carbon" import { MagicCard } from "./magicui/magic-card" import { Badge } from "./ui/badge" +import { formatIconName } from "@/lib/utils" export type IconDetailsProps = { icon: string @@ -26,10 +28,6 @@ export type IconDetailsProps = { } export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetailsProps) { - // Add state for the main preview icon - const [isPreviewLoading, setIsPreviewLoading] = useState(true) - const [hasPreviewError, setHasPreviewError] = useState(false) - const authorName = authorData.name || authorData.login || "" const iconColorVariants = iconData.colors const formattedDate = new Date(iconData.update.timestamp).toLocaleDateString("en-GB", { @@ -146,44 +144,13 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail } } - // Handlers for main preview icon - const handlePreviewLoadingComplete = () => { - setIsPreviewLoading(false) - setHasPreviewError(false) - } - - const handlePreviewError = () => { - setIsPreviewLoading(false) - setHasPreviewError(true) - } - - // URLs for main preview icon - const previewWebpSrc = `${BASE_URL}/webp/${icon}.webp` - const previewOriginalSrc = `${BASE_URL}/${iconData.base}/${icon}.${iconData.base}` - const previewOriginalFormat = iconData.base - const renderVariant = (format: string, iconName: string, theme?: "light" | "dark") => { - const [isLoading, setIsLoading] = useState(true) - const [hasError, setHasError] = useState(false) - const variantName = theme && iconColorVariants?.[theme] ? iconColorVariants[theme] : iconName - const originalFormat = iconData.base - const originalImageUrl = `${BASE_URL}/${originalFormat}/${variantName}.${originalFormat}` - const webpImageUrl = `${BASE_URL}/webp/${variantName}.webp` - const githubUrl = `${REPO_PATH}/tree/main/${originalFormat}/${iconName}.${originalFormat}` + const imageUrl = `${BASE_URL}/${format}/${variantName}.${format}` + const githubUrl = `${REPO_PATH}/tree/main/${format}/${iconName}.${format}` const variantKey = `${format}-${theme || "default"}` const isCopied = copiedVariants[variantKey] || false - const handleLoadingComplete = () => { - setIsLoading(false) - setHasError(false) - } - - const handleError = () => { - setIsLoading(false) - setHasError(true) - } - return ( @@ -191,65 +158,51 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail !hasError && handleCopy(format === 'webp' ? webpImageUrl : originalImageUrl, variantKey, e)} - aria-label={hasError ? "Image failed to load" : `Copy ${format.toUpperCase()} URL for ${iconName}${theme ? ` (${theme} theme)` : ""}`} + className="relative w-28 h-28 mb-3 cursor-pointer rounded-xl overflow-hidden group" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + onClick={(e) => handleCopy(imageUrl, variantKey, e)} + aria-label={`Copy ${format.toUpperCase()} URL for ${iconName}${theme ? ` (${theme} theme)` : ""}`} > - {isLoading && !hasError && ( -
- )} - {hasError ? ( - - ) : ( - <> -
- - - - - +
- - - - {`${iconName} - - - )} + + + + + + + {`${iconName} -

- {hasError - ? "Image failed to load, likely due to size limits. Please raise an issue on GitHub." - : isCopied - ? "URL Copied!" - : "Click to copy direct URL to clipboard"} -

+

Click to copy direct URL to clipboard

-

- {format.toUpperCase()} {theme && `(${theme})`} -

+

{format.toUpperCase()}

@@ -258,15 +211,14 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail variant="outline" size="icon" className="h-8 w-8 rounded-lg cursor-pointer" - onClick={(e) => !hasError && handleDownload(e, format === 'webp' ? webpImageUrl : originalImageUrl, `${variantName}.${format}`)} + onClick={(e) => handleDownload(e, imageUrl, `${iconName}.${format}`)} aria-label={`Download ${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`} - disabled={hasError} > -

{hasError ? "Download unavailable" : "Download icon file"}

+

Download icon file

@@ -276,26 +228,30 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail variant="outline" size="icon" className="h-8 w-8 rounded-lg cursor-pointer" - onClick={(e) => !hasError && handleCopy(format === 'webp' ? webpImageUrl : originalImageUrl, `btn-${variantKey}`, e)} + onClick={(e) => handleCopy(imageUrl, `btn-${variantKey}`, e)} aria-label={`Copy URL for ${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`} - disabled={hasError} > {copiedVariants[`btn-${variantKey}`] ? : } -

{hasError ? "Copy unavailable" : isCopied ? "URL Copied!" : "Copy direct URL to clipboard"}

+

Copy direct URL to clipboard

- -
- )} - - - - ) - })()} + {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={{}} + /> + + +
+ )} ) -} +} \ No newline at end of file diff --git a/web/src/components/icon-grid.tsx b/web/src/components/icon-grid.tsx index 54c44357..f7ee8bb7 100644 --- a/web/src/components/icon-grid.tsx +++ b/web/src/components/icon-grid.tsx @@ -45,7 +45,7 @@ export function VirtualizedIconsGrid({ filteredIcons, matchedAliases }: IconsGri const rowVirtualizer = useWindowVirtualizer({ count: rowCount, estimateSize: () => 140, - overscan: 5, + overscan: 2, }) return ( diff --git a/web/src/components/icon-search.tsx b/web/src/components/icon-search.tsx index c0d73340..cf0832cb 100644 --- a/web/src/components/icon-search.tsx +++ b/web/src/components/icon-search.tsx @@ -4,7 +4,6 @@ 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, @@ -18,13 +17,10 @@ import { } 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 type { 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" @@ -229,11 +225,11 @@ export function IconSearch({ icons }: IconSearchProps) { const getSortLabel = (sort: SortOption) => { switch (sort) { case "relevance": - return "Relevance" + return "Best match" case "alphabetical-asc": - return "Name (A-Z)" + return "A to Z" case "alphabetical-desc": - return "Name (Z-A)" + return "Z to A" case "newest": return "Newest first" default: @@ -266,7 +262,7 @@ export function IconSearch({ icons }: IconSearchProps) {
handleSearch(e.target.value)} @@ -278,18 +274,18 @@ export function IconSearch({ icons }: IconSearchProps) { {/* Filter dropdown */} - - Select Categories + Categories
@@ -315,7 +311,7 @@ export function IconSearch({ icons }: IconSearchProps) { }} className="cursor-pointer focus: focus:bg-rose-50 dark:focus:bg-rose-950/20" > - Clear categories + Clear all filters )} @@ -331,20 +327,18 @@ export function IconSearch({ icons }: IconSearchProps) { - Sort Icons + Sort By handleSortChange(value as SortOption)}> - Relevance + Best match - - Name (A-Z) + A to Z - - Name (Z-A) + Z to A @@ -356,15 +350,9 @@ export function IconSearch({ icons }: IconSearchProps) { {/* Clear all button */} {(searchQuery || selectedCategories.length > 0 || sortOption !== "relevance") && ( - )}
@@ -372,7 +360,7 @@ export function IconSearch({ icons }: IconSearchProps) { {/* Active filter badges */} {selectedCategories.length > 0 && (
- Selected: + Filters:
{selectedCategories.map((category) => ( @@ -398,7 +386,7 @@ export function IconSearch({ icons }: IconSearchProps) { }} className="text-xs h-7 px-2 cursor-pointer" > - Clear + Clear all
)} @@ -409,33 +397,27 @@ export function IconSearch({ icons }: IconSearchProps) { {filteredIcons.length === 0 ? (
-

Icon not found

-

Help us expand our collection

-
-
- -
- Can't submit it yourself? - -
+

We don't have this one...yet!

+ +
) : ( <> @@ -455,52 +437,4 @@ export function IconSearch({ icons }: IconSearchProps) { )} ) -} - -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...

} - - ) -} +} \ No newline at end of file diff --git a/web/src/components/recently-added-icons.tsx b/web/src/components/recently-added-icons.tsx index ca422d0d..eb852ff8 100644 --- a/web/src/components/recently-added-icons.tsx +++ b/web/src/components/recently-added-icons.tsx @@ -2,14 +2,12 @@ import { Marquee } from "@/components/magicui/marquee" import { BASE_URL } from "@/constants" -import { cn } from "@/lib/utils" +import { cn, formatIconName } from "@/lib/utils" import type { Icon, IconWithName } from "@/types/icons" import { format, isToday, isYesterday } from "date-fns" -import { ArrowRight, Clock, ExternalLink, AlertTriangle } from "lucide-react" +import { ArrowRight, Clock, ExternalLink } from "lucide-react" import Image from "next/image" import Link from "next/link" -import { useState } from "react" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" function formatIconDate(timestamp: string): string { const date = new Date(timestamp) @@ -32,7 +30,7 @@ export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) { {/* Background glow */}