diff --git a/web/src/components/icon-details.tsx b/web/src/components/icon-details.tsx index 7ef48c1e..ec70d298 100644 --- a/web/src/components/icon-details.tsx +++ b/web/src/components/icon-details.tsx @@ -1,278 +1,338 @@ -"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 { useTheme } from "next-themes" -import Image from "next/image" -import Link from "next/link" -import { useCallback, useState } from "react" -import { toast } from "sonner" -import { Carbon } from "./carbon" +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 { useTheme } from "next-themes"; +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 { resolvedTheme } = useTheme() - 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) => { const defaults = { - startVelocity: 30, - spread: 360, + startVelocity: 15, + spread: 180, 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) { confetti({ ...defaults, - particleCount: 100, - origin: { x: originX / window.innerWidth, y: originY / window.innerHeight }, - }) + particleCount: 50, + origin: { + x: originX / window.innerWidth, + y: originY / window.innerHeight, + }, + }); } else { // Default to center of screen confetti({ ...defaults, - particleCount: 100, + particleCount: 50, origin: { x: 0.5, y: 0.5 }, - }) + }); } - }, []) + }, []); - // Helper function to get the appropriate icon variant based on theme - const getIconVariant = (iconName: string) => { - // Check if the icon has theme variants - if (iconColorVariants) { - // If in dark mode and a light variant exists, use the light variant - if (resolvedTheme === "dark" && iconColorVariants.light) { - return iconColorVariants.light - } - // If in light mode and a dark variant exists, use the dark variant - if (resolvedTheme === "light" && iconColorVariants.dark) { - return iconColorVariants.dark - } - } - // Fall back to the default name if no appropriate variant - return iconName - } - - 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 url = `${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 url = `${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 ( - -
- - - handleCopy(url, variantKey, e)} - > -
- + + +
+ + handleCopy(url, variantKey, e)} > +
+ - + + + + + {`${iconName} - - {`${iconName} - - - -

Click to copy direct URL to clipboard

-
- - -

{format.toUpperCase()}

- -
- - - -

Download icon file

+

Click to copy direct URL to clipboard

- - - - - -

Copy direct URL to clipboard

-
-
+

{format.toUpperCase()}

- - - - - -

View on GitHub

-
-
+
+ + + + + +

Download icon file

+
+
+ + + + + + +

Copy direct URL to clipboard

+
+
+ + + + + + +

View on GitHub

+
+
+
-
+ - ) - } + ); + }; return (
{/* Left Column: Icon Info and Author */}
- +
-
+
{icon}
- {icon} + + {icon} +
@@ -281,15 +341,23 @@ 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(" ")} ))} @@ -330,7 +403,9 @@ 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. +

)}
@@ -352,34 +430,42 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { {/* Middle Column: Icon variants */}
- + 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), + )}
) : (
-

+

Light theme

-
- {availableFormats.map((format) => renderVariant(format, icon, "light"))} +
+ {availableFormats.map((format) => + renderVariant(format, icon, "light"), + )}
-

+

Dark theme

-
- {availableFormats.map((format) => renderVariant(format, icon, "dark"))} +
+ {availableFormats.map((format) => + renderVariant(format, icon, "dark"), + )}
@@ -390,29 +476,33 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) { {/* Right Column: Technical details */}
- + Technical details
-

Base format

+

+ Base format +

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

Available formats

+

+ Available formats +

{availableFormats.map((format) => (
{format.toUpperCase()}
@@ -422,25 +512,37 @@ 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 +

- ) + ); }