fix actions

This commit is contained in:
Thomas Camlong 2025-04-30 12:16:51 +02:00
parent 73b00ff7a4
commit a98058413d
No known key found for this signature in database
GPG Key ID: A678F374F428457B
3 changed files with 363 additions and 91 deletions

View File

@ -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<void>;
handleCopyUrl: (url: string, variantKey: string, event?: React.MouseEvent) => void;
handleCopyImage: (imageUrl: string, format: string, variantKey: string, event?: React.MouseEvent) => Promise<void>;
};
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 (
<TooltipProvider delayDuration={300}>
<div className="flex gap-2 mt-3 w-full justify-center">
{/* Download Button */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-lg cursor-pointer"
onClick={(e) => handleDownload(e, imageUrl, downloadFilename)}
aria-label={`Download ${iconName} as ${format.toUpperCase()}`}
>
<Download className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Download {format.toUpperCase()}</p>
</TooltipContent>
</Tooltip>
{/* Copy Image Button */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-lg cursor-pointer"
onClick={(e) => handleCopyImage(imageUrl, format, variantKey, e)}
aria-label={`Copy ${iconName} image as ${format.toUpperCase()}`}
>
{isImageCopied ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy image to clipboard</p>
</TooltipContent>
</Tooltip>
{/* Copy URL Button */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-lg cursor-pointer"
onClick={(e) => handleCopyUrl(imageUrl, variantKey, e)}
aria-label={`Copy direct URL for ${iconName} ${format.toUpperCase()}`}
>
{isUrlCopied ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<LinkIcon className="w-4 h-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy direct URL</p>
</TooltipContent>
</Tooltip>
{/* View on GitHub Button */}
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg" asChild>
<Link
href={githubUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`View ${iconName} ${format} file on GitHub`}
>
<Github className="w-4 h-4" />
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>View on GitHub</p>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
);
}

View File

@ -4,7 +4,6 @@ import { formatIconName } from "@/lib/utils"
import type { Icon } from "@/types/icons" import type { Icon } from "@/types/icons"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { preload } from "react-dom"
export function IconCard({ export function IconCard({
name, name,
@ -21,7 +20,7 @@ export function IconCard({
<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-16 w-16 mb-2"> <div className="relative h-16 w-16 mb-2">
<Image <Image
src={`${BASE_URL}/${iconData.base}/${name}.${iconData.base}`} src={`${BASE_URL}/${iconData.base}/${iconData.colors?.light || name}.${iconData.base}`}
alt={`${name} icon`} alt={`${name} icon`}
fill fill
className="object-contain p-1 group-hover:scale-110 transition-transform duration-300" className="object-contain p-1 group-hover:scale-110 transition-transform duration-300"

View File

@ -10,16 +10,122 @@ import { formatIconName } from "@/lib/utils"
import type { AuthorData, Icon, IconFile } from "@/types/icons" import type { AuthorData, Icon, IconFile } from "@/types/icons"
import confetti from "canvas-confetti" import confetti from "canvas-confetti"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { ArrowRight, Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun } from "lucide-react" import { ArrowRight, Check, FileType, Github, Moon, PaletteIcon, Sun, Type } from "lucide-react"
import dynamic from "next/dynamic"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import type React from "react"
import { useCallback, useState } from "react" import { useCallback, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
import { Carbon } from "./carbon" import { Carbon } from "./carbon"
import { IconActions } from "./icon-actions"
import { MagicCard } from "./magicui/magic-card" import { MagicCard } from "./magicui/magic-card"
import { Badge } from "./ui/badge" import { Badge } from "./ui/badge"
type RenderVariantFn = (
format: string,
iconName: string,
theme?: "light" | "dark"
) => 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<void>
copiedVariants: Record<string, boolean>
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 (
<div>
<h3 className="text-lg font-semibold flex items-center gap-2 mb-1">
{iconElement}
{title}
</h3>
<p className="text-sm text-muted-foreground mb-4">{description}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{aavailableFormats.map((format) => renderVariant(format, iconName, theme))}
</div>
</div>
)
}
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<void>
copiedVariants: Record<string, boolean>
renderVariant: RenderVariantFn
}
function WordmarkSection({
iconData,
aavailableFormats,
renderVariant,
}: WordmarkSectionProps) {
if (!iconData.wordmark) return null
return (
<div>
<h3 className="text-lg font-semibold flex items-center gap-2 mb-1">
<Type className="w-4 h-4 text-green-500" />
Wordmark Variants
</h3>
<p className="text-sm text-muted-foreground mb-4">
Icon variants that include the brand name. Click to copy URL.
</p>
<div className="space-y-6">
{iconData.wordmark.light && (
<div>
<h4 className="text-md font-medium flex items-center gap-2 mb-3">
<Sun className="w-4 h-4 text-amber-500" />
Light Theme Wordmark
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{aavailableFormats.map((format) => {
if (!iconData.wordmark?.light) return null
return renderVariant(format, iconData.wordmark.light, "light")
})}
</div>
</div>
)}
{iconData.wordmark.dark && (
<div>
<h4 className="text-md font-medium flex items-center gap-2 mb-3">
<Moon className="w-4 h-4 text-indigo-500" />
Dark Theme Wordmark
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{aavailableFormats.map((format) => {
if (!iconData.wordmark?.dark) return null
return renderVariant(format, iconData.wordmark.dark, "dark")
})}
</div>
</div>
)}
</div>
</div>
)
}
export type IconDetailsProps = { export type IconDetailsProps = {
icon: string icon: string
iconData: Icon iconData: Icon
@ -50,8 +156,12 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
const availableFormats = getAvailableFormats() const availableFormats = getAvailableFormats()
const [copiedVariants, setCopiedVariants] = useState<Record<string, boolean>>({}) const [copiedVariants, setCopiedVariants] = useState<Record<string, boolean>>({})
const [copiedUrlKey, setCopiedUrlKey] = useState<string | null>(null)
const [copiedImageKey, setCopiedImageKey] = useState<string | null>(null)
const launchConfetti = useCallback((originX?: number, originY?: number) => { const launchConfetti = useCallback((originX?: number, originY?: number) => {
if (typeof confetti !== "function") return
const defaults = { const defaults = {
startVelocity: 15, startVelocity: 15,
spread: 180, 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) navigator.clipboard.writeText(url)
setCopiedVariants((prev) => ({ setCopiedUrlKey(variantKey)
...prev,
[variantKey]: true,
}))
setTimeout(() => { setTimeout(() => {
setCopiedVariants((prev) => ({ setCopiedUrlKey(null)
...prev,
[variantKey]: false,
}))
}, 2000) }, 2000)
if (event) { 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) => { const handleDownload = async (event: React.MouseEvent, url: string, filename: string) => {
event.preventDefault() event.preventDefault()
launchConfetti(event.clientX, event.clientY) 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" className="relative w-28 h-28 mb-3 cursor-pointer rounded-xl overflow-hidden group"
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} 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)` : ""}`} aria-label={`Copy ${format.toUpperCase()} URL for ${iconName}${theme ? ` (${theme} theme)` : ""}`}
> >
<div className="absolute inset-0 border-2 border-transparent group-hover:border-primary/20 rounded-xl z-10 transition-colors" /> <div className="absolute inset-0 border-2 border-transparent group-hover:border-primary/20 rounded-xl z-10 transition-colors" />
@ -193,59 +376,18 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
<p className="text-sm font-medium">{format.toUpperCase()}</p> <p className="text-sm font-medium">{format.toUpperCase()}</p>
<div className="flex gap-2 mt-3 w-full justify-center"> <IconActions
<Tooltip> imageUrl={imageUrl}
<TooltipTrigger asChild> githubUrl={githubUrl}
<Button iconName={iconName}
variant="outline" format={format}
size="icon" variantKey={variantKey}
className="h-8 w-8 rounded-lg cursor-pointer" copiedUrlKey={copiedUrlKey}
onClick={(e) => handleDownload(e, imageUrl, `${iconName}.${format}`)} copiedImageKey={copiedImageKey}
aria-label={`Download ${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`} handleDownload={handleDownload}
> handleCopyUrl={handleCopyUrl}
<Download className="w-4 h-4" /> handleCopyImage={handleCopyImage}
</Button> />
</TooltipTrigger>
<TooltipContent>
<p>Download icon file</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-lg cursor-pointer"
onClick={(e) => handleCopy(imageUrl, `btn-${variantKey}`, e)}
aria-label={`Copy URL for ${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`}
>
{copiedVariants[`btn-${variantKey}`] ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy direct URL to clipboard</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg" asChild>
<Link
href={githubUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`View ${iconName} ${format} file on GitHub`}
>
<Github className="w-4 h-4" />
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>View on GitHub</p>
</TooltipContent>
</Tooltip>
</div>
</div> </div>
</MagicCard> </MagicCard>
</TooltipProvider> </TooltipProvider>
@ -263,7 +405,7 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="relative w-32 h-32 rounded-xl overflow-hidden border flex items-center justify-center p-3"> <div className="relative w-32 h-32 rounded-xl overflow-hidden border flex items-center justify-center p-3">
<Image <Image
src={`${BASE_URL}/${iconData.base}/${icon}.${iconData.base}`} src={`${BASE_URL}/${iconData.base}/${iconData.colors?.light || icon}.${iconData.base}`}
width={96} width={96}
height={96} height={96}
placeholder="empty" placeholder="empty"
@ -382,41 +524,49 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-10"> <div className="space-y-10">
<IconVariantsSection {!iconData.colors && (
availableFormats={availableFormats} <IconVariantsSection
icon={icon} title="Default"
iconData={iconData} description="Standard icon versions. Click to copy URL."
handleCopy={handleCopy} iconElement={<FileType className="w-4 h-4 text-blue-500" />}
handleDownload={handleDownload} aavailableFormats={availableFormats}
copiedVariants={copiedVariants} icon={icon}
title="Default" iconData={iconData}
iconElement={<FileType className="w-4 h-4 text-blue-500" />} handleCopy={handleCopyUrl}
/> handleDownload={handleDownload}
copiedVariants={copiedVariants}
renderVariant={renderVariant}
/>
)}
{iconData.colors && ( {iconData.colors && (
<> <>
<IconVariantsSection <IconVariantsSection
availableFormats={availableFormats} title="Light theme"
description="Icon variants optimized for light backgrounds (typically lighter icon colors). Click to copy URL."
iconElement={<Sun className="w-4 h-4 text-amber-500" />}
aavailableFormats={availableFormats}
icon={icon} icon={icon}
theme="light" theme="light"
iconData={iconData} iconData={iconData}
handleCopy={handleCopy} handleCopy={handleCopyUrl}
handleDownload={handleDownload} handleDownload={handleDownload}
copiedVariants={copiedVariants} copiedVariants={copiedVariants}
title="Light theme" renderVariant={renderVariant}
iconElement={<Sun className="w-4 h-4 text-amber-500" />}
/> />
<IconVariantsSection <IconVariantsSection
availableFormats={availableFormats} title="Dark theme"
description="Icon variants optimized for dark backgrounds (typically darker icon colors). Click to copy URL."
iconElement={<Moon className="w-4 h-4 text-indigo-500" />}
aavailableFormats={availableFormats}
icon={icon} icon={icon}
theme="dark" theme="dark"
iconData={iconData} iconData={iconData}
handleCopy={handleCopy} handleCopy={handleCopyUrl}
handleDownload={handleDownload} handleDownload={handleDownload}
copiedVariants={copiedVariants} copiedVariants={copiedVariants}
title="Dark theme" renderVariant={renderVariant}
iconElement={<Moon className="w-4 h-4 text-indigo-500" />}
/> />
</> </>
)} )}
@ -425,10 +575,11 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
<WordmarkSection <WordmarkSection
iconData={iconData} iconData={iconData}
icon={icon} icon={icon}
availableFormats={availableFormats} aavailableFormats={availableFormats}
handleCopy={handleCopy} handleCopy={handleCopyUrl}
handleDownload={handleDownload} handleDownload={handleDownload}
copiedVariants={copiedVariants} copiedVariants={copiedVariants}
renderVariant={renderVariant}
/> />
)} )}
</div> </div>