diff --git a/web/src/components/icon-actions.tsx b/web/src/components/icon-actions.tsx new file mode 100644 index 00000000..289c2937 --- /dev/null +++ b/web/src/components/icon-actions.tsx @@ -0,0 +1,122 @@ +import { Button } from "@/components/ui/button"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { Check, Copy, Download, Github, Link as LinkIcon } from "lucide-react"; +import Link from "next/link"; +import type React from "react"; + +export type IconActionsProps = { + imageUrl: string; + githubUrl: string; + iconName: string; + format: string; + variantKey: string; + copiedUrlKey: string | null; + copiedImageKey: string | null; + handleDownload: (event: React.MouseEvent, url: string, filename: string) => Promise; + handleCopyUrl: (url: string, variantKey: string, event?: React.MouseEvent) => void; + handleCopyImage: (imageUrl: string, format: string, variantKey: string, event?: React.MouseEvent) => Promise; +}; + +export function IconActions({ + imageUrl, + githubUrl, + iconName, + format, + variantKey, + copiedUrlKey, + copiedImageKey, + handleDownload, + handleCopyUrl, + handleCopyImage, +}: IconActionsProps) { + const downloadFilename = `${iconName}.${format}`; + const isUrlCopied = copiedUrlKey === variantKey; + const isImageCopied = copiedImageKey === variantKey; + + return ( + +
+ {/* Download Button */} + + + + + +

Download {format.toUpperCase()}

+
+
+ + {/* Copy Image Button */} + + + + + +

Copy image to clipboard

+
+
+ + {/* Copy URL Button */} + + + + + +

Copy direct URL

+
+
+ + {/* View on GitHub Button */} + + + + + +

View on GitHub

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/web/src/components/icon-card.tsx b/web/src/components/icon-card.tsx index 8d547b5f..7a1fc265 100644 --- a/web/src/components/icon-card.tsx +++ b/web/src/components/icon-card.tsx @@ -4,7 +4,6 @@ import { formatIconName } from "@/lib/utils" import type { Icon } from "@/types/icons" import Image from "next/image" import Link from "next/link" -import { preload } from "react-dom" export function IconCard({ name, @@ -21,7 +20,7 @@ export function IconCard({
{`${name} React.ReactNode + +type IconVariantsSectionProps = { + title: string + description: string + iconElement: React.ReactNode + aavailableFormats: string[] + icon: string + iconData: Icon + handleCopy: (url: string, variantKey: string, event?: React.MouseEvent) => void + handleDownload: (event: React.MouseEvent, url: string, filename: string) => Promise + copiedVariants: Record + theme?: "light" | "dark" + renderVariant: RenderVariantFn +} + +function IconVariantsSection({ + title, + description, + iconElement, + aavailableFormats, + icon, + iconData, + theme, + renderVariant, +}: IconVariantsSectionProps) { + const iconName = theme && iconData.colors?.[theme] ? iconData.colors[theme] : icon + return ( +
+

+ {iconElement} + {title} +

+

{description}

+
+ {aavailableFormats.map((format) => renderVariant(format, iconName, theme))} +
+
+ ) +} + +type WordmarkSectionProps = { + iconData: Icon + icon: string + aavailableFormats: string[] + handleCopy: (url: string, variantKey: string, event?: React.MouseEvent) => void + handleDownload: (event: React.MouseEvent, url: string, filename: string) => Promise + copiedVariants: Record + renderVariant: RenderVariantFn +} + +function WordmarkSection({ + iconData, + aavailableFormats, + renderVariant, +}: WordmarkSectionProps) { + if (!iconData.wordmark) return null + + return ( +
+

+ + Wordmark Variants +

+

+ Icon variants that include the brand name. Click to copy URL. +

+
+ {iconData.wordmark.light && ( +
+

+ + Light Theme Wordmark +

+
+ {aavailableFormats.map((format) => { + if (!iconData.wordmark?.light) return null + return renderVariant(format, iconData.wordmark.light, "light") + })} +
+
+ )} + {iconData.wordmark.dark && ( +
+

+ + Dark Theme Wordmark +

+
+ {aavailableFormats.map((format) => { + if (!iconData.wordmark?.dark) return null + return renderVariant(format, iconData.wordmark.dark, "dark") + })} +
+
+ )} +
+
+ ) +} + export type IconDetailsProps = { icon: string iconData: Icon @@ -50,8 +156,12 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail const availableFormats = getAvailableFormats() const [copiedVariants, setCopiedVariants] = useState>({}) + const [copiedUrlKey, setCopiedUrlKey] = useState(null) + const [copiedImageKey, setCopiedImageKey] = useState(null) const launchConfetti = useCallback((originX?: number, originY?: number) => { + if (typeof confetti !== "function") return + const defaults = { startVelocity: 15, spread: 180, @@ -79,17 +189,11 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail } }, []) - const handleCopy = (url: string, variantKey: string, event?: React.MouseEvent) => { + const handleCopyUrl = (url: string, variantKey: string, event?: React.MouseEvent) => { navigator.clipboard.writeText(url) - setCopiedVariants((prev) => ({ - ...prev, - [variantKey]: true, - })) + setCopiedUrlKey(variantKey) setTimeout(() => { - setCopiedVariants((prev) => ({ - ...prev, - [variantKey]: false, - })) + setCopiedUrlKey(null) }, 2000) if (event) { @@ -103,6 +207,85 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail }) } + const handleCopyImage = async ( + imageUrl: string, + format: string, + variantKey: string, + event?: React.MouseEvent + ) => { + try { + toast.loading("Copying image...") + + if (format === 'svg') { + const response = await fetch(imageUrl) + if (!response.ok) { + throw new Error(`Failed to fetch SVG: ${response.statusText}`) + } + const svgText = await response.text() + + await navigator.clipboard.writeText(svgText) + + setCopiedImageKey(variantKey) + setTimeout(() => { + setCopiedImageKey(null) + }, 2000) + + if (event) { + launchConfetti(event.clientX, event.clientY) + } else { + launchConfetti() + } + + toast.dismiss() + toast.success("SVG Markup Copied", { + description: "The SVG code has been copied to your clipboard.", + }) + + } else if (format === 'png' || format === 'webp') { + const mimeType = `image/${format}` + const response = await fetch(imageUrl) + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.statusText}`) + } + const blob = await response.blob() + + if (!blob) { + throw new Error('Failed to generate image blob') + } + + await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })]); + + setCopiedImageKey(variantKey) + setTimeout(() => { + setCopiedImageKey(null) + }, 2000) + + if (event) { + launchConfetti(event.clientX, event.clientY) + } else { + launchConfetti() + } + + toast.dismiss() + toast.success("Image copied", { + description: `The ${format.toUpperCase()} image has been copied to your clipboard.`, + }) + + } else { + throw new Error(`Unsupported format for image copy: ${format}`) + } + + } catch (error) { + console.error("Copy error:", error) + toast.dismiss() + let description = "Could not copy. Check console for details." + if (error instanceof Error) { + description = error.message + } + toast.error("Copy failed", { description }) + } + } + const handleDownload = async (event: React.MouseEvent, url: string, filename: string) => { event.preventDefault() launchConfetti(event.clientX, event.clientY) @@ -150,7 +333,7 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail className="relative w-28 h-28 mb-3 cursor-pointer rounded-xl overflow-hidden group" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} - onClick={(e) => handleCopy(imageUrl, variantKey, e)} + onClick={(e) => handleCopyUrl(imageUrl, variantKey, e)} aria-label={`Copy ${format.toUpperCase()} URL for ${iconName}${theme ? ` (${theme} theme)` : ""}`} >
@@ -193,59 +376,18 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail

{format.toUpperCase()}

-
- - - - - -

Download icon file

-
-
- - - - - - -

Copy direct URL to clipboard

-
-
- - - - - - -

View on GitHub

-
-
-
+
@@ -263,7 +405,7 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
- } - /> - + {!iconData.colors && ( + } + aavailableFormats={availableFormats} + icon={icon} + iconData={iconData} + handleCopy={handleCopyUrl} + handleDownload={handleDownload} + copiedVariants={copiedVariants} + renderVariant={renderVariant} + /> + )} + {iconData.colors && ( <> } + aavailableFormats={availableFormats} icon={icon} theme="light" iconData={iconData} - handleCopy={handleCopy} + handleCopy={handleCopyUrl} handleDownload={handleDownload} copiedVariants={copiedVariants} - title="Light theme" - iconElement={} + renderVariant={renderVariant} /> - + } + aavailableFormats={availableFormats} icon={icon} theme="dark" iconData={iconData} - handleCopy={handleCopy} + handleCopy={handleCopyUrl} handleDownload={handleDownload} copiedVariants={copiedVariants} - title="Dark theme" - iconElement={} + renderVariant={renderVariant} /> )} - + {iconData.wordmark && ( )}