diff --git a/web/src/app/icons/[icon]/page.tsx b/web/src/app/icons/[icon]/page.tsx index 1530f52d..3d83d4fb 100644 --- a/web/src/app/icons/[icon]/page.tsx +++ b/web/src/app/icons/[icon]/page.tsx @@ -99,5 +99,5 @@ export default async function IconPage({ params }: { params: Promise<{ icon: str const authorData = await getAuthorData(originalIconData.update.author.id) - return + return } diff --git a/web/src/app/icons/page.tsx b/web/src/app/icons/page.tsx index a218be25..ae8f4b5a 100644 --- a/web/src/app/icons/page.tsx +++ b/web/src/app/icons/page.tsx @@ -1,7 +1,7 @@ +import { IconSearch } from "@/components/icon-search" import { BASE_URL } from "@/constants" import { getIconsArray } from "@/lib/api" import type { Metadata } from "next" -import { IconSearch } from "./components/icon-search" export async function generateMetadata(): Promise { const icons = await getIconsArray() diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 34bd443b..b792f1d1 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -7,7 +7,7 @@ import type { Metadata, Viewport } from "next" import { Inter } from "next/font/google" import { Toaster } from "sonner" import "./globals.css" -import { BASE_URL, getDescription, WEB_URL, websiteTitle } from "@/constants" +import { BASE_URL, WEB_URL, getDescription, websiteTitle } from "@/constants" import { ThemeProvider } from "./theme-provider" const inter = Inter({ diff --git a/web/src/components/icon-card.tsx b/web/src/components/icon-card.tsx new file mode 100644 index 00000000..cc3ea24d --- /dev/null +++ b/web/src/components/icon-card.tsx @@ -0,0 +1,35 @@ +import { MagicCard } from "@/components/magicui/magic-card" +import { BASE_URL } from "@/constants" +import type { Icon } from "@/types/icons" +import Image from "next/image" +import Link from "next/link" + +export function IconCard({ + name, + data: iconData, + matchedAlias, +}: { + name: string + data: Icon + matchedAlias?: string | null +}) { + return ( + + +
+ {`${name} +
+ + {name.replace(/-/g, " ")} + + + {matchedAlias && Alias: {matchedAlias}} + +
+ ) +} diff --git a/web/src/components/icon-details.tsx b/web/src/components/icon-details.tsx index 5b760e2d..2bdd5430 100644 --- a/web/src/components/icon-details.tsx +++ b/web/src/components/icon-details.tsx @@ -1,15 +1,15 @@ "use client" +import { IconsGrid } from "@/components/icon-grid" 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 type { AuthorData, Icon, IconFile } from "@/types/icons" import confetti from "canvas-confetti" import { motion } from "framer-motion" import { Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun } from "lucide-react" -import { useTheme } from "next-themes" import Image from "next/image" import Link from "next/link" import { useCallback, useState } from "react" @@ -22,9 +22,10 @@ export type IconDetailsProps = { icon: string iconData: Icon authorData: AuthorData + allIcons: IconFile } -export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { +export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetailsProps) { const authorName = authorData.name || authorData.login || "" const iconColorVariants = iconData.colors const formattedDate = new Date(iconData.update.timestamp).toLocaleDateString("en-GB", { @@ -159,6 +160,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} onClick={(e) => handleCopy(imageUrl, variantKey, e)} + aria-label={`Copy ${format.toUpperCase()} URL for ${iconName}${theme ? ` (${theme} theme)` : ""}`} >
@@ -264,23 +266,25 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { } return ( -
+
{/* Left Column: Icon Info and Author */}
-
+
{icon}
- {icon} + +

{icon.replace(/-/g, " ")}

+
@@ -289,14 +293,14 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {

- Updated on: {formattedDate} + Updated on:

By:

- + {authorName ? authorName.slice(0, 2).toUpperCase() : "??"} {authorData.html_url ? ( @@ -379,7 +383,9 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
- Icon variants + +

Icon variants

+
Click on any icon to copy its URL to your clipboard
@@ -470,6 +476,31 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
-
+ {iconData.categories && iconData.categories.length > 0 && ( +
+ + + + + + + Other icons from {iconData.categories.map((cat) => cat.replace(/-/g, " ")).join(", ")} categories + + + + { + if (name === icon) return false + return data.categories?.some((cat) => iconData.categories?.includes(cat)) + }) + .map(([name, data]) => ({ name, data }))} + matchedAliases={{}} + /> + + +
+ )} +
) } diff --git a/web/src/components/icon-grid.tsx b/web/src/components/icon-grid.tsx new file mode 100644 index 00000000..241b069f --- /dev/null +++ b/web/src/components/icon-grid.tsx @@ -0,0 +1,21 @@ +import type { Icon } from "@/types/icons" + +import { IconCard } from "./icon-card" + +interface IconsGridProps { + filteredIcons: { name: string; data: Icon }[] + matchedAliases: Record +} + +export function IconsGrid({ filteredIcons, matchedAliases }: IconsGridProps) { + return ( + <> +
+ {filteredIcons.slice(0, 120).map(({ name, data }) => ( + + ))} +
+ {filteredIcons.length > 120 &&

And {filteredIcons.length - 120} more...

} + + ) +} diff --git a/web/src/app/icons/components/icon-search.tsx b/web/src/components/icon-search.tsx similarity index 76% rename from web/src/app/icons/components/icon-search.tsx rename to web/src/components/icon-search.tsx index 321a56fd..375716ba 100644 --- a/web/src/app/icons/components/icon-search.tsx +++ b/web/src/components/icon-search.tsx @@ -1,7 +1,7 @@ "use client" +import { IconsGrid } from "@/components/icon-grid" 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 { @@ -17,12 +17,9 @@ import { } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Separator } from "@/components/ui/separator" -import { BASE_URL } from "@/constants" -import type { Icon, IconSearchProps } from "@/types/icons" +import type { IconSearchProps } from "@/types/icons" import { ArrowDownAZ, ArrowUpZA, Calendar, Filter, Search, SortAsc, X } from "lucide-react" import { useTheme } from "next-themes" -import Image from "next/image" -import Link from "next/link" import { usePathname, useRouter, useSearchParams } from "next/navigation" import posthog from "posthog-js" import { useCallback, useEffect, useMemo, useRef, useState } from "react" @@ -228,11 +225,11 @@ export function IconSearch({ icons }: IconSearchProps) { const getSortLabel = (sort: SortOption) => { switch (sort) { case "relevance": - return "Relevance" + return "Best match" case "alphabetical-asc": - return "Name (A-Z)" + return "A to Z" case "alphabetical-desc": - return "Name (Z-A)" + return "Z to A" case "newest": return "Newest first" default: @@ -265,7 +262,7 @@ export function IconSearch({ icons }: IconSearchProps) {
handleSearch(e.target.value)} @@ -277,18 +274,18 @@ export function IconSearch({ icons }: IconSearchProps) { {/* Filter dropdown */} - - Select Categories + Categories
@@ -314,7 +311,7 @@ export function IconSearch({ icons }: IconSearchProps) { }} className="cursor-pointer focus: focus:bg-rose-50 dark:focus:bg-rose-950/20" > - Clear categories + Clear all filters )} @@ -330,18 +327,18 @@ export function IconSearch({ icons }: IconSearchProps) { - Sort Icons + Sort By handleSortChange(value as SortOption)}> - Relevance + Best match - Name (A-Z) + A to Z - Name (Z-A) + Z to A @@ -353,15 +350,9 @@ export function IconSearch({ icons }: IconSearchProps) { {/* Clear all button */} {(searchQuery || selectedCategories.length > 0 || sortOption !== "relevance") && ( - )}
@@ -369,7 +360,7 @@ export function IconSearch({ icons }: IconSearchProps) { {/* Active filter badges */} {selectedCategories.length > 0 && (
- Selected: + Filters:
{selectedCategories.map((category) => ( @@ -395,7 +386,7 @@ export function IconSearch({ icons }: IconSearchProps) { }} className="text-xs h-7 px-2 cursor-pointer" > - Clear + Clear all
)} @@ -406,33 +397,27 @@ export function IconSearch({ icons }: IconSearchProps) { {filteredIcons.length === 0 ? (
-

Icon not found

-

Help us expand our collection

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

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

+ +
) : ( <> @@ -453,51 +438,3 @@ export function IconSearch({ icons }: IconSearchProps) { ) } - -function IconCard({ - name, - data: iconData, - matchedAlias, -}: { - name: string - data: Icon - matchedAlias?: string | null -}) { - return ( - - -
- {`${name} -
- - {name.replace(/-/g, " ")} - - - {matchedAlias && Alias: {matchedAlias}} - -
- ) -} - -interface IconsGridProps { - filteredIcons: { name: string; data: Icon }[] - matchedAliases: Record -} - -function IconsGrid({ filteredIcons, matchedAliases }: IconsGridProps) { - return ( - <> -
- {filteredIcons.slice(0, 120).map(({ name, data }) => ( - - ))} -
- {filteredIcons.length > 120 &&

And {filteredIcons.length - 120} more...

} - - ) -}