diff --git a/web/biome.json b/web/biome.json index 1dbf24a0..6738ae2b 100644 --- a/web/biome.json +++ b/web/biome.json @@ -19,7 +19,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "suspicious": { + "noArrayIndexKey": "off" + } } }, "javascript": { diff --git a/web/package.json b/web/package.json index 04b1cbed..3289eded 100644 --- a/web/package.json +++ b/web/package.json @@ -78,10 +78,6 @@ }, "packageManager": "pnpm@10.8.0", "pnpm": { - "onlyBuiltDependencies": [ - "@biomejs/biome", - "core-js", - "sharp" - ] + "onlyBuiltDependencies": ["@biomejs/biome", "core-js", "sharp"] } } diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 9e3e3366..43ec4467 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -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; } -} \ No newline at end of file +} diff --git a/web/src/app/icons/[icon]/page.tsx b/web/src/app/icons/[icon]/page.tsx index 6d4373e3..ef157e18 100644 --- a/web/src/app/icons/[icon]/page.tsx +++ b/web/src/app/icons/[icon]/page.tsx @@ -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(), diff --git a/web/src/app/icons/components/icon-search.tsx b/web/src/app/icons/components/icon-search.tsx index c3311820..cb534905 100644 --- a/web/src/app/icons/components/icon-search.tsx +++ b/web/src/app/icons/components/icon-search.tsx @@ -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( - initialCategories ?? [], - ); - const [sortOption, setSortOption] = useState(initialSort); - const timeoutRef = useRef(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(initialCategories ?? []) + const [sortOption, setSortOption] = useState(initialSort) + const timeoutRef = useRef(null) + const { resolvedTheme } = useTheme() // Extract all unique categories const allCategories = useMemo(() => { - const categories = new Set(); + const categories = new Set() 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 = {}; + const q = searchQuery.toLowerCase() + const matches: Record = {} - 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 ; + return case "alphabetical-asc": - return ; + return case "alphabetical-desc": - return ; + return case "newest": - return ; + return default: - return ; + return } - }; + } return ( <> @@ -273,11 +248,7 @@ export function IconSearch({ icons }: IconSearchProps) { {/* Filter dropdown */} - - - Categories - + Categories
@@ -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())} ))}
@@ -313,8 +280,8 @@ export function IconSearch({ icons }: IconSearchProps) { { - 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) { - - Sort By - + Sort By - handleSortChange(value as SortOption)} - > - + handleSortChange(value as SortOption)}> + Best match - + A to Z - + Z to A - + Newest first @@ -377,15 +327,8 @@ export function IconSearch({ icons }: IconSearchProps) {
{/* Clear all button */} - {(searchQuery || - selectedCategories.length > 0 || - sortOption !== "relevance") && ( - @@ -398,14 +341,8 @@ export function IconSearch({ icons }: IconSearchProps) { Filters:
{selectedCategories.map((category) => ( - - {category - .replace(/-/g, " ") - .replace(/\b\w/g, (c) => c.toUpperCase())} + + {category.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}
- ); + ) } diff --git a/web/src/components/icon-details.tsx b/web/src/components/icon-details.tsx index 35b2130d..ddcf8800 100644 --- a/web/src/components/icon-details.tsx +++ b/web/src/components/icon-details.tsx @@ -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>( - {}, - ); + const availableFormats = getAvailableFormats() + const [copiedVariants, setCopiedVariants] = useState>({}) // 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 ( @@ -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}`)} > @@ -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}`] ? ( - - ) : ( - - )} + {copiedVariants[`btn-${variantKey}`] ? : } @@ -286,17 +232,8 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { - @@ -309,8 +246,8 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { - ); - }; + ) + } return (
@@ -329,9 +266,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { className="w-full h-full object-contain" />
- - {icon} - + {icon} @@ -340,23 +275,15 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {

- Updated on:{" "} - {formattedDate} + Updated on: {formattedDate}

By:

- - - {authorName - ? authorName.slice(0, 2).toUpperCase() - : "??"} - + + {authorName ? authorName.slice(0, 2).toUpperCase() : "??"} {authorData.html_url ? ( 0 && (
-

- Categories -

+

Categories

{iconData.categories.map((category) => ( {category .split("-") - .map( - (word) => - word.charAt(0).toUpperCase() + word.slice(1), - ) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" ")} ))} @@ -402,9 +324,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { {iconData.aliases && iconData.aliases.length > 0 && (
-

- Aliases -

+

Aliases

{iconData.aliases.map((alias) => ( ))}
-

- These aliases can be used to find this icon in search - results. -

+

These aliases can be used to find this icon in search results.

)}
@@ -432,16 +349,12 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { Icon variants - - Click on any icon to copy its URL to your clipboard - + Click on any icon to copy its URL to your clipboard {!iconData.colors ? (
- {availableFormats.map((format) => - renderVariant(format, icon), - )} + {availableFormats.map((format) => renderVariant(format, icon))}
) : (
@@ -451,9 +364,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { Light theme
- {availableFormats.map((format) => - renderVariant(format, icon, "light"), - )} + {availableFormats.map((format) => renderVariant(format, icon, "light"))}
@@ -462,9 +373,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { Dark theme
- {availableFormats.map((format) => - renderVariant(format, icon, "dark"), - )} + {availableFormats.map((format) => renderVariant(format, icon, "dark"))}
@@ -482,27 +391,18 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
-

- Base format -

+

Base format

-
- {iconData.base.toUpperCase()} -
+
{iconData.base.toUpperCase()}
-

- Available formats -

+

Available formats

{availableFormats.map((format) => ( -
+
{format.toUpperCase()}
))} @@ -511,37 +411,23 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { {iconData.colors && (
-

- Color variants -

+

Color variants

- {Object.entries(iconData.colors).map( - ([theme, variant]) => ( -
- - - {theme}: - - - {variant} - -
- ), - )} + {Object.entries(iconData.colors).map(([theme, variant]) => ( +
+ + {theme}: + {variant} +
+ ))}
)}
-

- Source -

+

Source

- ); + ) } diff --git a/web/src/components/magicui/animated-shiny-text.tsx b/web/src/components/magicui/animated-shiny-text.tsx index 7a8506ba..4b92a5c7 100644 --- a/web/src/components/magicui/animated-shiny-text.tsx +++ b/web/src/components/magicui/animated-shiny-text.tsx @@ -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 = ({ - children, - className, - shimmerWidth = 100, - ...props -}) => { - return ( - = ({ children, className, shimmerWidth = 100, ...props }) => { + return ( + - {children} - - ); -}; + className, + )} + {...props} + > + {children} + + ) +} diff --git a/web/src/components/magicui/aurora-text.tsx b/web/src/components/magicui/aurora-text.tsx index 4b379636..e00fd98c 100644 --- a/web/src/components/magicui/aurora-text.tsx +++ b/web/src/components/magicui/aurora-text.tsx @@ -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 ( - - {children} - - - ); - }, -); + return ( + + {children} + + + ) + }, +) -AuroraText.displayName = "AuroraText"; +AuroraText.displayName = "AuroraText" diff --git a/web/src/components/magicui/interactive-hover-button.tsx b/web/src/components/magicui/interactive-hover-button.tsx index 05ea22eb..bd72e24d 100644 --- a/web/src/components/magicui/interactive-hover-button.tsx +++ b/web/src/components/magicui/interactive-hover-button.tsx @@ -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 {} +interface InteractiveHoverButtonProps extends React.ButtonHTMLAttributes {} -export const InteractiveHoverButton = React.forwardRef< - HTMLButtonElement, - InteractiveHoverButtonProps ->(({ children, className, ...props }, ref) => { - return ( - - ); -}); +export const InteractiveHoverButton = React.forwardRef( + ({ children, className, ...props }, ref) => { + return ( + + ) + }, +) -InteractiveHoverButton.displayName = "InteractiveHoverButton"; +InteractiveHoverButton.displayName = "InteractiveHoverButton" diff --git a/web/src/components/magicui/magic-card.tsx b/web/src/components/magicui/magic-card.tsx index 40c2c0ae..bedfe8f2 100644 --- a/web/src/components/magicui/magic-card.tsx +++ b/web/src/components/magicui/magic-card.tsx @@ -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(null); - const mouseX = useMotionValue(-gradientSize); - const mouseY = useMotionValue(-gradientSize); + const cardRef = useRef(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 ( -
- + -
- +
+ -
{children}
-
- ); + opacity: gradientOpacity, + }} + /> +
{children}
+
+ ) } diff --git a/web/src/components/magicui/marquee.tsx b/web/src/components/magicui/marquee.tsx index fa9c129b..b92c3067 100644 --- a/web/src/components/magicui/marquee.tsx +++ b/web/src/components/magicui/marquee.tsx @@ -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 ( -
- {Array(repeat) - .fill(0) - .map((_, i) => ( -
- {children} -
- ))} -
- ); + return ( +
+ {Array(repeat) + .fill(0) + .map((_, i) => ( +
+ {children} +
+ ))} +
+ ) } diff --git a/web/src/components/recently-added-icons.tsx b/web/src/components/recently-added-icons.tsx index b31a3bd5..c5e76eec 100644 --- a/web/src/components/recently-added-icons.tsx +++ b/web/src/components/recently-added-icons.tsx @@ -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 (
{/* Background glow */} -