mirror of
				https://github.com/walkxcode/dashboard-icons.git
				synced 2025-10-31 16:57:58 +01:00 
			
		
		
		
	Format codebase
This commit is contained in:
		| @@ -19,7 +19,10 @@ | ||||
| 	"linter": { | ||||
| 		"enabled": true, | ||||
| 		"rules": { | ||||
| 			"recommended": true | ||||
| 			"recommended": true, | ||||
| 			"suspicious": { | ||||
| 				"noArrayIndexKey": "off" | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 	"javascript": { | ||||
|   | ||||
| @@ -78,10 +78,6 @@ | ||||
| 	}, | ||||
| 	"packageManager": "pnpm@10.8.0", | ||||
| 	"pnpm": { | ||||
| 		"onlyBuiltDependencies": [ | ||||
| 			"@biomejs/biome", | ||||
| 			"core-js", | ||||
| 			"sharp" | ||||
| 		] | ||||
| 		"onlyBuiltDependencies": ["@biomejs/biome", "core-js", "sharp"] | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -5,56 +5,56 @@ | ||||
| @custom-variant dark (&:is(.dark *)); | ||||
|  | ||||
| @theme inline { | ||||
|   --color-background: var(--background); | ||||
|   --color-foreground: var(--foreground); | ||||
|   --color-card: var(--card); | ||||
|   --color-card-foreground: var(--card-foreground); | ||||
|   --color-popover: var(--popover); | ||||
|   --color-popover-foreground: var(--popover-foreground); | ||||
|   --color-primary: var(--primary); | ||||
|   --color-primary-foreground: var(--primary-foreground); | ||||
|   --color-secondary: var(--secondary); | ||||
|   --color-secondary-foreground: var(--secondary-foreground); | ||||
|   --color-muted: var(--muted); | ||||
|   --color-muted-foreground: var(--muted-foreground); | ||||
|   --color-accent: var(--accent); | ||||
|   --color-accent-foreground: var(--accent-foreground); | ||||
|   --color-destructive: var(--destructive); | ||||
|   --color-destructive-foreground: var(--destructive-foreground); | ||||
|   --color-border: var(--border); | ||||
|   --color-input: var(--input); | ||||
|   --color-ring: var(--ring); | ||||
|   --color-chart-1: var(--chart-1); | ||||
|   --color-chart-2: var(--chart-2); | ||||
|   --color-chart-3: var(--chart-3); | ||||
|   --color-chart-4: var(--chart-4); | ||||
|   --color-chart-5: var(--chart-5); | ||||
|   --color-sidebar: var(--sidebar); | ||||
|   --color-sidebar-foreground: var(--sidebar-foreground); | ||||
|   --color-sidebar-primary: var(--sidebar-primary); | ||||
|   --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); | ||||
|   --color-sidebar-accent: var(--sidebar-accent); | ||||
|   --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); | ||||
|   --color-sidebar-border: var(--sidebar-border); | ||||
|   --color-sidebar-ring: var(--sidebar-ring); | ||||
| 	--color-background: var(--background); | ||||
| 	--color-foreground: var(--foreground); | ||||
| 	--color-card: var(--card); | ||||
| 	--color-card-foreground: var(--card-foreground); | ||||
| 	--color-popover: var(--popover); | ||||
| 	--color-popover-foreground: var(--popover-foreground); | ||||
| 	--color-primary: var(--primary); | ||||
| 	--color-primary-foreground: var(--primary-foreground); | ||||
| 	--color-secondary: var(--secondary); | ||||
| 	--color-secondary-foreground: var(--secondary-foreground); | ||||
| 	--color-muted: var(--muted); | ||||
| 	--color-muted-foreground: var(--muted-foreground); | ||||
| 	--color-accent: var(--accent); | ||||
| 	--color-accent-foreground: var(--accent-foreground); | ||||
| 	--color-destructive: var(--destructive); | ||||
| 	--color-destructive-foreground: var(--destructive-foreground); | ||||
| 	--color-border: var(--border); | ||||
| 	--color-input: var(--input); | ||||
| 	--color-ring: var(--ring); | ||||
| 	--color-chart-1: var(--chart-1); | ||||
| 	--color-chart-2: var(--chart-2); | ||||
| 	--color-chart-3: var(--chart-3); | ||||
| 	--color-chart-4: var(--chart-4); | ||||
| 	--color-chart-5: var(--chart-5); | ||||
| 	--color-sidebar: var(--sidebar); | ||||
| 	--color-sidebar-foreground: var(--sidebar-foreground); | ||||
| 	--color-sidebar-primary: var(--sidebar-primary); | ||||
| 	--color-sidebar-primary-foreground: var(--sidebar-primary-foreground); | ||||
| 	--color-sidebar-accent: var(--sidebar-accent); | ||||
| 	--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); | ||||
| 	--color-sidebar-border: var(--sidebar-border); | ||||
| 	--color-sidebar-ring: var(--sidebar-ring); | ||||
|  | ||||
|   --font-sans: var(--font-sans); | ||||
|   --font-mono: var(--font-mono); | ||||
|   --font-serif: var(--font-serif); | ||||
| 	--font-sans: var(--font-sans); | ||||
| 	--font-mono: var(--font-mono); | ||||
| 	--font-serif: var(--font-serif); | ||||
|  | ||||
|   --radius-sm: calc(var(--radius) - 4px); | ||||
|   --radius-md: calc(var(--radius) - 2px); | ||||
|   --radius-lg: var(--radius); | ||||
|   --radius-xl: calc(var(--radius) + 4px); | ||||
| 	--radius-sm: calc(var(--radius) - 4px); | ||||
| 	--radius-md: calc(var(--radius) - 2px); | ||||
| 	--radius-lg: var(--radius); | ||||
| 	--radius-xl: calc(var(--radius) + 4px); | ||||
|  | ||||
|   --shadow-2xs: var(--shadow-2xs); | ||||
|   --shadow-xs: var(--shadow-xs); | ||||
|   --shadow-sm: var(--shadow-sm); | ||||
|   --shadow: var(--shadow); | ||||
|   --shadow-md: var(--shadow-md); | ||||
|   --shadow-lg: var(--shadow-lg); | ||||
|   --shadow-xl: var(--shadow-xl); | ||||
|   --shadow-2xl: var(--shadow-2xl); | ||||
| 	--shadow-2xs: var(--shadow-2xs); | ||||
| 	--shadow-xs: var(--shadow-xs); | ||||
| 	--shadow-sm: var(--shadow-sm); | ||||
| 	--shadow: var(--shadow); | ||||
| 	--shadow-md: var(--shadow-md); | ||||
| 	--shadow-lg: var(--shadow-lg); | ||||
| 	--shadow-xl: var(--shadow-xl); | ||||
| 	--shadow-2xl: var(--shadow-2xl); | ||||
|  | ||||
| 	--animate-marquee: marquee var(--duration) infinite linear; | ||||
| 	--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; | ||||
| @@ -79,71 +79,80 @@ | ||||
| 	} | ||||
|  | ||||
| 	@keyframes marquee { | ||||
| 	from { | ||||
| 		transform: translateX(0);} | ||||
| 	to { | ||||
| 		transform: translateX(calc(-100% - var(--gap)));} | ||||
| 		from { | ||||
| 			transform: translateX(0); | ||||
| 		} | ||||
| 		to { | ||||
| 			transform: translateX(calc(-100% - var(--gap))); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	@keyframes marquee-vertical { | ||||
| 	from { | ||||
| 		transform: translateY(0);} | ||||
| 	to { | ||||
| 		transform: translateY(calc(-100% - var(--gap)));} | ||||
| 		from { | ||||
| 			transform: translateY(0); | ||||
| 		} | ||||
| 		to { | ||||
| 			transform: translateY(calc(-100% - var(--gap))); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	@keyframes aurora { | ||||
|     0% { | ||||
|       background-position: 0% 50%; | ||||
|       transform: rotate(-5deg) scale(0.9); | ||||
|     } | ||||
|     25% { | ||||
|       background-position: 50% 100%; | ||||
|       transform: rotate(5deg) scale(1.1); | ||||
|     } | ||||
|     50% { | ||||
|       background-position: 100% 50%; | ||||
|       transform: rotate(-3deg) scale(0.95); | ||||
|     } | ||||
|     75% { | ||||
|       background-position: 50% 0%; | ||||
|       transform: rotate(3deg) scale(1.05); | ||||
|     } | ||||
|     100% { | ||||
|       background-position: 0% 50%; | ||||
|       transform: rotate(-5deg) scale(0.9); | ||||
|     } | ||||
|   } | ||||
| 		0% { | ||||
| 			background-position: 0% 50%; | ||||
| 			transform: rotate(-5deg) scale(0.9); | ||||
| 		} | ||||
| 		25% { | ||||
| 			background-position: 50% 100%; | ||||
| 			transform: rotate(5deg) scale(1.1); | ||||
| 		} | ||||
| 		50% { | ||||
| 			background-position: 100% 50%; | ||||
| 			transform: rotate(-3deg) scale(0.95); | ||||
| 		} | ||||
| 		75% { | ||||
| 			background-position: 50% 0%; | ||||
| 			transform: rotate(3deg) scale(1.05); | ||||
| 		} | ||||
| 		100% { | ||||
| 			background-position: 0% 50%; | ||||
| 			transform: rotate(-5deg) scale(0.9); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	--animate-shiny-text: shiny-text 8s infinite | ||||
|  | ||||
| ; | ||||
|   @keyframes shiny-text { | ||||
|   0%, 90%, 100% { | ||||
|     background-position: calc(-100% - var(--shiny-width)) 0;} | ||||
|   30%, 60% { | ||||
|     background-position: calc(100% + var(--shiny-width)) 0;}}} | ||||
| 	--animate-shiny-text: shiny-text 8s infinite; | ||||
| 	@keyframes shiny-text { | ||||
| 		0%, | ||||
| 		90%, | ||||
| 		100% { | ||||
| 			background-position: calc(-100% - var(--shiny-width)) 0; | ||||
| 		} | ||||
| 		30%, | ||||
| 		60% { | ||||
| 			background-position: calc(100% + var(--shiny-width)) 0; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| :root { | ||||
| 	--radius: 0.4rem; | ||||
|  | ||||
|   --background: oklch(0.99 0 0); | ||||
|   --foreground: oklch(0.32 0 0); | ||||
|   --card: oklch(1.00 0 0); | ||||
|   --card-foreground: oklch(0.32 0 0); | ||||
|   --popover: oklch(1.00 0 0); | ||||
|   --popover-foreground: oklch(0.32 0 0); | ||||
|   --primary: oklch(0.67 0.20 23.80); | ||||
|   --primary-foreground: oklch(1.00 0 0); | ||||
|   --secondary: oklch(0.97 0.00 264.54); | ||||
|   --secondary-foreground: oklch(0.45 0.03 256.80); | ||||
|   --muted: oklch(0.98 0.00 247.84); | ||||
|   --muted-foreground: oklch(0.55 0.02 264.36); | ||||
| 	--background: oklch(0.99 0 0); | ||||
| 	--foreground: oklch(0.32 0 0); | ||||
| 	--card: oklch(1.0 0 0); | ||||
| 	--card-foreground: oklch(0.32 0 0); | ||||
| 	--popover: oklch(1.0 0 0); | ||||
| 	--popover-foreground: oklch(0.32 0 0); | ||||
| 	--primary: oklch(0.67 0.2 23.8); | ||||
| 	--primary-foreground: oklch(1.0 0 0); | ||||
| 	--secondary: oklch(0.97 0.0 264.54); | ||||
| 	--secondary-foreground: oklch(0.45 0.03 256.8); | ||||
| 	--muted: oklch(0.98 0.0 247.84); | ||||
| 	--muted-foreground: oklch(0.55 0.02 264.36); | ||||
| 	--accent: oklch(0.967 0.001 286.375); | ||||
| 	--accent-foreground: oklch(0.21 0.006 285.885); | ||||
|   --destructive: oklch(0.64 0.21 25.33); | ||||
|   --destructive-foreground: oklch(1.00 0 0); | ||||
|   --border: oklch(0.90 0.01 247.88); | ||||
| 	--destructive: oklch(0.64 0.21 25.33); | ||||
| 	--destructive-foreground: oklch(1.0 0 0); | ||||
| 	--border: oklch(0.9 0.01 247.88); | ||||
|  | ||||
| 	--input: oklch(0.92 0.004 286.32); | ||||
|  | ||||
| @@ -162,33 +171,38 @@ | ||||
| 	--sidebar-ring: oklch(0.637 0.237 25.331); | ||||
|  | ||||
| 	--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05); | ||||
|   --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05); | ||||
|   --shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10); | ||||
|   --shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10); | ||||
|   --shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 2px 4px -1px hsl(0 0% 0% / 0.10); | ||||
|   --shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 4px 6px -1px hsl(0 0% 0% / 0.10); | ||||
|   --shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 8px 10px -1px hsl(0 0% 0% / 0.10); | ||||
|   --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.25); | ||||
| 	--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05); | ||||
| 	--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px -1px | ||||
| 		hsl(0 0% 0% / 0.1); | ||||
| 	--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px -1px | ||||
| 		hsl(0 0% 0% / 0.1); | ||||
| 	--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 2px 4px -1px | ||||
| 		hsl(0 0% 0% / 0.1); | ||||
| 	--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 4px 6px -1px | ||||
| 		hsl(0 0% 0% / 0.1); | ||||
| 	--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 8px 10px -1px | ||||
| 		hsl(0 0% 0% / 0.1); | ||||
| 	--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.25); | ||||
| } | ||||
|  | ||||
| .dark { | ||||
|   --background: oklch(.141 .005 285.823); | ||||
|   --foreground: oklch(0.92 0 0); | ||||
|   --card: oklch(0.31 0.03 268.64); | ||||
|   --card-foreground: oklch(0.92 0 0); | ||||
|   --popover: oklch(0.29 0.02 268.40); | ||||
|   --popover-foreground: oklch(0.92 0 0); | ||||
|   --primary: oklch(0.67 0.20 23.80); | ||||
|   --primary-foreground: oklch(1.00 0 0); | ||||
|   --secondary: oklch(0.31 0.03 266.71); | ||||
|   --secondary-foreground: oklch(0.92 0 0); | ||||
|   --muted: oklch(0.31 0.03 266.71); | ||||
|   --muted-foreground: oklch(0.72 0 0); | ||||
|   --accent: oklch(0.34 0.06 267.59); | ||||
|   --accent-foreground: oklch(0.88 0.06 254.13); | ||||
|   --destructive: oklch(0.64 0.21 25.33); | ||||
|   --destructive-foreground: oklch(1.00 0 0); | ||||
|   --border: oklch(0.38 0.03 269.73); | ||||
| 	--background: oklch(0.141 0.005 285.823); | ||||
| 	--foreground: oklch(0.92 0 0); | ||||
| 	--card: oklch(0.31 0.03 268.64); | ||||
| 	--card-foreground: oklch(0.92 0 0); | ||||
| 	--popover: oklch(0.29 0.02 268.4); | ||||
| 	--popover-foreground: oklch(0.92 0 0); | ||||
| 	--primary: oklch(0.67 0.2 23.8); | ||||
| 	--primary-foreground: oklch(1.0 0 0); | ||||
| 	--secondary: oklch(0.31 0.03 266.71); | ||||
| 	--secondary-foreground: oklch(0.92 0 0); | ||||
| 	--muted: oklch(0.31 0.03 266.71); | ||||
| 	--muted-foreground: oklch(0.72 0 0); | ||||
| 	--accent: oklch(0.34 0.06 267.59); | ||||
| 	--accent-foreground: oklch(0.88 0.06 254.13); | ||||
| 	--destructive: oklch(0.64 0.21 25.33); | ||||
| 	--destructive-foreground: oklch(1.0 0 0); | ||||
| 	--border: oklch(0.38 0.03 269.73); | ||||
|  | ||||
| 	--input: oklch(1 0 0 / 15%); | ||||
| 	--ring: oklch(0.637 0.237 25.331); | ||||
| @@ -207,13 +221,18 @@ | ||||
| 	--sidebar-ring: oklch(0.637 0.237 25.331); | ||||
|  | ||||
| 	--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05); | ||||
|   --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05); | ||||
|   --shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10); | ||||
|   --shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10); | ||||
|   --shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 2px 4px -1px hsl(0 0% 0% / 0.10); | ||||
|   --shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 4px 6px -1px hsl(0 0% 0% / 0.10); | ||||
|   --shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.10), 0px 8px 10px -1px hsl(0 0% 0% / 0.10); | ||||
|   --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.25); | ||||
| 	--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05); | ||||
| 	--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px -1px | ||||
| 		hsl(0 0% 0% / 0.1); | ||||
| 	--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px -1px | ||||
| 		hsl(0 0% 0% / 0.1); | ||||
| 	--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 2px 4px -1px | ||||
| 		hsl(0 0% 0% / 0.1); | ||||
| 	--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 4px 6px -1px | ||||
| 		hsl(0 0% 0% / 0.1); | ||||
| 	--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 8px 10px -1px | ||||
| 		hsl(0 0% 0% / 0.1); | ||||
| 	--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.25); | ||||
| } | ||||
|  | ||||
| @layer base { | ||||
| @@ -241,4 +260,4 @@ | ||||
| 	.glass-effect { | ||||
| 		@apply backdrop-blur-sm; | ||||
| 	} | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -74,7 +74,7 @@ export async function generateMetadata({ params, searchParams }: Props, parent: | ||||
| 					height: 512, | ||||
| 					alt: `${formattedIconName} Icon`, | ||||
| 					type: "image/png", | ||||
| 				} | ||||
| 				}, | ||||
| 			], | ||||
| 			authors: [authorName, "homarr"], | ||||
| 			publishedTime: updateDate.toISOString(), | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| "use client"; | ||||
| "use client" | ||||
|  | ||||
| import { IconSubmissionContent } from "@/components/icon-submission-form"; | ||||
| import { MagicCard } from "@/components/magicui/magic-card"; | ||||
| import { Badge } from "@/components/ui/badge"; | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { IconSubmissionContent } from "@/components/icon-submission-form" | ||||
| import { MagicCard } from "@/components/magicui/magic-card" | ||||
| import { Badge } from "@/components/ui/badge" | ||||
| import { Button } from "@/components/ui/button" | ||||
| import { | ||||
| 	DropdownMenu, | ||||
| 	DropdownMenuCheckboxItem, | ||||
| @@ -14,242 +14,217 @@ import { | ||||
| 	DropdownMenuRadioItem, | ||||
| 	DropdownMenuSeparator, | ||||
| 	DropdownMenuTrigger, | ||||
| } 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 { useInView } from "framer-motion"; | ||||
| import { | ||||
| 	ArrowDownAZ, | ||||
| 	ArrowUpZA, | ||||
| 	Calendar, | ||||
| 	Filter, | ||||
| 	Search, | ||||
| 	SortAsc, | ||||
| 	X, | ||||
| } from "lucide-react"; | ||||
| import { useTheme } from "next-themes"; | ||||
| import Image from "next/image"; | ||||
| import Link from "next/link"; | ||||
| import { usePathname, useRouter, useSearchParams } from "next/navigation"; | ||||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||||
| } 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 { useInView } from "framer-motion" | ||||
| import { ArrowDownAZ, ArrowUpZA, Calendar, Filter, Search, SortAsc, X } from "lucide-react" | ||||
| import { useTheme } from "next-themes" | ||||
| import Image from "next/image" | ||||
| import Link from "next/link" | ||||
| import { usePathname, useRouter, useSearchParams } from "next/navigation" | ||||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react" | ||||
|  | ||||
| type SortOption = | ||||
| 	| "relevance" | ||||
| 	| "alphabetical-asc" | ||||
| 	| "alphabetical-desc" | ||||
| 	| "newest"; | ||||
| type SortOption = "relevance" | "alphabetical-asc" | "alphabetical-desc" | "newest" | ||||
|  | ||||
| export function IconSearch({ icons }: IconSearchProps) { | ||||
| 	const searchParams = useSearchParams(); | ||||
| 	const initialQuery = searchParams.get("q"); | ||||
| 	const initialCategories = searchParams.getAll("category"); | ||||
| 	const initialSort = (searchParams.get("sort") as SortOption) || "relevance"; | ||||
| 	const router = useRouter(); | ||||
| 	const pathname = usePathname(); | ||||
| 	const [searchQuery, setSearchQuery] = useState(initialQuery ?? ""); | ||||
| 	const [selectedCategories, setSelectedCategories] = useState<string[]>( | ||||
| 		initialCategories ?? [], | ||||
| 	); | ||||
| 	const [sortOption, setSortOption] = useState<SortOption>(initialSort); | ||||
| 	const timeoutRef = useRef<NodeJS.Timeout | null>(null); | ||||
| 	const { resolvedTheme } = useTheme(); | ||||
| 	const searchParams = useSearchParams() | ||||
| 	const initialQuery = searchParams.get("q") | ||||
| 	const initialCategories = searchParams.getAll("category") | ||||
| 	const initialSort = (searchParams.get("sort") as SortOption) || "relevance" | ||||
| 	const router = useRouter() | ||||
| 	const pathname = usePathname() | ||||
| 	const [searchQuery, setSearchQuery] = useState(initialQuery ?? "") | ||||
| 	const [selectedCategories, setSelectedCategories] = useState<string[]>(initialCategories ?? []) | ||||
| 	const [sortOption, setSortOption] = useState<SortOption>(initialSort) | ||||
| 	const timeoutRef = useRef<NodeJS.Timeout | null>(null) | ||||
| 	const { resolvedTheme } = useTheme() | ||||
|  | ||||
| 	// Extract all unique categories | ||||
| 	const allCategories = useMemo(() => { | ||||
| 		const categories = new Set<string>(); | ||||
| 		const categories = new Set<string>() | ||||
| 		for (const icon of icons) { | ||||
| 			for (const category of icon.data.categories) { | ||||
| 				categories.add(category); | ||||
| 				categories.add(category) | ||||
| 			} | ||||
| 		} | ||||
| 		return Array.from(categories).sort(); | ||||
| 	}, [icons]); | ||||
| 		return Array.from(categories).sort() | ||||
| 	}, [icons]) | ||||
|  | ||||
| 	// Simple filter function using substring matching | ||||
| 	const filterIcons = useCallback( | ||||
| 		(query: string, categories: string[], sort: SortOption) => { | ||||
| 			// First filter by categories if any are selected | ||||
| 			let filtered = icons; | ||||
| 			let filtered = icons | ||||
| 			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())), | ||||
| 				) | ||||
| 			} | ||||
|  | ||||
| 			// Then filter by search query | ||||
| 			if (query.trim()) { | ||||
| 				const q = query.toLowerCase(); | ||||
| 				const q = query.toLowerCase() | ||||
| 				filtered = filtered.filter(({ name, data }) => { | ||||
| 					if (name.toLowerCase().includes(q)) return true; | ||||
| 					if (data.aliases.some((alias) => alias.toLowerCase().includes(q))) return true; | ||||
| 					if (data.categories.some((category) => category.toLowerCase().includes(q))) return true; | ||||
| 					return false; | ||||
| 				}); | ||||
| 					if (name.toLowerCase().includes(q)) return true | ||||
| 					if (data.aliases.some((alias) => alias.toLowerCase().includes(q))) return true | ||||
| 					if (data.categories.some((category) => category.toLowerCase().includes(q))) return true | ||||
| 					return false | ||||
| 				}) | ||||
| 			} | ||||
|  | ||||
| 			// Apply sorting | ||||
| 			if (sort === "alphabetical-asc") { | ||||
| 				return filtered.sort((a, b) => a.name.localeCompare(b.name)); | ||||
| 				return filtered.sort((a, b) => a.name.localeCompare(b.name)) | ||||
| 			} | ||||
| 			if (sort === "alphabetical-desc") { | ||||
| 				return filtered.sort((a, b) => b.name.localeCompare(a.name)); | ||||
| 				return filtered.sort((a, b) => b.name.localeCompare(a.name)) | ||||
| 			} | ||||
| 			if (sort === "newest") { | ||||
| 				return filtered.sort((a, b) => { | ||||
| 					return ( | ||||
| 						new Date(b.data.update.timestamp).getTime() - | ||||
| 						new Date(a.data.update.timestamp).getTime() | ||||
| 					); | ||||
| 				}); | ||||
| 					return new Date(b.data.update.timestamp).getTime() - new Date(a.data.update.timestamp).getTime() | ||||
| 				}) | ||||
| 			} | ||||
|  | ||||
| 			// Default sort (relevance or fallback to alphabetical) | ||||
| 			return filtered.sort((a, b) => a.name.localeCompare(b.name)); | ||||
| 			return filtered.sort((a, b) => a.name.localeCompare(b.name)) | ||||
| 		}, | ||||
| 		[icons], | ||||
| 	); | ||||
| 	) | ||||
|  | ||||
| 	// Find matched aliases for display purposes | ||||
| 	const matchedAliases = useMemo(() => { | ||||
| 		if (!searchQuery.trim()) return {}; | ||||
| 		if (!searchQuery.trim()) return {} | ||||
|  | ||||
| 		const q = searchQuery.toLowerCase(); | ||||
| 		const matches: Record<string, string> = {}; | ||||
| 		const q = searchQuery.toLowerCase() | ||||
| 		const matches: Record<string, string> = {} | ||||
|  | ||||
| 		icons.forEach(({ name, data }) => { | ||||
| 		for (const { name, data } of icons) { | ||||
| 			// If name doesn't match but an alias does, store the first matching alias | ||||
| 			if (!name.toLowerCase().includes(q)) { | ||||
| 				const matchingAlias = data.aliases.find((alias) => | ||||
| 					alias.toLowerCase().includes(q) | ||||
| 				); | ||||
| 				const matchingAlias = data.aliases.find((alias) => alias.toLowerCase().includes(q)) | ||||
| 				if (matchingAlias) { | ||||
| 					matches[name] = matchingAlias; | ||||
| 					matches[name] = matchingAlias | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 		} | ||||
|  | ||||
| 		return matches; | ||||
| 	}, [icons, searchQuery]); | ||||
| 		return matches | ||||
| 	}, [icons, searchQuery]) | ||||
|  | ||||
| 	// Use useMemo for filtered icons | ||||
| 	const filteredIcons = useMemo(() => { | ||||
| 		return filterIcons(searchQuery, selectedCategories, sortOption); | ||||
| 	}, [filterIcons, searchQuery, selectedCategories, sortOption]); | ||||
| 		return filterIcons(searchQuery, selectedCategories, sortOption) | ||||
| 	}, [filterIcons, searchQuery, selectedCategories, sortOption]) | ||||
|  | ||||
| 	const updateResults = useCallback( | ||||
| 		(query: string, categories: string[], sort: SortOption) => { | ||||
| 			const params = new URLSearchParams(); | ||||
| 			if (query) params.set("q", query); | ||||
| 			const params = new URLSearchParams() | ||||
| 			if (query) params.set("q", query) | ||||
|  | ||||
| 			// Clear existing category params and add new ones | ||||
| 			for (const category of categories) { | ||||
| 				params.append("category", category); | ||||
| 				params.append("category", category) | ||||
| 			} | ||||
|  | ||||
| 			// Add sort parameter if not default | ||||
| 			if (sort !== "relevance" || initialSort !== "relevance") { | ||||
| 				params.set("sort", sort); | ||||
| 				params.set("sort", sort) | ||||
| 			} | ||||
|  | ||||
| 			const newUrl = params.toString() | ||||
| 				? `${pathname}?${params.toString()}` | ||||
| 				: pathname; | ||||
| 			router.push(newUrl, { scroll: false }); | ||||
| 			const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname | ||||
| 			router.push(newUrl, { scroll: false }) | ||||
| 		}, | ||||
| 		[pathname, router, initialSort], | ||||
| 	); | ||||
| 	) | ||||
|  | ||||
| 	const handleSearch = useCallback( | ||||
| 		(query: string) => { | ||||
| 			setSearchQuery(query); | ||||
| 			setSearchQuery(query) | ||||
| 			if (timeoutRef.current) { | ||||
| 				clearTimeout(timeoutRef.current); | ||||
| 				clearTimeout(timeoutRef.current) | ||||
| 			} | ||||
| 			timeoutRef.current = setTimeout(() => { | ||||
| 				updateResults(query, selectedCategories, sortOption); | ||||
| 			}, 200); // Changed from 100ms to 200ms | ||||
| 				updateResults(query, selectedCategories, sortOption) | ||||
| 			}, 200) // Changed from 100ms to 200ms | ||||
| 		}, | ||||
| 		[updateResults, selectedCategories, sortOption], | ||||
| 	); | ||||
| 	) | ||||
|  | ||||
| 	const handleCategoryChange = useCallback( | ||||
| 		(category: string) => { | ||||
| 			let newCategories: string[]; | ||||
| 			let newCategories: string[] | ||||
|  | ||||
| 			if (selectedCategories.includes(category)) { | ||||
| 				// Remove the category if it's already selected | ||||
| 				newCategories = selectedCategories.filter((c) => c !== category); | ||||
| 				newCategories = selectedCategories.filter((c) => c !== category) | ||||
| 			} else { | ||||
| 				// Add the category if it's not selected | ||||
| 				newCategories = [...selectedCategories, category]; | ||||
| 				newCategories = [...selectedCategories, category] | ||||
| 			} | ||||
|  | ||||
| 			setSelectedCategories(newCategories); | ||||
| 			updateResults(searchQuery, newCategories, sortOption); | ||||
| 			setSelectedCategories(newCategories) | ||||
| 			updateResults(searchQuery, newCategories, sortOption) | ||||
| 		}, | ||||
| 		[updateResults, searchQuery, selectedCategories, sortOption], | ||||
| 	); | ||||
| 	) | ||||
|  | ||||
| 	const handleSortChange = useCallback( | ||||
| 		(sort: SortOption) => { | ||||
| 			setSortOption(sort); | ||||
| 			updateResults(searchQuery, selectedCategories, sort); | ||||
| 			setSortOption(sort) | ||||
| 			updateResults(searchQuery, selectedCategories, sort) | ||||
| 		}, | ||||
| 		[updateResults, searchQuery, selectedCategories], | ||||
| 	); | ||||
| 	) | ||||
|  | ||||
| 	const clearFilters = useCallback(() => { | ||||
| 		setSearchQuery(""); | ||||
| 		setSelectedCategories([]); | ||||
| 		setSortOption("relevance"); | ||||
| 		updateResults("", [], "relevance"); | ||||
| 	}, [updateResults]); | ||||
| 		setSearchQuery("") | ||||
| 		setSelectedCategories([]) | ||||
| 		setSortOption("relevance") | ||||
| 		updateResults("", [], "relevance") | ||||
| 	}, [updateResults]) | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		return () => { | ||||
| 			if (timeoutRef.current) { | ||||
| 				clearTimeout(timeoutRef.current); | ||||
| 				clearTimeout(timeoutRef.current) | ||||
| 			} | ||||
| 		}; | ||||
| 	}, []); | ||||
| 		} | ||||
| 	}, []) | ||||
|  | ||||
| 	if (!searchParams) return null; | ||||
| 	if (!searchParams) return null | ||||
|  | ||||
| 	const getSortLabel = (sort: SortOption) => { | ||||
| 		switch (sort) { | ||||
| 			case "relevance": | ||||
| 				return "Best match"; | ||||
| 				return "Best match" | ||||
| 			case "alphabetical-asc": | ||||
| 				return "A to Z"; | ||||
| 				return "A to Z" | ||||
| 			case "alphabetical-desc": | ||||
| 				return "Z to A"; | ||||
| 				return "Z to A" | ||||
| 			case "newest": | ||||
| 				return "Newest first"; | ||||
| 				return "Newest first" | ||||
| 			default: | ||||
| 				return "Sort"; | ||||
| 				return "Sort" | ||||
| 		} | ||||
| 	}; | ||||
| 	} | ||||
|  | ||||
| 	const getSortIcon = (sort: SortOption) => { | ||||
| 		switch (sort) { | ||||
| 			case "relevance": | ||||
| 				return <Search className="h-4 w-4" />; | ||||
| 				return <Search className="h-4 w-4" /> | ||||
| 			case "alphabetical-asc": | ||||
| 				return <ArrowDownAZ className="h-4 w-4" />; | ||||
| 				return <ArrowDownAZ className="h-4 w-4" /> | ||||
| 			case "alphabetical-desc": | ||||
| 				return <ArrowUpZA className="h-4 w-4" />; | ||||
| 				return <ArrowUpZA className="h-4 w-4" /> | ||||
| 			case "newest": | ||||
| 				return <Calendar className="h-4 w-4" />; | ||||
| 				return <Calendar className="h-4 w-4" /> | ||||
| 			default: | ||||
| 				return <SortAsc className="h-4 w-4" />; | ||||
| 				return <SortAsc className="h-4 w-4" /> | ||||
| 		} | ||||
| 	}; | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<> | ||||
| @@ -273,11 +248,7 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 					{/* Filter dropdown */} | ||||
| 					<DropdownMenu> | ||||
| 						<DropdownMenuTrigger asChild> | ||||
| 							<Button | ||||
| 								variant="outline" | ||||
| 								size="sm" | ||||
| 								className="flex-1 sm:flex-none cursor-pointer   border-border shadow-sm0/10 " | ||||
| 							> | ||||
| 							<Button variant="outline" size="sm" className="flex-1 sm:flex-none cursor-pointer   border-border shadow-sm0/10 "> | ||||
| 								<Filter className="h-4 w-4 mr-2" /> | ||||
| 								<span>Filter</span> | ||||
| 								{selectedCategories.length > 0 && ( | ||||
| @@ -288,9 +259,7 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 							</Button> | ||||
| 						</DropdownMenuTrigger> | ||||
| 						<DropdownMenuContent align="start" className="w-64 sm:w-56"> | ||||
| 							<DropdownMenuLabel className="font-semibold"> | ||||
| 								Categories | ||||
| 							</DropdownMenuLabel> | ||||
| 							<DropdownMenuLabel className="font-semibold">Categories</DropdownMenuLabel> | ||||
| 							<DropdownMenuSeparator /> | ||||
|  | ||||
| 							<div className="max-h-[40vh] overflow-y-auto p-1"> | ||||
| @@ -301,9 +270,7 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 										onCheckedChange={() => handleCategoryChange(category)} | ||||
| 										className="cursor-pointer capitalize" | ||||
| 									> | ||||
| 										{category | ||||
| 											.replace(/-/g, " ") | ||||
| 											.replace(/\b\w/g, (c) => c.toUpperCase())} | ||||
| 										{category.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} | ||||
| 									</DropdownMenuCheckboxItem> | ||||
| 								))} | ||||
| 							</div> | ||||
| @@ -313,8 +280,8 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 									<DropdownMenuSeparator /> | ||||
| 									<DropdownMenuItem | ||||
| 										onClick={() => { | ||||
| 											setSelectedCategories([]); | ||||
| 											updateResults(searchQuery, [], sortOption); | ||||
| 											setSelectedCategories([]) | ||||
| 											updateResults(searchQuery, [], sortOption) | ||||
| 										}} | ||||
| 										className="cursor-pointer  focus: focus:bg-rose-50 dark:focus:bg-rose-950/20" | ||||
| 									> | ||||
| @@ -338,37 +305,20 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 							</Button> | ||||
| 						</DropdownMenuTrigger> | ||||
| 						<DropdownMenuContent align="start" className="w-56"> | ||||
| 							<DropdownMenuLabel className="font-semibold"> | ||||
| 								Sort By | ||||
| 							</DropdownMenuLabel> | ||||
| 							<DropdownMenuLabel className="font-semibold">Sort By</DropdownMenuLabel> | ||||
| 							<DropdownMenuSeparator /> | ||||
| 							<DropdownMenuRadioGroup | ||||
| 								value={sortOption} | ||||
| 								onValueChange={(value) => handleSortChange(value as SortOption)} | ||||
| 							> | ||||
| 								<DropdownMenuRadioItem | ||||
| 									value="relevance" | ||||
| 									className="cursor-pointer" | ||||
| 								> | ||||
| 							<DropdownMenuRadioGroup value={sortOption} onValueChange={(value) => handleSortChange(value as SortOption)}> | ||||
| 								<DropdownMenuRadioItem value="relevance" className="cursor-pointer"> | ||||
| 									<Search className="h-4 w-4 mr-2" /> | ||||
| 									Best match | ||||
| 								</DropdownMenuRadioItem> | ||||
| 								<DropdownMenuRadioItem | ||||
| 									value="alphabetical-asc" | ||||
| 									className="cursor-pointer" | ||||
| 								> | ||||
| 								<DropdownMenuRadioItem value="alphabetical-asc" className="cursor-pointer"> | ||||
| 									<ArrowDownAZ className="h-4 w-4 mr-2" />A to Z | ||||
| 								</DropdownMenuRadioItem> | ||||
| 								<DropdownMenuRadioItem | ||||
| 									value="alphabetical-desc" | ||||
| 									className="cursor-pointer" | ||||
| 								> | ||||
| 								<DropdownMenuRadioItem value="alphabetical-desc" className="cursor-pointer"> | ||||
| 									<ArrowUpZA className="h-4 w-4 mr-2" />Z to A | ||||
| 								</DropdownMenuRadioItem> | ||||
| 								<DropdownMenuRadioItem | ||||
| 									value="newest" | ||||
| 									className="cursor-pointer" | ||||
| 								> | ||||
| 								<DropdownMenuRadioItem value="newest" className="cursor-pointer"> | ||||
| 									<Calendar className="h-4 w-4 mr-2" /> | ||||
| 									Newest first | ||||
| 								</DropdownMenuRadioItem> | ||||
| @@ -377,15 +327,8 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 					</DropdownMenu> | ||||
|  | ||||
| 					{/* Clear all button */} | ||||
| 					{(searchQuery || | ||||
| 						selectedCategories.length > 0 || | ||||
| 						sortOption !== "relevance") && ( | ||||
| 						<Button | ||||
| 							variant="outline" | ||||
| 							size="sm" | ||||
| 							onClick={clearFilters} | ||||
| 							className="flex-1 sm:flex-none cursor-pointer    border-rose-500/20" | ||||
| 						> | ||||
| 					{(searchQuery || selectedCategories.length > 0 || sortOption !== "relevance") && ( | ||||
| 						<Button variant="outline" size="sm" onClick={clearFilters} className="flex-1 sm:flex-none cursor-pointer    border-rose-500/20"> | ||||
| 							<X className="h-4 w-4 mr-2" /> | ||||
| 							<span>Clear all</span> | ||||
| 						</Button> | ||||
| @@ -398,14 +341,8 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 						<span className="text-sm text-muted-foreground">Filters:</span> | ||||
| 						<div className="flex flex-wrap gap-2"> | ||||
| 							{selectedCategories.map((category) => ( | ||||
| 								<Badge | ||||
| 									key={category} | ||||
| 									variant="secondary" | ||||
| 									className="flex items-center gap-1 pl-2 pr-1" | ||||
| 								> | ||||
| 									{category | ||||
| 										.replace(/-/g, " ") | ||||
| 										.replace(/\b\w/g, (c) => c.toUpperCase())} | ||||
| 								<Badge key={category} variant="secondary" className="flex items-center gap-1 pl-2 pr-1"> | ||||
| 									{category.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} | ||||
| 									<Button | ||||
| 										variant="ghost" | ||||
| 										size="sm" | ||||
| @@ -422,8 +359,8 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 							variant="ghost" | ||||
| 							size="sm" | ||||
| 							onClick={() => { | ||||
| 								setSelectedCategories([]); | ||||
| 								updateResults(searchQuery, [], sortOption); | ||||
| 								setSelectedCategories([]) | ||||
| 								updateResults(searchQuery, [], sortOption) | ||||
| 							}} | ||||
| 							className="text-xs h-7 px-2  0/10 cursor-pointer" | ||||
| 						> | ||||
| @@ -438,9 +375,7 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 			{filteredIcons.length === 0 ? ( | ||||
| 				<div className="flex flex-col gap-8 py-12 max-w-2xl mx-auto"> | ||||
| 					<div className="text-center"> | ||||
| 						<h2 className="text-3xl sm:text-5xl font-semibold"> | ||||
| 							We don't have this one...yet! | ||||
| 						</h2> | ||||
| 						<h2 className="text-3xl sm:text-5xl font-semibold">We don't have this one...yet!</h2> | ||||
| 						<p className="mt-4 text-muted-foreground"> | ||||
| 							{searchQuery && selectedCategories.length > 0 | ||||
| 								? `No icons found matching "${searchQuery}" with the selected filters.` | ||||
| @@ -467,18 +402,13 @@ export function IconSearch({ icons }: IconSearchProps) { | ||||
| 					</div> | ||||
| 					<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4 mt-2"> | ||||
| 						{filteredIcons.map(({ name, data }) => ( | ||||
| 							<IconCard | ||||
| 								key={name} | ||||
| 								name={name} | ||||
| 								data={data} | ||||
| 								matchedAlias={matchedAliases[name] || null} | ||||
| 							/> | ||||
| 							<IconCard key={name} name={name} data={data} matchedAlias={matchedAliases[name] || null} /> | ||||
| 						))} | ||||
| 					</div> | ||||
| 				</> | ||||
| 			)} | ||||
| 		</> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|  | ||||
| function IconCard({ | ||||
| @@ -486,16 +416,16 @@ function IconCard({ | ||||
| 	data: iconData, | ||||
| 	matchedAlias, | ||||
| }: { | ||||
| 	name: string; | ||||
| 	data: Icon; | ||||
| 	matchedAlias?: string | null; | ||||
| 	name: string | ||||
| 	data: Icon | ||||
| 	matchedAlias?: string | null | ||||
| }) { | ||||
| 	const ref = useRef(null); | ||||
| 	const ref = useRef(null) | ||||
| 	const isInView = useInView(ref, { | ||||
| 		once: false, | ||||
| 		amount: 0.2, | ||||
| 		margin: "100px 0px", | ||||
| 	}); | ||||
| 	}) | ||||
|  | ||||
| 	const variants = { | ||||
| 		hidden: { opacity: 0, y: 20, scale: 0.95 }, | ||||
| @@ -511,15 +441,11 @@ function IconCard({ | ||||
| 			scale: 0.98, | ||||
| 			transition: { duration: 0.3, ease: [0.25, 0.1, 0.25, 1.0] }, | ||||
| 		}, | ||||
| 	}; | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<MagicCard className="rounded-md shadow-md"> | ||||
| 			<Link | ||||
| 				prefetch={false} | ||||
| 				href={`/icons/${name}`} | ||||
| 				className="group flex flex-col items-center p-3 sm:p-4 cursor-pointer" | ||||
| 			> | ||||
| 			<Link prefetch={false} href={`/icons/${name}`} className="group flex flex-col items-center p-3 sm:p-4 cursor-pointer"> | ||||
| 				<div className="relative h-12 w-12 sm:h-16 sm:w-16 mb-2"> | ||||
| 					<Image | ||||
| 						src={`${BASE_URL}/${iconData.base}/${name}.${iconData.base}`} | ||||
| @@ -532,12 +458,8 @@ function IconCard({ | ||||
| 					{name.replace(/-/g, " ")} | ||||
| 				</span> | ||||
|  | ||||
| 				{matchedAlias && ( | ||||
| 					<span className="text-[10px] text-center truncate w-full mt-1"> | ||||
| 						Alias: {matchedAlias} | ||||
| 					</span> | ||||
| 				)} | ||||
| 				{matchedAlias && <span className="text-[10px] text-center truncate w-full mt-1">Alias: {matchedAlias}</span>} | ||||
| 			</Link> | ||||
| 		</MagicCard> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| "use client"; | ||||
| "use client" | ||||
|  | ||||
| import { REPO_PATH } from "@/constants"; | ||||
| import { motion } from "framer-motion"; | ||||
| import { ExternalLink, Github, Heart } from "lucide-react"; | ||||
| import Link from "next/link"; | ||||
| import { useState } from "react"; | ||||
| import { REPO_PATH } from "@/constants" | ||||
| import { motion } from "framer-motion" | ||||
| import { ExternalLink, Github, Heart } from "lucide-react" | ||||
| import Link from "next/link" | ||||
| import { useState } from "react" | ||||
|  | ||||
| // Pre-define unique IDs for animations to avoid using array indices as keys | ||||
| const HOVER_HEART_IDS = [ | ||||
| @@ -16,23 +16,17 @@ const HOVER_HEART_IDS = [ | ||||
| 	"hover-heart-6", | ||||
| 	"hover-heart-7", | ||||
| 	"hover-heart-8", | ||||
| ]; | ||||
| const BURST_HEART_IDS = [ | ||||
| 	"burst-heart-1", | ||||
| 	"burst-heart-2", | ||||
| 	"burst-heart-3", | ||||
| 	"burst-heart-4", | ||||
| 	"burst-heart-5", | ||||
| ]; | ||||
| ] | ||||
| const BURST_HEART_IDS = ["burst-heart-1", "burst-heart-2", "burst-heart-3", "burst-heart-4", "burst-heart-5"] | ||||
|  | ||||
| export function Footer() { | ||||
| 	const [isHeartHovered, setIsHeartHovered] = useState(false); | ||||
| 	const [isHeartFilled, setIsHeartFilled] = useState(false); | ||||
| 	const [isHeartHovered, setIsHeartHovered] = useState(false) | ||||
| 	const [isHeartFilled, setIsHeartFilled] = useState(false) | ||||
|  | ||||
| 	// Toggle heart fill state and add extra mini hearts on click | ||||
| 	const handleHeartClick = () => { | ||||
| 		setIsHeartFilled(!isHeartFilled); | ||||
| 	}; | ||||
| 		setIsHeartFilled(!isHeartFilled) | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<footer className="border-t py-4  relative overflow-hidden"> | ||||
| @@ -41,28 +35,19 @@ export function Footer() { | ||||
| 			<div className="container mx-auto px-4 md:px-6 relative z-10"> | ||||
| 				<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12"> | ||||
| 					<div className="flex flex-col gap-3"> | ||||
| 						<h3 className="font-bold text-lg text-foreground/90"> | ||||
| 							Dashboard Icons | ||||
| 						</h3> | ||||
| 						<h3 className="font-bold text-lg text-foreground/90">Dashboard Icons</h3> | ||||
| 						<p className="text-sm text-muted-foreground leading-relaxed"> | ||||
| 							A collection of curated icons for services, applications and | ||||
| 							tools, designed specifically for dashboards and app directories. | ||||
| 							A collection of curated icons for services, applications and tools, designed specifically for dashboards and app directories. | ||||
| 						</p> | ||||
| 					</div> | ||||
|  | ||||
| 					<div className="flex flex-col gap-3"> | ||||
| 						<h3 className="font-bold text-lg text-foreground/90">Links</h3> | ||||
| 						<div className="flex flex-col gap-2"> | ||||
| 							<Link | ||||
| 								href="/" | ||||
| 								className="text-sm text-muted-foreground hover: transition-colors duration-200 flex items-center w-fit" | ||||
| 							> | ||||
| 							<Link href="/" className="text-sm text-muted-foreground hover: transition-colors duration-200 flex items-center w-fit"> | ||||
| 								<span>Home</span> | ||||
| 							</Link> | ||||
| 							<Link | ||||
| 								href="/icons" | ||||
| 								className="text-sm text-muted-foreground hover: transition-colors duration-200 flex items-center w-fit" | ||||
| 							> | ||||
| 							<Link href="/icons" className="text-sm text-muted-foreground hover: transition-colors duration-200 flex items-center w-fit"> | ||||
| 								<span>Icons</span> | ||||
| 							</Link> | ||||
| 							<Link | ||||
| @@ -132,9 +117,7 @@ export function Footer() { | ||||
| 												}} | ||||
| 												className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none" | ||||
| 											> | ||||
| 												<Heart | ||||
| 													className={`h-2 w-2 ${i < 3 ? "text-rose-300" : i < 6 ? "text-rose-400" : ""}`} | ||||
| 												/> | ||||
| 												<Heart className={`h-2 w-2 ${i < 3 ? "text-rose-300" : i < 6 ? "text-rose-400" : ""}`} /> | ||||
| 											</motion.div> | ||||
| 										))} | ||||
|  | ||||
| @@ -182,10 +165,7 @@ export function Footer() { | ||||
| 												}} | ||||
| 												className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none" | ||||
| 											> | ||||
| 												<Heart | ||||
| 													className="h-2 w-2 " | ||||
| 													fill="#f43f5e" | ||||
| 												/> | ||||
| 												<Heart className="h-2 w-2 " fill="#f43f5e" /> | ||||
| 											</motion.div> | ||||
| 										))} | ||||
| 									</> | ||||
| @@ -210,5 +190,5 @@ export function Footer() { | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</footer> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,48 +1,43 @@ | ||||
| "use client"; | ||||
| "use client" | ||||
|  | ||||
| import { IconSubmissionForm } from "@/components/icon-submission-form"; | ||||
| import { ThemeSwitcher } from "@/components/theme-switcher"; | ||||
| import { REPO_PATH } from "@/constants"; | ||||
| import { getIconsArray } from "@/lib/api"; | ||||
| import type { IconWithName } from "@/types/icons"; | ||||
| import { motion } from "framer-motion"; | ||||
| 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"; | ||||
| import { IconSubmissionForm } from "@/components/icon-submission-form" | ||||
| import { ThemeSwitcher } from "@/components/theme-switcher" | ||||
| import { REPO_PATH } from "@/constants" | ||||
| import { getIconsArray } from "@/lib/api" | ||||
| import type { IconWithName } from "@/types/icons" | ||||
| import { motion } from "framer-motion" | ||||
| 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<IconWithName[]>([]); | ||||
| 	const [isLoaded, setIsLoaded] = useState(false); | ||||
| 	const [commandMenuOpen, setCommandMenuOpen] = useState(false); | ||||
| 	const [iconsData, setIconsData] = useState<IconWithName[]>([]) | ||||
| 	const [isLoaded, setIsLoaded] = useState(false) | ||||
| 	const [commandMenuOpen, setCommandMenuOpen] = useState(false) | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		async function loadIcons() { | ||||
| 			try { | ||||
| 				const icons = await getIconsArray(); | ||||
| 				setIconsData(icons); | ||||
| 				setIsLoaded(true); | ||||
| 				const icons = await getIconsArray() | ||||
| 				setIconsData(icons) | ||||
| 				setIsLoaded(true) | ||||
| 			} catch (error) { | ||||
| 				console.error("Failed to load icons:", error); | ||||
| 				setIsLoaded(true); | ||||
| 				console.error("Failed to load icons:", error) | ||||
| 				setIsLoaded(true) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		loadIcons(); | ||||
| 	}, []); | ||||
| 		loadIcons() | ||||
| 	}, []) | ||||
|  | ||||
| 	// Function to open the command menu | ||||
| 	const openCommandMenu = () => { | ||||
| 		setCommandMenuOpen(true); | ||||
| 	}; | ||||
| 		setCommandMenuOpen(true) | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<motion.header | ||||
| @@ -53,13 +48,8 @@ export function Header() { | ||||
| 		> | ||||
| 			<div className="px-4 md:px-12 flex items-center justify-between h-16 md:h-18"> | ||||
| 				<div className="flex items-center gap-2 md:gap-6"> | ||||
| 					<Link | ||||
| 						href="/" | ||||
| 						className="text-lg md:text-xl font-bold group hidden md:block" | ||||
| 					> | ||||
| 						<span className="transition-colors duration-300 group-hover:"> | ||||
| 							Dashboard Icons | ||||
| 						</span> | ||||
| 					<Link href="/" className="text-lg md:text-xl font-bold group hidden md:block"> | ||||
| 						<span className="transition-colors duration-300 group-hover:">Dashboard Icons</span> | ||||
| 					</Link> | ||||
| 					<div className="flex-nowrap"> | ||||
| 						<HeaderNav /> | ||||
| @@ -122,13 +112,7 @@ export function Header() { | ||||
| 			</div> | ||||
|  | ||||
| 			{/* Single instance of CommandMenu */} | ||||
| 			{isLoaded && ( | ||||
| 				<CommandMenu | ||||
| 					icons={iconsData} | ||||
| 					open={commandMenuOpen} | ||||
| 					onOpenChange={setCommandMenuOpen} | ||||
| 				/> | ||||
| 			)} | ||||
| 			{isLoaded && <CommandMenu icons={iconsData} open={commandMenuOpen} onOpenChange={setCommandMenuOpen} />} | ||||
| 		</motion.header> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,20 +1,20 @@ | ||||
| "use client"; | ||||
| "use client" | ||||
|  | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { Card } from "@/components/ui/card"; | ||||
| import { Input } from "@/components/ui/input"; | ||||
| import { cn } from "@/lib/utils"; | ||||
| import { motion, useAnimation, useInView } from "framer-motion"; | ||||
| import { Github, Heart, Search } from "lucide-react"; | ||||
| import Link from "next/link"; | ||||
| import { useEffect, useRef, useState } from "react"; | ||||
| import { AnimatedShinyText } from "./magicui/animated-shiny-text"; | ||||
| import { AuroraText } from "./magicui/aurora-text"; | ||||
| import { InteractiveHoverButton } from "./magicui/interactive-hover-button"; | ||||
| import { Button } from "@/components/ui/button" | ||||
| import { Card } from "@/components/ui/card" | ||||
| import { Input } from "@/components/ui/input" | ||||
| import { cn } from "@/lib/utils" | ||||
| import { motion, useAnimation, useInView } from "framer-motion" | ||||
| import { Github, Heart, Search } from "lucide-react" | ||||
| import Link from "next/link" | ||||
| import { useEffect, useRef, useState } from "react" | ||||
| import { AnimatedShinyText } from "./magicui/animated-shiny-text" | ||||
| import { AuroraText } from "./magicui/aurora-text" | ||||
| import { InteractiveHoverButton } from "./magicui/interactive-hover-button" | ||||
|  | ||||
| interface IconCardProps { | ||||
| 	name: string; | ||||
| 	imageUrl: string; | ||||
| 	name: string | ||||
| 	imageUrl: string | ||||
| } | ||||
|  | ||||
| function IconCard({ name, imageUrl }: IconCardProps) { | ||||
| @@ -23,11 +23,9 @@ function IconCard({ name, imageUrl }: IconCardProps) { | ||||
| 			<div className="w-16 h-16 flex items-center justify-center"> | ||||
| 				<img src={imageUrl} alt={name} className="max-w-full max-h-full" /> | ||||
| 			</div> | ||||
| 			<p className="text-sm text-center text-muted-foreground group-hover:text-foreground transition-colors"> | ||||
| 				{name} | ||||
| 			</p> | ||||
| 			<p className="text-sm text-center text-muted-foreground group-hover:text-foreground transition-colors">{name}</p> | ||||
| 		</Card> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|  | ||||
| function ElegantShape({ | ||||
| @@ -40,28 +38,28 @@ function ElegantShape({ | ||||
| 	mobileWidth, | ||||
| 	mobileHeight, | ||||
| }: { | ||||
| 	className?: string; | ||||
| 	delay?: number; | ||||
| 	width?: number; | ||||
| 	height?: number; | ||||
| 	rotate?: number; | ||||
| 	gradient?: string; | ||||
| 	mobileWidth?: number; | ||||
| 	mobileHeight?: number; | ||||
| 	className?: string | ||||
| 	delay?: number | ||||
| 	width?: number | ||||
| 	height?: number | ||||
| 	rotate?: number | ||||
| 	gradient?: string | ||||
| 	mobileWidth?: number | ||||
| 	mobileHeight?: number | ||||
| }) { | ||||
| 	const controls = useAnimation(); | ||||
| 	const [isMobile, setIsMobile] = useState(false); | ||||
| 	const ref = useRef(null); | ||||
| 	const isInView = useInView(ref, { once: true, amount: 0.1 }); | ||||
| 	const controls = useAnimation() | ||||
| 	const [isMobile, setIsMobile] = useState(false) | ||||
| 	const ref = useRef(null) | ||||
| 	const isInView = useInView(ref, { once: true, amount: 0.1 }) | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		const checkMobile = () => { | ||||
| 			setIsMobile(window.innerWidth < 768); | ||||
| 		}; | ||||
| 		checkMobile(); | ||||
| 		window.addEventListener("resize", checkMobile); | ||||
| 		return () => window.removeEventListener("resize", checkMobile); | ||||
| 	}, []); | ||||
| 			setIsMobile(window.innerWidth < 768) | ||||
| 		} | ||||
| 		checkMobile() | ||||
| 		window.addEventListener("resize", checkMobile) | ||||
| 		return () => window.removeEventListener("resize", checkMobile) | ||||
| 	}, []) | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		if (isInView) { | ||||
| @@ -78,9 +76,9 @@ function ElegantShape({ | ||||
| 					ease: [0.23, 0.86, 0.39, 0.96], | ||||
| 					opacity: { duration: 1.2 }, | ||||
| 				}, | ||||
| 			}); | ||||
| 			}) | ||||
| 		} | ||||
| 	}, [controls, delay, isInView, rotate]); | ||||
| 	}, [controls, delay, isInView, rotate]) | ||||
|  | ||||
| 	return ( | ||||
| 		<motion.div | ||||
| @@ -122,11 +120,11 @@ function ElegantShape({ | ||||
| 				/> | ||||
| 			</motion.div> | ||||
| 		</motion.div> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|  | ||||
| export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
| 	const [searchQuery, setSearchQuery] = useState(""); | ||||
| 	const [searchQuery, setSearchQuery] = useState("") | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="relative my-20 w-full flex items-center justify-center overflow-hidden"> | ||||
| @@ -191,40 +189,26 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
|  | ||||
| 			<div className="relative z-10 container mx-auto px-4 md:px-6"> | ||||
| 				<div className="max-w-4xl mx-auto text-center flex flex-col gap-4"> | ||||
| 					<Link | ||||
| 						prefetch | ||||
| 						href="https://github.com/homarr-labs" | ||||
| 						target="_blank" | ||||
| 						rel="noopener noreferrer" | ||||
| 						className="mx-auto" | ||||
| 					> | ||||
| 					<Link prefetch href="https://github.com/homarr-labs" target="_blank" rel="noopener noreferrer" className="mx-auto"> | ||||
| 						<Card className="group p-2 px-4 flex flex-row items-center gap-2 border-2  z-10 relative glass-effect motion-safe:motion-preset-slide-up motion-duration-1500 hover:scale-105 transition-all duration-300"> | ||||
| 							<Heart | ||||
| 								// Filled when hovered | ||||
| 								className="h-4 w-4 text-primary group-hover:fill-primary transition-all duration-300" | ||||
| 							/> | ||||
| 							<span className="text-sm text-foreground/70 tracking-wide"> | ||||
| 								Made with love by Homarr Labs | ||||
| 							</span> | ||||
| 							<span className="text-sm text-foreground/70 tracking-wide">Made with love by Homarr Labs</span> | ||||
| 						</Card> | ||||
| 					</Link> | ||||
|  | ||||
| 					<h1 className="text-3xl sm:text-5xl md:text-7xl font-bold mb-4 md:mb-8 tracking-tight motion-safe:motion-preset-slide-up motion-duration-1500"> | ||||
| 						Your definitive source for | ||||
| 						<br /> | ||||
| 						<AuroraText colors={["#FA5352", "#FA5352", "orange"]}> | ||||
| 							dashboard icons | ||||
| 						</AuroraText> | ||||
| 						<AuroraText colors={["#FA5352", "#FA5352", "orange"]}>dashboard icons</AuroraText> | ||||
| 					</h1> | ||||
|  | ||||
| 					<motion.div | ||||
| 						custom={2} | ||||
| 						className="motion-safe:motion-preset-slide-up motion-duration-1500" | ||||
| 					> | ||||
| 					<motion.div custom={2} className="motion-safe:motion-preset-slide-up motion-duration-1500"> | ||||
| 						<p className="text-sm sm:text-base md:text-xl text-muted-foreground mb-6 md:mb-8 leading-relaxed font-light tracking-wide max-w-2xl mx-auto px-4"> | ||||
| 							A collection of <span className="font-medium ">{totalIcons}</span>{" "} | ||||
| 							curated icons for services, applications and tools, designed | ||||
| 							specifically for dashboards and app directories. | ||||
| 							A collection of <span className="font-medium ">{totalIcons}</span> curated icons for services, applications and tools, | ||||
| 							designed specifically for dashboards and app directories. | ||||
| 						</p> | ||||
| 					</motion.div> | ||||
|  | ||||
| @@ -232,11 +216,7 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
| 						custom={3} | ||||
| 						className="flex flex-col items-center gap-4 md:gap-6 mb-8 md:mb-12 motion-safe:motion-preset-slide-up motion-duration-1500" | ||||
| 					> | ||||
| 						<form | ||||
| 							action="/icons" | ||||
| 							method="GET" | ||||
| 							className="relative w-full max-w-md group" | ||||
| 						> | ||||
| 						<form action="/icons" method="GET" className="relative w-full max-w-md group"> | ||||
| 							<Input | ||||
| 								name="q" | ||||
| 								type="search" | ||||
| @@ -257,11 +237,7 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
| 							<Link href="/icons"> | ||||
| 								<InteractiveHoverButton className="rounded-md">Explore icons</InteractiveHoverButton> | ||||
| 							</Link> | ||||
| 							<Button | ||||
| 								variant="outline" | ||||
| 								className="h-9 md:h-10 px-4 gap-2 backdrop-blur-sm" | ||||
| 								asChild | ||||
| 							> | ||||
| 							<Button variant="outline" className="h-9 md:h-10 px-4 gap-2 backdrop-blur-sm" asChild> | ||||
| 								<Link | ||||
| 									href="https://github.com/homarr-labs/dashboard-icons" | ||||
| 									target="_blank" | ||||
| @@ -279,5 +255,5 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) { | ||||
|  | ||||
| 			<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-background/80 pointer-events-none" /> | ||||
| 		</div> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,73 +1,48 @@ | ||||
| "use client"; | ||||
| "use client" | ||||
|  | ||||
| import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { | ||||
| 	Card, | ||||
| 	CardContent, | ||||
| 	CardDescription, | ||||
| 	CardHeader, | ||||
| 	CardTitle, | ||||
| } from "@/components/ui/card"; | ||||
| import { | ||||
| 	Tooltip, | ||||
| 	TooltipContent, | ||||
| 	TooltipProvider, | ||||
| 	TooltipTrigger, | ||||
| } from "@/components/ui/tooltip"; | ||||
| import { BASE_URL, REPO_PATH } from "@/constants"; | ||||
| import type { AuthorData, Icon } 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 Image from "next/image"; | ||||
| import Link from "next/link"; | ||||
| import { useCallback, useState } from "react"; | ||||
| import { toast } from "sonner"; | ||||
| import { Carbon } from "./carbon"; | ||||
| import { MagicCard } from "./magicui/magic-card"; | ||||
| import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" | ||||
| import { Button } from "@/components/ui/button" | ||||
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" | ||||
| import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" | ||||
| import { BASE_URL, REPO_PATH } from "@/constants" | ||||
| import type { AuthorData, Icon } 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 Image from "next/image" | ||||
| import Link from "next/link" | ||||
| import { useCallback, useState } from "react" | ||||
| import { toast } from "sonner" | ||||
| import { Carbon } from "./carbon" | ||||
| import { MagicCard } from "./magicui/magic-card" | ||||
|  | ||||
| export type IconDetailsProps = { | ||||
| 	icon: string; | ||||
| 	iconData: Icon; | ||||
| 	authorData: AuthorData; | ||||
| }; | ||||
| 	icon: string | ||||
| 	iconData: Icon | ||||
| 	authorData: AuthorData | ||||
| } | ||||
|  | ||||
| export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 	const authorName = authorData.name || authorData.login || ""; | ||||
| 	const iconColorVariants = iconData.colors; | ||||
| 	const formattedDate = new Date(iconData.update.timestamp).toLocaleDateString( | ||||
| 		"en-GB", | ||||
| 		{ | ||||
| 			day: "numeric", | ||||
| 			month: "long", | ||||
| 			year: "numeric", | ||||
| 		}, | ||||
| 	); | ||||
| 	const authorName = authorData.name || authorData.login || "" | ||||
| 	const iconColorVariants = iconData.colors | ||||
| 	const formattedDate = new Date(iconData.update.timestamp).toLocaleDateString("en-GB", { | ||||
| 		day: "numeric", | ||||
| 		month: "long", | ||||
| 		year: "numeric", | ||||
| 	}) | ||||
| 	const getAvailableFormats = () => { | ||||
| 		switch (iconData.base) { | ||||
| 			case "svg": | ||||
| 				return ["svg", "png", "webp"]; | ||||
| 				return ["svg", "png", "webp"] | ||||
| 			case "png": | ||||
| 				return ["png", "webp"]; | ||||
| 				return ["png", "webp"] | ||||
| 			default: | ||||
| 				return [iconData.base]; | ||||
| 				return [iconData.base] | ||||
| 		} | ||||
| 	}; | ||||
| 	} | ||||
|  | ||||
| 	const availableFormats = getAvailableFormats(); | ||||
| 	const [copiedVariants, setCopiedVariants] = useState<Record<string, boolean>>( | ||||
| 		{}, | ||||
| 	); | ||||
| 	const availableFormats = getAvailableFormats() | ||||
| 	const [copiedVariants, setCopiedVariants] = useState<Record<string, boolean>>({}) | ||||
|  | ||||
| 	// Launch confetti from the pointer position | ||||
| 	const launchConfetti = useCallback((originX?: number, originY?: number) => { | ||||
| @@ -77,15 +52,8 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 			ticks: 50, | ||||
| 			zIndex: 0, | ||||
| 			disableForReducedMotion: true, | ||||
| 			colors: [ | ||||
| 				"#ff0a54", | ||||
| 				"#ff477e", | ||||
| 				"#ff7096", | ||||
| 				"#ff85a1", | ||||
| 				"#fbb1bd", | ||||
| 				"#f9bec7", | ||||
| 			], | ||||
| 		}; | ||||
| 			colors: ["#ff0a54", "#ff477e", "#ff7096", "#ff85a1", "#fbb1bd", "#f9bec7"], | ||||
| 		} | ||||
|  | ||||
| 		// If we have origin coordinates, use them | ||||
| 		if (originX !== undefined && originY !== undefined) { | ||||
| @@ -96,103 +64,87 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 					x: originX / window.innerWidth, | ||||
| 					y: originY / window.innerHeight, | ||||
| 				}, | ||||
| 			}); | ||||
| 			}) | ||||
| 		} else { | ||||
| 			// Default to center of screen | ||||
| 			confetti({ | ||||
| 				...defaults, | ||||
| 				particleCount: 50, | ||||
| 				origin: { x: 0.5, y: 0.5 }, | ||||
| 			}); | ||||
| 			}) | ||||
| 		} | ||||
| 	}, []); | ||||
| 	}, []) | ||||
|  | ||||
| 	const handleCopy = ( | ||||
| 		url: string, | ||||
| 		variantKey: string, | ||||
| 		event?: React.MouseEvent, | ||||
| 	) => { | ||||
| 		navigator.clipboard.writeText(url); | ||||
| 	const handleCopy = (url: string, variantKey: string, event?: React.MouseEvent) => { | ||||
| 		navigator.clipboard.writeText(url) | ||||
| 		setCopiedVariants((prev) => ({ | ||||
| 			...prev, | ||||
| 			[variantKey]: true, | ||||
| 		})); | ||||
| 		})) | ||||
| 		setTimeout(() => { | ||||
| 			setCopiedVariants((prev) => ({ | ||||
| 				...prev, | ||||
| 				[variantKey]: false, | ||||
| 			})); | ||||
| 		}, 2000); | ||||
| 			})) | ||||
| 		}, 2000) | ||||
|  | ||||
| 		// Launch confetti from click position or center of screen | ||||
| 		if (event) { | ||||
| 			launchConfetti(event.clientX, event.clientY); | ||||
| 			launchConfetti(event.clientX, event.clientY) | ||||
| 		} else { | ||||
| 			launchConfetti(); | ||||
| 			launchConfetti() | ||||
| 		} | ||||
|  | ||||
| 		toast.success("URL copied", { | ||||
| 			description: | ||||
| 				"The icon URL has been copied to your clipboard. Ready to use!", | ||||
| 		}); | ||||
| 	}; | ||||
| 			description: "The icon URL has been copied to your clipboard. Ready to use!", | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	const handleDownload = async ( | ||||
| 		event: React.MouseEvent, | ||||
| 		url: string, | ||||
| 		filename: string, | ||||
| 	) => { | ||||
| 		event.preventDefault(); | ||||
| 	const handleDownload = async (event: React.MouseEvent, url: string, filename: string) => { | ||||
| 		event.preventDefault() | ||||
|  | ||||
| 		// Launch confetti from download button position | ||||
| 		launchConfetti(event.clientX, event.clientY); | ||||
| 		launchConfetti(event.clientX, event.clientY) | ||||
|  | ||||
| 		try { | ||||
| 			// Show loading toast | ||||
| 			toast.loading("Preparing download..."); | ||||
| 			toast.loading("Preparing download...") | ||||
|  | ||||
| 			// Fetch the file first as a blob | ||||
| 			const response = await fetch(url); | ||||
| 			const blob = await response.blob(); | ||||
| 			const response = await fetch(url) | ||||
| 			const blob = await response.blob() | ||||
|  | ||||
| 			// Create a blob URL and use it for download | ||||
| 			const blobUrl = URL.createObjectURL(blob); | ||||
| 			const link = document.createElement("a"); | ||||
| 			link.href = blobUrl; | ||||
| 			link.download = filename; | ||||
| 			document.body.appendChild(link); | ||||
| 			link.click(); | ||||
| 			const blobUrl = URL.createObjectURL(blob) | ||||
| 			const link = document.createElement("a") | ||||
| 			link.href = blobUrl | ||||
| 			link.download = filename | ||||
| 			document.body.appendChild(link) | ||||
| 			link.click() | ||||
|  | ||||
| 			// Clean up | ||||
| 			document.body.removeChild(link); | ||||
| 			setTimeout(() => URL.revokeObjectURL(blobUrl), 100); | ||||
| 			document.body.removeChild(link) | ||||
| 			setTimeout(() => URL.revokeObjectURL(blobUrl), 100) | ||||
|  | ||||
| 			toast.dismiss(); | ||||
| 			toast.dismiss() | ||||
| 			toast.success("Download started", { | ||||
| 				description: | ||||
| 					"Your icon file is being downloaded and will be saved to your device.", | ||||
| 			}); | ||||
| 				description: "Your icon file is being downloaded and will be saved to your device.", | ||||
| 			}) | ||||
| 		} catch (error) { | ||||
| 			console.error("Download error:", error); | ||||
| 			toast.dismiss(); | ||||
| 			console.error("Download error:", error) | ||||
| 			toast.dismiss() | ||||
| 			toast.error("Download failed", { | ||||
| 				description: | ||||
| 					"There was an error downloading the file. Please try again.", | ||||
| 			}); | ||||
| 				description: "There was an error downloading the file. Please try again.", | ||||
| 			}) | ||||
| 		} | ||||
| 	}; | ||||
| 	} | ||||
|  | ||||
| 	const renderVariant = ( | ||||
| 		format: string, | ||||
| 		iconName: string, | ||||
| 		theme?: "light" | "dark", | ||||
| 	) => { | ||||
| 		const variantName = | ||||
| 			theme && iconColorVariants?.[theme] ? iconColorVariants[theme] : iconName; | ||||
| 		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 renderVariant = (format: string, iconName: string, theme?: "light" | "dark") => { | ||||
| 		const variantName = theme && iconColorVariants?.[theme] ? iconColorVariants[theme] : iconName | ||||
| 		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 | ||||
|  | ||||
| 		return ( | ||||
| 			<TooltipProvider key={variantKey} delayDuration={500}> | ||||
| @@ -252,9 +204,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 										variant="outline" | ||||
| 										size="icon" | ||||
| 										className="h-8 w-8 rounded-lg cursor-pointer" | ||||
| 										onClick={(e) => | ||||
| 											handleDownload(e, imageUrl, `${iconName}.${format}`) | ||||
| 										} | ||||
| 										onClick={(e) => handleDownload(e, imageUrl, `${iconName}.${format}`)} | ||||
| 									> | ||||
| 										<Download className="w-4 h-4" /> | ||||
| 									</Button> | ||||
| @@ -272,11 +222,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 										className="h-8 w-8 rounded-lg cursor-pointer" | ||||
| 										onClick={(e) => handleCopy(imageUrl, `btn-${variantKey}`, e)} | ||||
| 									> | ||||
| 										{copiedVariants[`btn-${variantKey}`] ? ( | ||||
| 											<Check className="w-4 h-4 text-green-500" /> | ||||
| 										) : ( | ||||
| 											<Copy className="w-4 h-4" /> | ||||
| 										)} | ||||
| 										{copiedVariants[`btn-${variantKey}`] ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />} | ||||
| 									</Button> | ||||
| 								</TooltipTrigger> | ||||
| 								<TooltipContent> | ||||
| @@ -286,17 +232,8 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
|  | ||||
| 							<Tooltip> | ||||
| 								<TooltipTrigger asChild> | ||||
| 									<Button | ||||
| 										variant="outline" | ||||
| 										size="icon" | ||||
| 										className="h-8 w-8 rounded-lg" | ||||
| 										asChild | ||||
| 									> | ||||
| 										<Link | ||||
| 											href={githubUrl} | ||||
| 											target="_blank" | ||||
| 											rel="noopener noreferrer" | ||||
| 										> | ||||
| 									<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg" asChild> | ||||
| 										<Link href={githubUrl} target="_blank" rel="noopener noreferrer"> | ||||
| 											<Github className="w-4 h-4" /> | ||||
| 										</Link> | ||||
| 									</Button> | ||||
| @@ -309,8 +246,8 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 					</div> | ||||
| 				</MagicCard> | ||||
| 			</TooltipProvider> | ||||
| 		); | ||||
| 	}; | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="container mx-auto px-4 py-8"> | ||||
| @@ -329,9 +266,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 										className="w-full h-full object-contain" | ||||
| 									/> | ||||
| 								</div> | ||||
| 								<CardTitle className="text-2xl font-bold capitalize text-center mb-2"> | ||||
| 									{icon} | ||||
| 								</CardTitle> | ||||
| 								<CardTitle className="text-2xl font-bold capitalize text-center mb-2">{icon}</CardTitle> | ||||
| 							</div> | ||||
| 						</CardHeader> | ||||
| 						<CardContent> | ||||
| @@ -340,23 +275,15 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 									<div className="space-y-2"> | ||||
| 										<div className="flex items-center gap-2"> | ||||
| 											<p className="text-sm"> | ||||
| 												<span className="font-medium">Updated on:</span>{" "} | ||||
| 												{formattedDate} | ||||
| 												<span className="font-medium">Updated on:</span> {formattedDate} | ||||
| 											</p> | ||||
| 										</div> | ||||
| 										<div className="flex items-center gap-2"> | ||||
| 											<div className="flex items-center gap-2"> | ||||
| 												<p className="text-sm font-medium">By:</p> | ||||
| 												<Avatar className="h-5 w-5 border"> | ||||
| 													<AvatarImage | ||||
| 														src={authorData.avatar_url} | ||||
| 														alt={authorName} | ||||
| 													/> | ||||
| 													<AvatarFallback> | ||||
| 														{authorName | ||||
| 															? authorName.slice(0, 2).toUpperCase() | ||||
| 															: "??"} | ||||
| 													</AvatarFallback> | ||||
| 													<AvatarImage src={authorData.avatar_url} alt={authorName} /> | ||||
| 													<AvatarFallback>{authorName ? authorName.slice(0, 2).toUpperCase() : "??"}</AvatarFallback> | ||||
| 												</Avatar> | ||||
| 												{authorData.html_url ? ( | ||||
| 													<Link | ||||
| @@ -377,9 +304,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
|  | ||||
| 								{iconData.categories && iconData.categories.length > 0 && ( | ||||
| 									<div className="space-y-3"> | ||||
| 										<h3 className="text-sm font-semibold text-muted-foreground"> | ||||
| 											Categories | ||||
| 										</h3> | ||||
| 										<h3 className="text-sm font-semibold text-muted-foreground">Categories</h3> | ||||
| 										<div className="flex flex-wrap gap-2"> | ||||
| 											{iconData.categories.map((category) => ( | ||||
| 												<Link | ||||
| @@ -389,10 +314,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 												> | ||||
| 													{category | ||||
| 														.split("-") | ||||
| 														.map( | ||||
| 															(word) => | ||||
| 																word.charAt(0).toUpperCase() + word.slice(1), | ||||
| 														) | ||||
| 														.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) | ||||
| 														.join(" ")} | ||||
| 												</Link> | ||||
| 											))} | ||||
| @@ -402,9 +324,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
|  | ||||
| 								{iconData.aliases && iconData.aliases.length > 0 && ( | ||||
| 									<div className="space-y-3"> | ||||
| 										<h3 className="text-sm font-semibold text-muted-foreground"> | ||||
| 											Aliases | ||||
| 										</h3> | ||||
| 										<h3 className="text-sm font-semibold text-muted-foreground">Aliases</h3> | ||||
| 										<div className="flex flex-wrap gap-2"> | ||||
| 											{iconData.aliases.map((alias) => ( | ||||
| 												<span | ||||
| @@ -416,10 +336,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 												</span> | ||||
| 											))} | ||||
| 										</div> | ||||
| 										<p className="text-[10px] text-muted-foreground mt-1"> | ||||
| 											These aliases can be used to find this icon in search | ||||
| 											results. | ||||
| 										</p> | ||||
| 										<p className="text-[10px] text-muted-foreground mt-1">These aliases can be used to find this icon in search results.</p> | ||||
| 									</div> | ||||
| 								)} | ||||
| 							</div> | ||||
| @@ -432,16 +349,12 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 					<Card className="h-full backdrop-blur-sm bg-card/50 shadow-lg"> | ||||
| 						<CardHeader> | ||||
| 							<CardTitle>Icon variants</CardTitle> | ||||
| 							<CardDescription> | ||||
| 								Click on any icon to copy its URL to your clipboard | ||||
| 							</CardDescription> | ||||
| 							<CardDescription>Click on any icon to copy its URL to your clipboard</CardDescription> | ||||
| 						</CardHeader> | ||||
| 						<CardContent> | ||||
| 							{!iconData.colors ? ( | ||||
| 								<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | ||||
| 									{availableFormats.map((format) => | ||||
| 										renderVariant(format, icon), | ||||
| 									)} | ||||
| 									{availableFormats.map((format) => renderVariant(format, icon))} | ||||
| 								</div> | ||||
| 							) : ( | ||||
| 								<div className="space-y-10"> | ||||
| @@ -451,9 +364,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 											Light theme | ||||
| 										</h3> | ||||
| 										<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg "> | ||||
| 											{availableFormats.map((format) => | ||||
| 												renderVariant(format, icon, "light"), | ||||
| 											)} | ||||
| 											{availableFormats.map((format) => renderVariant(format, icon, "light"))} | ||||
| 										</div> | ||||
| 									</div> | ||||
| 									<div> | ||||
| @@ -462,9 +373,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 											Dark theme | ||||
| 										</h3> | ||||
| 										<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg "> | ||||
| 											{availableFormats.map((format) => | ||||
| 												renderVariant(format, icon, "dark"), | ||||
| 											)} | ||||
| 											{availableFormats.map((format) => renderVariant(format, icon, "dark"))} | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								</div> | ||||
| @@ -482,27 +391,18 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 						<CardContent> | ||||
| 							<div className="space-y-6"> | ||||
| 								<div className="space-y-3"> | ||||
| 									<h3 className="text-sm font-semibold text-muted-foreground"> | ||||
| 										Base format | ||||
| 									</h3> | ||||
| 									<h3 className="text-sm font-semibold text-muted-foreground">Base format</h3> | ||||
| 									<div className="flex items-center gap-2"> | ||||
| 										<FileType className="w-4 h-4 text-blue-500" /> | ||||
| 										<div className="px-3 py-1.5  border border-border rounded-lg text-sm font-medium"> | ||||
| 											{iconData.base.toUpperCase()} | ||||
| 										</div> | ||||
| 										<div className="px-3 py-1.5  border border-border rounded-lg text-sm font-medium">{iconData.base.toUpperCase()}</div> | ||||
| 									</div> | ||||
| 								</div> | ||||
|  | ||||
| 								<div className="space-y-3"> | ||||
| 									<h3 className="text-sm font-semibold text-muted-foreground"> | ||||
| 										Available formats | ||||
| 									</h3> | ||||
| 									<h3 className="text-sm font-semibold text-muted-foreground">Available formats</h3> | ||||
| 									<div className="flex flex-wrap gap-2"> | ||||
| 										{availableFormats.map((format) => ( | ||||
| 											<div | ||||
| 												key={format} | ||||
| 												className="px-3 py-1.5  border border-border rounded-lg text-xs font-medium" | ||||
| 											> | ||||
| 											<div key={format} className="px-3 py-1.5  border border-border rounded-lg text-xs font-medium"> | ||||
| 												{format.toUpperCase()} | ||||
| 											</div> | ||||
| 										))} | ||||
| @@ -511,37 +411,23 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
|  | ||||
| 								{iconData.colors && ( | ||||
| 									<div className="space-y-3"> | ||||
| 										<h3 className="text-sm font-semibold text-muted-foreground"> | ||||
| 											Color variants | ||||
| 										</h3> | ||||
| 										<h3 className="text-sm font-semibold text-muted-foreground">Color variants</h3> | ||||
| 										<div className="space-y-2"> | ||||
| 											{Object.entries(iconData.colors).map( | ||||
| 												([theme, variant]) => ( | ||||
| 													<div key={theme} className="flex items-center gap-2"> | ||||
| 														<PaletteIcon className="w-4 h-4 text-purple-500" /> | ||||
| 														<span className="capitalize font-medium text-sm"> | ||||
| 															{theme}: | ||||
| 														</span> | ||||
| 														<code className=" border border-border px-2 py-0.5 rounded-lg text-xs"> | ||||
| 															{variant} | ||||
| 														</code> | ||||
| 													</div> | ||||
| 												), | ||||
| 											)} | ||||
| 											{Object.entries(iconData.colors).map(([theme, variant]) => ( | ||||
| 												<div key={theme} className="flex items-center gap-2"> | ||||
| 													<PaletteIcon className="w-4 h-4 text-purple-500" /> | ||||
| 													<span className="capitalize font-medium text-sm">{theme}:</span> | ||||
| 													<code className=" border border-border px-2 py-0.5 rounded-lg text-xs">{variant}</code> | ||||
| 												</div> | ||||
| 											))} | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								)} | ||||
|  | ||||
| 								<div className="space-y-3"> | ||||
| 									<h3 className="text-sm font-semibold text-muted-foreground"> | ||||
| 										Source | ||||
| 									</h3> | ||||
| 									<h3 className="text-sm font-semibold text-muted-foreground">Source</h3> | ||||
| 									<Button variant="outline" className="w-full" asChild> | ||||
| 										<Link | ||||
| 											href={`${REPO_PATH}/blob/main/meta/${icon}.json`} | ||||
| 											target="_blank" | ||||
| 											rel="noopener noreferrer" | ||||
| 										> | ||||
| 										<Link href={`${REPO_PATH}/blob/main/meta/${icon}.json`} target="_blank" rel="noopener noreferrer"> | ||||
| 											<Github className="w-4 h-4 mr-2" /> | ||||
| 											View on GitHub | ||||
| 										</Link> | ||||
| @@ -554,5 +440,5 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,39 +1,33 @@ | ||||
| import { ComponentPropsWithoutRef, CSSProperties, FC } from "react"; | ||||
| import type { CSSProperties, ComponentPropsWithoutRef, FC } from "react" | ||||
|  | ||||
| import { cn } from "@/lib/utils"; | ||||
| import { cn } from "@/lib/utils" | ||||
|  | ||||
| export interface AnimatedShinyTextProps | ||||
|   extends ComponentPropsWithoutRef<"span"> { | ||||
|   shimmerWidth?: number; | ||||
| export interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<"span"> { | ||||
| 	shimmerWidth?: number | ||||
| } | ||||
|  | ||||
| export const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({ | ||||
|   children, | ||||
|   className, | ||||
|   shimmerWidth = 100, | ||||
|   ...props | ||||
| }) => { | ||||
|   return ( | ||||
|     <span | ||||
|       style={ | ||||
|         { | ||||
|           "--shiny-width": `${shimmerWidth}px`, | ||||
|         } as CSSProperties | ||||
|       } | ||||
|       className={cn( | ||||
|         "mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70", | ||||
| export const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({ children, className, shimmerWidth = 100, ...props }) => { | ||||
| 	return ( | ||||
| 		<span | ||||
| 			style={ | ||||
| 				{ | ||||
| 					"--shiny-width": `${shimmerWidth}px`, | ||||
| 				} as CSSProperties | ||||
| 			} | ||||
| 			className={cn( | ||||
| 				"mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70", | ||||
|  | ||||
|         // Shine effect | ||||
|         "animate-shiny-text bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shiny-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]", | ||||
| 				// Shine effect | ||||
| 				"animate-shiny-text bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shiny-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]", | ||||
|  | ||||
|         // Shine gradient | ||||
|         "bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent  dark:via-white/80", | ||||
| 				// Shine gradient | ||||
| 				"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent  dark:via-white/80", | ||||
|  | ||||
|         className, | ||||
|       )} | ||||
|       {...props} | ||||
|     > | ||||
|       {children} | ||||
|     </span> | ||||
|   ); | ||||
| }; | ||||
| 				className, | ||||
| 			)} | ||||
| 			{...props} | ||||
| 		> | ||||
| 			{children} | ||||
| 		</span> | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,43 +1,37 @@ | ||||
| "use client"; | ||||
| "use client" | ||||
|  | ||||
| import React, { memo } from "react"; | ||||
| import type React from "react" | ||||
| import { memo } from "react" | ||||
|  | ||||
| interface AuroraTextProps { | ||||
|   children: React.ReactNode; | ||||
|   className?: string; | ||||
|   colors?: string[]; | ||||
|   speed?: number; | ||||
| 	children: React.ReactNode | ||||
| 	className?: string | ||||
| 	colors?: string[] | ||||
| 	speed?: number | ||||
| } | ||||
|  | ||||
| export const AuroraText = memo( | ||||
|   ({ | ||||
|     children, | ||||
|     className = "", | ||||
|     colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"], | ||||
|     speed = 1, | ||||
|   }: AuroraTextProps) => { | ||||
|     const gradientStyle = { | ||||
|       backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${ | ||||
|         colors[0] | ||||
|       })`, | ||||
|       WebkitBackgroundClip: "text", | ||||
|       WebkitTextFillColor: "transparent", | ||||
|       animationDuration: `${10 / speed}s`, | ||||
|     }; | ||||
| 	({ children, className = "", colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"], speed = 1 }: AuroraTextProps) => { | ||||
| 		const gradientStyle = { | ||||
| 			backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${colors[0]})`, | ||||
| 			WebkitBackgroundClip: "text", | ||||
| 			WebkitTextFillColor: "transparent", | ||||
| 			animationDuration: `${10 / speed}s`, | ||||
| 		} | ||||
|  | ||||
|     return ( | ||||
|       <span className={`relative inline-block ${className}`}> | ||||
|         <span className="sr-only">{children}</span> | ||||
|         <span | ||||
|           className="relative animate-aurora bg-[length:200%_auto] bg-clip-text text-transparent" | ||||
|           style={gradientStyle} | ||||
|           aria-hidden="true" | ||||
|         > | ||||
|           {children} | ||||
|         </span> | ||||
|       </span> | ||||
|     ); | ||||
|   }, | ||||
| ); | ||||
| 		return ( | ||||
| 			<span className={`relative inline-block ${className}`}> | ||||
| 				<span className="sr-only">{children}</span> | ||||
| 				<span | ||||
| 					className="relative animate-aurora bg-[length:200%_auto] bg-clip-text text-transparent" | ||||
| 					style={gradientStyle} | ||||
| 					aria-hidden="true" | ||||
| 				> | ||||
| 					{children} | ||||
| 				</span> | ||||
| 			</span> | ||||
| 		) | ||||
| 	}, | ||||
| ) | ||||
|  | ||||
| AuroraText.displayName = "AuroraText"; | ||||
| AuroraText.displayName = "AuroraText" | ||||
|   | ||||
| @@ -1,35 +1,31 @@ | ||||
| import React from "react"; | ||||
| import { ArrowRight } from "lucide-react"; | ||||
| import { cn } from "@/lib/utils"; | ||||
| import { cn } from "@/lib/utils" | ||||
| import { ArrowRight } from "lucide-react" | ||||
| import React from "react" | ||||
|  | ||||
| interface InteractiveHoverButtonProps | ||||
| 	extends React.ButtonHTMLAttributes<HTMLButtonElement> {} | ||||
| interface InteractiveHoverButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {} | ||||
|  | ||||
| export const InteractiveHoverButton = React.forwardRef< | ||||
| 	HTMLButtonElement, | ||||
| 	InteractiveHoverButtonProps | ||||
| >(({ children, className, ...props }, ref) => { | ||||
| 	return ( | ||||
| 		<button | ||||
| 			ref={ref} | ||||
| 			className={cn( | ||||
| 				"group relative w-auto cursor-pointer overflow-hidden rounded-full border bg-background p-2 px-6 text-center font-semibold", | ||||
| 				className, | ||||
| 			)} | ||||
| 			{...props} | ||||
| 		> | ||||
| 			<div className="flex items-center gap-2"> | ||||
| 				<div className="h-2 w-2 rounded-full bg-primary transition-all duration-300 group-hover:scale-[100.8]"></div> | ||||
| 				<span className="inline-block transition-all duration-300 group-hover:translate-x-12 group-hover:opacity-0"> | ||||
| 					{children} | ||||
| 				</span> | ||||
| 			</div> | ||||
| 			<div className="absolute top-0 z-10 flex h-full w-full translate-x-12 items-center justify-center gap-2 text-primary-foreground opacity-0 transition-all duration-300 group-hover:-translate-x-5 group-hover:opacity-100"> | ||||
| 				<span>{children}</span> | ||||
| 				<ArrowRight /> | ||||
| 			</div> | ||||
| 		</button> | ||||
| 	); | ||||
| }); | ||||
| export const InteractiveHoverButton = React.forwardRef<HTMLButtonElement, InteractiveHoverButtonProps>( | ||||
| 	({ children, className, ...props }, ref) => { | ||||
| 		return ( | ||||
| 			<button | ||||
| 				ref={ref} | ||||
| 				className={cn( | ||||
| 					"group relative w-auto cursor-pointer overflow-hidden rounded-full border bg-background p-2 px-6 text-center font-semibold", | ||||
| 					className, | ||||
| 				)} | ||||
| 				{...props} | ||||
| 			> | ||||
| 				<div className="flex items-center gap-2"> | ||||
| 					<div className="h-2 w-2 rounded-full bg-primary transition-all duration-300 group-hover:scale-[100.8]" /> | ||||
| 					<span className="inline-block transition-all duration-300 group-hover:translate-x-12 group-hover:opacity-0">{children}</span> | ||||
| 				</div> | ||||
| 				<div className="absolute top-0 z-10 flex h-full w-full translate-x-12 items-center justify-center gap-2 text-primary-foreground opacity-0 transition-all duration-300 group-hover:-translate-x-5 group-hover:opacity-100"> | ||||
| 					<span>{children}</span> | ||||
| 					<ArrowRight /> | ||||
| 				</div> | ||||
| 			</button> | ||||
| 		) | ||||
| 	}, | ||||
| ) | ||||
|  | ||||
| InteractiveHoverButton.displayName = "InteractiveHoverButton"; | ||||
| InteractiveHoverButton.displayName = "InteractiveHoverButton" | ||||
|   | ||||
| @@ -1,108 +1,106 @@ | ||||
| "use client"; | ||||
| "use client" | ||||
|  | ||||
| import { motion, useMotionTemplate, useMotionValue } from "motion/react"; | ||||
| import React, { useCallback, useEffect, useRef } from "react"; | ||||
| import { motion, useMotionTemplate, useMotionValue } from "motion/react" | ||||
| import type React from "react" | ||||
| import { useCallback, useEffect, useRef } from "react" | ||||
|  | ||||
| import { cn } from "@/lib/utils"; | ||||
| import { cn } from "@/lib/utils" | ||||
|  | ||||
| interface MagicCardProps { | ||||
|   children?: React.ReactNode; | ||||
|   className?: string; | ||||
|   gradientSize?: number; | ||||
|   gradientColor?: string; | ||||
|   gradientOpacity?: number; | ||||
|   gradientFrom?: string; | ||||
|   gradientTo?: string; | ||||
| 	children?: React.ReactNode | ||||
| 	className?: string | ||||
| 	gradientSize?: number | ||||
| 	gradientColor?: string | ||||
| 	gradientOpacity?: number | ||||
| 	gradientFrom?: string | ||||
| 	gradientTo?: string | ||||
| } | ||||
|  | ||||
| export function MagicCard({ | ||||
|   children, | ||||
|   className, | ||||
|   gradientSize = 200, | ||||
|   gradientColor = "", | ||||
|   gradientOpacity = 0.8, | ||||
|   gradientFrom = "#ff0a54", | ||||
|   gradientTo = "#f9bec7", | ||||
| 	children, | ||||
| 	className, | ||||
| 	gradientSize = 200, | ||||
| 	gradientColor = "", | ||||
| 	gradientOpacity = 0.8, | ||||
| 	gradientFrom = "#ff0a54", | ||||
| 	gradientTo = "#f9bec7", | ||||
| }: MagicCardProps) { | ||||
|   const cardRef = useRef<HTMLDivElement>(null); | ||||
|   const mouseX = useMotionValue(-gradientSize); | ||||
|   const mouseY = useMotionValue(-gradientSize); | ||||
| 	const cardRef = useRef<HTMLDivElement>(null) | ||||
| 	const mouseX = useMotionValue(-gradientSize) | ||||
| 	const mouseY = useMotionValue(-gradientSize) | ||||
|  | ||||
|   const handleMouseMove = useCallback( | ||||
|     (e: MouseEvent) => { | ||||
|       if (cardRef.current) { | ||||
|         const { left, top } = cardRef.current.getBoundingClientRect(); | ||||
|         const clientX = e.clientX; | ||||
|         const clientY = e.clientY; | ||||
|         mouseX.set(clientX - left); | ||||
|         mouseY.set(clientY - top); | ||||
|       } | ||||
|     }, | ||||
|     [mouseX, mouseY], | ||||
|   ); | ||||
| 	const handleMouseMove = useCallback( | ||||
| 		(e: MouseEvent) => { | ||||
| 			if (cardRef.current) { | ||||
| 				const { left, top } = cardRef.current.getBoundingClientRect() | ||||
| 				const clientX = e.clientX | ||||
| 				const clientY = e.clientY | ||||
| 				mouseX.set(clientX - left) | ||||
| 				mouseY.set(clientY - top) | ||||
| 			} | ||||
| 		}, | ||||
| 		[mouseX, mouseY], | ||||
| 	) | ||||
|  | ||||
|   const handleMouseOut = useCallback( | ||||
|     (e: MouseEvent) => { | ||||
|       if (!e.relatedTarget) { | ||||
|         document.removeEventListener("mousemove", handleMouseMove); | ||||
|         mouseX.set(-gradientSize); | ||||
|         mouseY.set(-gradientSize); | ||||
|       } | ||||
|     }, | ||||
|     [handleMouseMove, mouseX, gradientSize, mouseY], | ||||
|   ); | ||||
| 	const handleMouseOut = useCallback( | ||||
| 		(e: MouseEvent) => { | ||||
| 			if (!e.relatedTarget) { | ||||
| 				document.removeEventListener("mousemove", handleMouseMove) | ||||
| 				mouseX.set(-gradientSize) | ||||
| 				mouseY.set(-gradientSize) | ||||
| 			} | ||||
| 		}, | ||||
| 		[handleMouseMove, mouseX, gradientSize, mouseY], | ||||
| 	) | ||||
|  | ||||
|   const handleMouseEnter = useCallback(() => { | ||||
|     document.addEventListener("mousemove", handleMouseMove); | ||||
|     mouseX.set(-gradientSize); | ||||
|     mouseY.set(-gradientSize); | ||||
|   }, [handleMouseMove, mouseX, gradientSize, mouseY]); | ||||
| 	const handleMouseEnter = useCallback(() => { | ||||
| 		document.addEventListener("mousemove", handleMouseMove) | ||||
| 		mouseX.set(-gradientSize) | ||||
| 		mouseY.set(-gradientSize) | ||||
| 	}, [handleMouseMove, mouseX, gradientSize, mouseY]) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     document.addEventListener("mousemove", handleMouseMove); | ||||
|     document.addEventListener("mouseout", handleMouseOut); | ||||
|     document.addEventListener("mouseenter", handleMouseEnter); | ||||
| 	useEffect(() => { | ||||
| 		document.addEventListener("mousemove", handleMouseMove) | ||||
| 		document.addEventListener("mouseout", handleMouseOut) | ||||
| 		document.addEventListener("mouseenter", handleMouseEnter) | ||||
|  | ||||
|     return () => { | ||||
|       document.removeEventListener("mousemove", handleMouseMove); | ||||
|       document.removeEventListener("mouseout", handleMouseOut); | ||||
|       document.removeEventListener("mouseenter", handleMouseEnter); | ||||
|     }; | ||||
|   }, [handleMouseEnter, handleMouseMove, handleMouseOut]); | ||||
| 		return () => { | ||||
| 			document.removeEventListener("mousemove", handleMouseMove) | ||||
| 			document.removeEventListener("mouseout", handleMouseOut) | ||||
| 			document.removeEventListener("mouseenter", handleMouseEnter) | ||||
| 		} | ||||
| 	}, [handleMouseEnter, handleMouseMove, handleMouseOut]) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     mouseX.set(-gradientSize); | ||||
|     mouseY.set(-gradientSize); | ||||
|   }, [gradientSize, mouseX, mouseY]); | ||||
| 	useEffect(() => { | ||||
| 		mouseX.set(-gradientSize) | ||||
| 		mouseY.set(-gradientSize) | ||||
| 	}, [gradientSize, mouseX, mouseY]) | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       ref={cardRef} | ||||
|       className={cn("group relative rounded-[inherit]", className)} | ||||
|     > | ||||
|       <motion.div | ||||
|         className="pointer-events-none absolute inset-0 rounded-[inherit] bg-border duration-300 group-hover:opacity-100" | ||||
|         style={{ | ||||
|           background: useMotionTemplate` | ||||
| 	return ( | ||||
| 		<div ref={cardRef} className={cn("group relative rounded-[inherit]", className)}> | ||||
| 			<motion.div | ||||
| 				className="pointer-events-none absolute inset-0 rounded-[inherit] bg-border duration-300 group-hover:opacity-100" | ||||
| 				style={{ | ||||
| 					background: useMotionTemplate` | ||||
|           radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, | ||||
|           ${gradientFrom},  | ||||
|           ${gradientTo},  | ||||
|           var(--border) 100% | ||||
|           ) | ||||
|           `, | ||||
|         }} | ||||
|       /> | ||||
|       <div className="absolute inset-px rounded-[inherit] bg-background" /> | ||||
|       <motion.div | ||||
|         className="pointer-events-none absolute inset-px rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover:opacity-100" | ||||
|         style={{ | ||||
|           background: useMotionTemplate` | ||||
| 				}} | ||||
| 			/> | ||||
| 			<div className="absolute inset-px rounded-[inherit] bg-background" /> | ||||
| 			<motion.div | ||||
| 				className="pointer-events-none absolute inset-px rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover:opacity-100" | ||||
| 				style={{ | ||||
| 					background: useMotionTemplate` | ||||
|             radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, ${gradientColor}, transparent 100%) | ||||
|           `, | ||||
|           opacity: gradientOpacity, | ||||
|         }} | ||||
|       /> | ||||
|       <div className="relative">{children}</div> | ||||
|     </div> | ||||
|   ); | ||||
| 					opacity: gradientOpacity, | ||||
| 				}} | ||||
| 			/> | ||||
| 			<div className="relative">{children}</div> | ||||
| 		</div> | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,73 +1,73 @@ | ||||
| import { cn } from "@/lib/utils"; | ||||
| import { ComponentPropsWithoutRef } from "react"; | ||||
| import { cn } from "@/lib/utils" | ||||
| import type { ComponentPropsWithoutRef } from "react" | ||||
|  | ||||
| interface MarqueeProps extends ComponentPropsWithoutRef<"div"> { | ||||
|   /** | ||||
|    * Optional CSS class name to apply custom styles | ||||
|    */ | ||||
|   className?: string; | ||||
|   /** | ||||
|    * Whether to reverse the animation direction | ||||
|    * @default false | ||||
|    */ | ||||
|   reverse?: boolean; | ||||
|   /** | ||||
|    * Whether to pause the animation on hover | ||||
|    * @default false | ||||
|    */ | ||||
|   pauseOnHover?: boolean; | ||||
|   /** | ||||
|    * Content to be displayed in the marquee | ||||
|    */ | ||||
|   children: React.ReactNode; | ||||
|   /** | ||||
|    * Whether to animate vertically instead of horizontally | ||||
|    * @default false | ||||
|    */ | ||||
|   vertical?: boolean; | ||||
|   /** | ||||
|    * Number of times to repeat the content | ||||
|    * @default 4 | ||||
|    */ | ||||
|   repeat?: number; | ||||
| 	/** | ||||
| 	 * Optional CSS class name to apply custom styles | ||||
| 	 */ | ||||
| 	className?: string | ||||
| 	/** | ||||
| 	 * Whether to reverse the animation direction | ||||
| 	 * @default false | ||||
| 	 */ | ||||
| 	reverse?: boolean | ||||
| 	/** | ||||
| 	 * Whether to pause the animation on hover | ||||
| 	 * @default false | ||||
| 	 */ | ||||
| 	pauseOnHover?: boolean | ||||
| 	/** | ||||
| 	 * Content to be displayed in the marquee | ||||
| 	 */ | ||||
| 	children: React.ReactNode | ||||
| 	/** | ||||
| 	 * Whether to animate vertically instead of horizontally | ||||
| 	 * @default false | ||||
| 	 */ | ||||
| 	vertical?: boolean | ||||
| 	/** | ||||
| 	 * Number of times to repeat the content | ||||
| 	 * @default 4 | ||||
| 	 */ | ||||
| 	repeat?: number | ||||
| } | ||||
|  | ||||
| export function Marquee({ | ||||
|   className, | ||||
|   reverse = false, | ||||
|   pauseOnHover = false, | ||||
|   children, | ||||
|   vertical = false, | ||||
|   repeat = 4, | ||||
|   ...props | ||||
| 	className, | ||||
| 	reverse = false, | ||||
| 	pauseOnHover = false, | ||||
| 	children, | ||||
| 	vertical = false, | ||||
| 	repeat = 4, | ||||
| 	...props | ||||
| }: MarqueeProps) { | ||||
|   return ( | ||||
|     <div | ||||
|       {...props} | ||||
|       className={cn( | ||||
|         "group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]", | ||||
|         { | ||||
|           "flex-row": !vertical, | ||||
|           "flex-col": vertical, | ||||
|         }, | ||||
|         className, | ||||
|       )} | ||||
|     > | ||||
|       {Array(repeat) | ||||
|         .fill(0) | ||||
|         .map((_, i) => ( | ||||
|           <div | ||||
|             key={i} | ||||
|             className={cn("flex shrink-0 justify-around [gap:var(--gap)]", { | ||||
|               "animate-marquee flex-row": !vertical, | ||||
|               "animate-marquee-vertical flex-col": vertical, | ||||
|               "group-hover:[animation-play-state:paused]": pauseOnHover, | ||||
|               "[animation-direction:reverse]": reverse, | ||||
|             })} | ||||
|           > | ||||
|             {children} | ||||
|           </div> | ||||
|         ))} | ||||
|     </div> | ||||
|   ); | ||||
| 	return ( | ||||
| 		<div | ||||
| 			{...props} | ||||
| 			className={cn( | ||||
| 				"group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]", | ||||
| 				{ | ||||
| 					"flex-row": !vertical, | ||||
| 					"flex-col": vertical, | ||||
| 				}, | ||||
| 				className, | ||||
| 			)} | ||||
| 		> | ||||
| 			{Array(repeat) | ||||
| 				.fill(0) | ||||
| 				.map((_, i) => ( | ||||
| 					<div | ||||
| 						key={i} | ||||
| 						className={cn("flex shrink-0 justify-around [gap:var(--gap)]", { | ||||
| 							"animate-marquee flex-row": !vertical, | ||||
| 							"animate-marquee-vertical flex-col": vertical, | ||||
| 							"group-hover:[animation-play-state:paused]": pauseOnHover, | ||||
| 							"[animation-direction:reverse]": reverse, | ||||
| 						})} | ||||
| 					> | ||||
| 						{children} | ||||
| 					</div> | ||||
| 				))} | ||||
| 		</div> | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,37 +1,34 @@ | ||||
| "use client"; | ||||
| "use client" | ||||
|  | ||||
| import { Marquee } from "@/components/magicui/marquee"; | ||||
| import { BASE_URL } from "@/constants"; | ||||
| import { cn } from "@/lib/utils"; | ||||
| import type { Icon, IconWithName } from "@/types/icons"; | ||||
| import { format, isToday, isYesterday } from "date-fns"; | ||||
| import { ArrowRight, Clock, ExternalLink } from "lucide-react"; | ||||
| import Image from "next/image"; | ||||
| import Link from "next/link"; | ||||
| import { Marquee } from "@/components/magicui/marquee" | ||||
| import { BASE_URL } from "@/constants" | ||||
| import { cn } from "@/lib/utils" | ||||
| import type { Icon, IconWithName } from "@/types/icons" | ||||
| import { format, isToday, isYesterday } from "date-fns" | ||||
| import { ArrowRight, Clock, ExternalLink } from "lucide-react" | ||||
| import Image from "next/image" | ||||
| import Link from "next/link" | ||||
|  | ||||
| function formatIconDate(timestamp: string): string { | ||||
| 	const date = new Date(timestamp); | ||||
| 	const date = new Date(timestamp) | ||||
| 	if (isToday(date)) { | ||||
| 		return "Today"; | ||||
| 		return "Today" | ||||
| 	} | ||||
| 	if (isYesterday(date)) { | ||||
| 		return "Yesterday"; | ||||
| 		return "Yesterday" | ||||
| 	} | ||||
| 	return format(date, "MMM d, yyyy"); | ||||
| 	return format(date, "MMM d, yyyy") | ||||
| } | ||||
|  | ||||
| export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) { | ||||
| 	// Split icons into two rows for the marquee | ||||
| 	const firstRow = icons.slice(0, Math.ceil(icons.length / 2)); | ||||
| 	const secondRow = icons.slice(Math.ceil(icons.length / 2)); | ||||
| 	const firstRow = icons.slice(0, Math.ceil(icons.length / 2)) | ||||
| 	const secondRow = icons.slice(Math.ceil(icons.length / 2)) | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="relative isolate overflow-hidden my-8"> | ||||
| 			{/* Background glow */} | ||||
| 			<div | ||||
| 				className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" | ||||
| 				aria-hidden="true" | ||||
| 			/> | ||||
| 			<div className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" aria-hidden="true" /> | ||||
|  | ||||
| 			<div className="mx-auto px-6 lg:px-8"> | ||||
| 				<div className="mx-auto max-w-2xl text-center my-4"> | ||||
| @@ -41,10 +38,7 @@ export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) { | ||||
| 				</div> | ||||
|  | ||||
| 				<div className="relative flex w-full flex-col items-center justify-center overflow-hidden"> | ||||
| 					<Marquee | ||||
| 						pauseOnHover | ||||
| 						className="[--duration:60s] [--gap:1em] motion-safe:motion-preset-slide-left-sm motion-duration-1000" | ||||
| 					> | ||||
| 					<Marquee pauseOnHover className="[--duration:60s] [--gap:1em] motion-safe:motion-preset-slide-left-sm motion-duration-1000"> | ||||
| 						{firstRow.map(({ name, data }) => ( | ||||
| 							<RecentIconCard key={name} name={name} data={data} /> | ||||
| 						))} | ||||
| @@ -73,7 +67,7 @@ export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) { | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|  | ||||
| // Marquee-compatible icon card | ||||
| @@ -81,8 +75,8 @@ function RecentIconCard({ | ||||
| 	name, | ||||
| 	data, | ||||
| }: { | ||||
| 	name: string; | ||||
| 	data: Icon; | ||||
| 	name: string | ||||
| 	data: Icon | ||||
| }) { | ||||
| 	return ( | ||||
| 		<Link | ||||
| @@ -118,5 +112,5 @@ function RecentIconCard({ | ||||
| 				<ExternalLink className="w-3 h-3 " /> | ||||
| 			</div> | ||||
| 		</Link> | ||||
| 	); | ||||
| 	) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user