mirror of
https://github.com/walkxcode/dashboard-icons.git
synced 2025-06-28 15:30:22 +08:00
feat: add workmark icons
This commit is contained in:
parent
523ba5ffd1
commit
beab14f535
@ -1,22 +1,12 @@
|
|||||||
import { IconDetails } from "@/components/icon-details"
|
import { IconDetails } from "@/components/icon-details"
|
||||||
import { BASE_URL, WEB_URL } from "@/constants"
|
import { BASE_URL, WEB_URL } from "@/constants"
|
||||||
import { getAllIcons, getAuthorData } from "@/lib/api"
|
import { getAllIcons, getAuthorData } from "@/lib/api"
|
||||||
import { formatIconName } from "@/lib/utils"
|
|
||||||
import type { Metadata, ResolvingMetadata } from "next"
|
import type { Metadata, ResolvingMetadata } from "next"
|
||||||
import { default as dynamicImport } from "next/dynamic"
|
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
export const dynamicParams = false
|
export const dynamicParams = false
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const iconsData = await getAllIcons()
|
const iconsData = await getAllIcons()
|
||||||
if (process.env.CI_MODE === "false") {
|
|
||||||
// This is meant to speed up the build process in local development
|
|
||||||
return Object.keys(iconsData)
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((icon) => ({
|
|
||||||
icon,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return Object.keys(iconsData).map((icon) => ({
|
return Object.keys(iconsData).map((icon) => ({
|
||||||
icon,
|
icon,
|
||||||
}))
|
}))
|
||||||
|
122
web/src/components/icon-actions.tsx
Normal file
122
web/src/components/icon-actions.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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"
|
||||||
|
@ -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
|
||||||
@ -30,11 +136,13 @@ export type IconDetailsProps = {
|
|||||||
export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetailsProps) {
|
export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetailsProps) {
|
||||||
const authorName = authorData.name || authorData.login || ""
|
const authorName = authorData.name || authorData.login || ""
|
||||||
const iconColorVariants = iconData.colors
|
const iconColorVariants = iconData.colors
|
||||||
|
const iconWordmarkVariants = iconData.wordmark
|
||||||
const formattedDate = new Date(iconData.update.timestamp).toLocaleDateString("en-GB", {
|
const formattedDate = new Date(iconData.update.timestamp).toLocaleDateString("en-GB", {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})
|
})
|
||||||
|
|
||||||
const getAvailableFormats = () => {
|
const getAvailableFormats = () => {
|
||||||
switch (iconData.base) {
|
switch (iconData.base) {
|
||||||
case "svg":
|
case "svg":
|
||||||
@ -48,9 +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)
|
||||||
|
|
||||||
// Launch confetti from the pointer position
|
|
||||||
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,
|
||||||
@ -60,7 +171,6 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
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) {
|
if (originX !== undefined && originY !== undefined) {
|
||||||
confetti({
|
confetti({
|
||||||
...defaults,
|
...defaults,
|
||||||
@ -71,7 +181,6 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Default to center of screen
|
|
||||||
confetti({
|
confetti({
|
||||||
...defaults,
|
...defaults,
|
||||||
particleCount: 50,
|
particleCount: 50,
|
||||||
@ -80,20 +189,13 @@ 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)
|
||||||
|
|
||||||
// Launch confetti from click position or center of screen
|
|
||||||
if (event) {
|
if (event) {
|
||||||
launchConfetti(event.clientX, event.clientY)
|
launchConfetti(event.clientX, event.clientY)
|
||||||
} else {
|
} else {
|
||||||
@ -105,29 +207,99 @@ 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()
|
||||||
|
|
||||||
// Launch confetti from download button position
|
|
||||||
launchConfetti(event.clientX, event.clientY)
|
launchConfetti(event.clientX, event.clientY)
|
||||||
|
|
||||||
try {
|
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 response = await fetch(url)
|
||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
|
|
||||||
// Create a blob URL and use it for download
|
|
||||||
const blobUrl = URL.createObjectURL(blob)
|
const blobUrl = URL.createObjectURL(blob)
|
||||||
const link = document.createElement("a")
|
const link = document.createElement("a")
|
||||||
link.href = blobUrl
|
link.href = blobUrl
|
||||||
link.download = filename
|
link.download = filename
|
||||||
document.body.appendChild(link)
|
document.body.appendChild(link)
|
||||||
link.click()
|
link.click()
|
||||||
|
|
||||||
// Clean up
|
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 100)
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 100)
|
||||||
|
|
||||||
@ -161,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" />
|
||||||
@ -204,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>
|
||||||
@ -268,14 +399,13 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
return (
|
return (
|
||||||
<main className="container mx-auto pt-12 pb-14 px-4 sm:px-6 lg:px-8">
|
<main className="container mx-auto pt-12 pb-14 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||||
{/* Left Column: Icon Info and Author */}
|
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<Card className="h-full bg-background/50 border shadow-lg">
|
<Card className="h-full bg-background/50 border shadow-lg">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
<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"
|
||||||
@ -304,7 +434,7 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
<AvatarImage src={authorData.avatar_url} alt={`${authorName}'s avatar`} />
|
<AvatarImage src={authorData.avatar_url} alt={`${authorName}'s avatar`} />
|
||||||
<AvatarFallback>{authorName ? authorName.slice(0, 2).toUpperCase() : "??"}</AvatarFallback>
|
<AvatarFallback>{authorName ? authorName.slice(0, 2).toUpperCase() : "??"}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
{authorData.html_url ? (
|
{authorData.html_url && (
|
||||||
<Link
|
<Link
|
||||||
href={authorData.html_url}
|
href={authorData.html_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -313,7 +443,8 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
>
|
>
|
||||||
{authorName}
|
{authorName}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
)}
|
||||||
|
{!authorData.html_url && (
|
||||||
<span className="text-sm">{authorName}</span>
|
<span className="text-sm">{authorName}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -370,6 +501,7 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
: `${availableFormats[0].toUpperCase()} format `}
|
: `${availableFormats[0].toUpperCase()} format `}
|
||||||
with a base format of {iconData.base.toUpperCase()}.
|
with a base format of {iconData.base.toUpperCase()}.
|
||||||
{iconData.colors && " Includes both light and dark theme variants for better integration with different UI designs."}
|
{iconData.colors && " Includes both light and dark theme variants for better integration with different UI designs."}
|
||||||
|
{iconData.wordmark && " Wordmark variants are also available for enhanced branding options."}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Perfect for adding to dashboards, app directories, documentation, or anywhere you need the {formatIconName(icon)}{" "}
|
Perfect for adding to dashboards, app directories, documentation, or anywhere you need the {formatIconName(icon)}{" "}
|
||||||
@ -382,7 +514,6 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Middle Column: Icon variants */}
|
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<Card className="h-full bg-background/50 shadow-lg">
|
<Card className="h-full bg-background/50 shadow-lg">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -392,37 +523,70 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
<CardDescription>Click on any icon to copy its URL to your clipboard</CardDescription>
|
<CardDescription>Click on any icon to copy its URL to your clipboard</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!iconData.colors ? (
|
<div className="space-y-10">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
{!iconData.colors && (
|
||||||
{availableFormats.map((format) => renderVariant(format, icon))}
|
<IconVariantsSection
|
||||||
</div>
|
title="Default"
|
||||||
) : (
|
description="Standard icon versions. Click to copy URL."
|
||||||
<div className="space-y-10">
|
iconElement={<FileType className="w-4 h-4 text-blue-500" />}
|
||||||
<div>
|
aavailableFormats={availableFormats}
|
||||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
icon={icon}
|
||||||
<Sun className="w-4 h-4 text-amber-500" />
|
iconData={iconData}
|
||||||
Light theme
|
handleCopy={handleCopyUrl}
|
||||||
</h3>
|
handleDownload={handleDownload}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg ">
|
copiedVariants={copiedVariants}
|
||||||
{availableFormats.map((format) => renderVariant(format, icon, "light"))}
|
renderVariant={renderVariant}
|
||||||
</div>
|
/>
|
||||||
</div>
|
)}
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
{iconData.colors && (
|
||||||
<Moon className="w-4 h-4 text-indigo-500" />
|
<>
|
||||||
Dark theme
|
<IconVariantsSection
|
||||||
</h3>
|
title="Light theme"
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg ">
|
description="Icon variants optimized for light backgrounds (typically lighter icon colors). Click to copy URL."
|
||||||
{availableFormats.map((format) => renderVariant(format, icon, "dark"))}
|
iconElement={<Sun className="w-4 h-4 text-amber-500" />}
|
||||||
</div>
|
aavailableFormats={availableFormats}
|
||||||
</div>
|
icon={icon}
|
||||||
</div>
|
theme="light"
|
||||||
)}
|
iconData={iconData}
|
||||||
|
handleCopy={handleCopyUrl}
|
||||||
|
handleDownload={handleDownload}
|
||||||
|
copiedVariants={copiedVariants}
|
||||||
|
renderVariant={renderVariant}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconVariantsSection
|
||||||
|
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}
|
||||||
|
theme="dark"
|
||||||
|
iconData={iconData}
|
||||||
|
handleCopy={handleCopyUrl}
|
||||||
|
handleDownload={handleDownload}
|
||||||
|
copiedVariants={copiedVariants}
|
||||||
|
renderVariant={renderVariant}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{iconData.wordmark && (
|
||||||
|
<WordmarkSection
|
||||||
|
iconData={iconData}
|
||||||
|
icon={icon}
|
||||||
|
aavailableFormats={availableFormats}
|
||||||
|
handleCopy={handleCopyUrl}
|
||||||
|
handleDownload={handleDownload}
|
||||||
|
copiedVariants={copiedVariants}
|
||||||
|
renderVariant={renderVariant}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column: Technical details */}
|
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<Card className="h-full bg-background/50 border shadow-lg">
|
<Card className="h-full bg-background/50 border shadow-lg">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -464,6 +628,28 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{iconData.wordmark && (
|
||||||
|
<div className="">
|
||||||
|
<h3 className="text-sm font-semibold text-muted-foreground">Wordmark variants</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{iconData.wordmark.light && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Type className="w-4 h-4 text-green-500" />
|
||||||
|
<span className="capitalize font-medium text-sm">Light:</span>
|
||||||
|
<code className="border border-border px-2 py-0.5 rounded-lg text-xs">{iconData.wordmark.light}</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{iconData.wordmark.dark && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Type className="w-4 h-4 text-green-500" />
|
||||||
|
<span className="capitalize font-medium text-sm">Dark:</span>
|
||||||
|
<code className="border border-border px-2 py-0.5 rounded-lg text-xs">{iconData.wordmark.dark}</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="">
|
<div className="">
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Source</h3>
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Source</h3>
|
||||||
<Button variant="outline" className="w-full" asChild>
|
<Button variant="outline" className="w-full" asChild>
|
||||||
|
@ -13,12 +13,18 @@ export type IconColors = {
|
|||||||
light?: string
|
light?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IconWordmarkColors = {
|
||||||
|
dark?: string
|
||||||
|
light?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type Icon = {
|
export type Icon = {
|
||||||
base: string | "svg" | "png" | "webp"
|
base: string | "svg" | "png" | "webp"
|
||||||
aliases: string[]
|
aliases: string[]
|
||||||
categories: string[]
|
categories: string[]
|
||||||
update: IconUpdate
|
update: IconUpdate
|
||||||
colors?: IconColors
|
colors?: IconColors
|
||||||
|
wordmark?: IconWordmarkColors
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IconFile = {
|
export type IconFile = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user