mirror of
https://github.com/walkxcode/dashboard-icons.git
synced 2025-06-28 23:40:21 +08:00
wip (don't push)
This commit is contained in:
parent
e5777ff2d6
commit
d1152dbaf6
@ -1 +0,0 @@
|
|||||||
<svg width="196" height="256" viewBox="0 0 196 256" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M109.337 48.428c0-6.421-2.518-12.579-7-17.12-4.483-4.54-10.563-7.092-16.902-7.094 6.34-.002 12.42-2.554 16.902-7.094s7-10.7 7-17.12c0 6.421 2.518 12.58 7.002 17.121a23.76 23.76 0 0 0 16.905 7.093 23.76 23.76 0 0 0-16.905 7.093c-4.484 4.54-7.002 10.7-7.002 17.12m29.694 44.055a31.5 31.5 0 0 0-11.353-15.22 30.87 30.87 0 0 0-17.943-5.805 30.86 30.86 0 0 0-17.964 5.74 31.5 31.5 0 0 0-11.406 15.18l-25.795 73.16h37.412l17.46-57.182 22.773 77.722 10.669 31.871c3.384 11.026 10.162 20.667 19.343 27.512 9.182 6.846 20.285 10.537 31.687 10.534H196zM22.692 256h1.791a53 53 0 0 0 31.165-10.155c9.085-6.602 15.897-15.923 19.477-26.651l.418-1.249.307-.961H36.44z" fill="#000"/><path d="M79.43 209.426c17.013-3.116 29.917-18.19 29.917-36.321H35.864c-9.511.001-18.633 3.829-25.358 10.64S0 199.793 0 209.426z" fill="#000"/></svg>
|
|
Before Width: | Height: | Size: 924 B |
@ -8,8 +8,7 @@ import { BASE_URL, REPO_PATH } from "@/constants"
|
|||||||
import type { AuthorData, Icon } from "@/types/icons"
|
import type { AuthorData, Icon } from "@/types/icons"
|
||||||
import confetti from "canvas-confetti"
|
import confetti from "canvas-confetti"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
import { Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun } from "lucide-react"
|
import { Check, Copy, Download, FileType, Github, Moon, PaletteIcon, Sun, Type } from "lucide-react"
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useCallback, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
@ -24,14 +23,312 @@ export type IconDetailsProps = {
|
|||||||
authorData: AuthorData
|
authorData: AuthorData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IconVariant({
|
||||||
|
format,
|
||||||
|
iconName,
|
||||||
|
theme,
|
||||||
|
isWordmark = false,
|
||||||
|
iconData,
|
||||||
|
onCopy,
|
||||||
|
onDownload,
|
||||||
|
copiedVariants
|
||||||
|
}: {
|
||||||
|
format: string
|
||||||
|
iconName: string
|
||||||
|
theme?: "light" | "dark"
|
||||||
|
isWordmark?: boolean
|
||||||
|
iconData: Icon
|
||||||
|
onCopy: (url: string, variantKey: string, event?: React.MouseEvent) => void
|
||||||
|
onDownload: (event: React.MouseEvent, url: string, filename: string) => void
|
||||||
|
copiedVariants: Record<string, boolean>
|
||||||
|
}) {
|
||||||
|
let variantName = '';
|
||||||
|
|
||||||
|
if (isWordmark) {
|
||||||
|
if (theme && iconData.wordmark && typeof iconData.wordmark !== 'string') {
|
||||||
|
variantName = iconData.wordmark[theme] || `${iconName}_wordmark_${theme}`;
|
||||||
|
} else if (iconData.wordmark && typeof iconData.wordmark === 'string') {
|
||||||
|
variantName = iconData.wordmark;
|
||||||
|
} else {
|
||||||
|
variantName = `${iconName}_wordmark`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (theme && iconData.colors) {
|
||||||
|
variantName = iconData.colors[theme] || `${iconName}_${theme}`;
|
||||||
|
} else {
|
||||||
|
variantName = iconName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = `${BASE_URL}/${format}/${variantName}.${format}`;
|
||||||
|
const githubUrl = `${REPO_PATH}/tree/main/${format}/${variantName}.${format}`;
|
||||||
|
const variantKey = `${format}-${theme || "default"}${isWordmark ? "-wordmark" : ""}`;
|
||||||
|
const isCopied = copiedVariants[variantKey] || false;
|
||||||
|
const btnCopied = copiedVariants[`btn-${variantKey}`] || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider delayDuration={500}>
|
||||||
|
<MagicCard className="p-0 rounded-md">
|
||||||
|
<div className="flex flex-col items-center p-4 transition-all">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<motion.div
|
||||||
|
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) => onCopy(imageUrl, variantKey, e)}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 border-2 border-transparent group-hover:border-primary/20 rounded-xl z-10 transition-colors" />
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 bg-primary/10 flex items-center justify-center z-20 rounded-xl"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: isCopied ? 1 : 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
{isCopied && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.5, opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 300,
|
||||||
|
damping: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check className="w-8 h-8 text-primary" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src={imageUrl}
|
||||||
|
alt={`${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`}
|
||||||
|
fill
|
||||||
|
className="object-contain p-4"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Click to copy direct URL to clipboard</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<p className="text-sm font-medium">{format.toUpperCase()}</p>
|
||||||
|
|
||||||
|
<div className="flex gap-2 mt-3 w-full justify-center">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 rounded-lg cursor-pointer"
|
||||||
|
onClick={(e) => onDownload(e, imageUrl, `${iconName}.${format}`)}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</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) => onCopy(imageUrl, `btn-${variantKey}`, e)}
|
||||||
|
>
|
||||||
|
{btnCopied && <Check className="w-4 h-4 text-green-500" />}
|
||||||
|
{!btnCopied && <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">
|
||||||
|
<Github className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>View on GitHub</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MagicCard>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function IconVariantsSection({
|
||||||
|
availableFormats,
|
||||||
|
icon,
|
||||||
|
theme,
|
||||||
|
isWordmark = false,
|
||||||
|
iconData,
|
||||||
|
handleCopy,
|
||||||
|
handleDownload,
|
||||||
|
copiedVariants,
|
||||||
|
title,
|
||||||
|
iconElement
|
||||||
|
}: {
|
||||||
|
availableFormats: string[]
|
||||||
|
icon: string
|
||||||
|
theme?: "light" | "dark"
|
||||||
|
isWordmark?: boolean
|
||||||
|
iconData: Icon
|
||||||
|
handleCopy: (url: string, variantKey: string, event?: React.MouseEvent) => void
|
||||||
|
handleDownload: (event: React.MouseEvent, url: string, filename: string) => void
|
||||||
|
copiedVariants: Record<string, boolean>
|
||||||
|
title: string
|
||||||
|
iconElement: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
{iconElement}
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg">
|
||||||
|
{availableFormats.map((format) => (
|
||||||
|
<IconVariant
|
||||||
|
key={format}
|
||||||
|
format={format}
|
||||||
|
iconName={icon}
|
||||||
|
theme={theme}
|
||||||
|
isWordmark={isWordmark}
|
||||||
|
iconData={iconData}
|
||||||
|
onCopy={handleCopy}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
copiedVariants={copiedVariants}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function WordmarkSection({
|
||||||
|
iconData,
|
||||||
|
icon,
|
||||||
|
availableFormats,
|
||||||
|
handleCopy,
|
||||||
|
handleDownload,
|
||||||
|
copiedVariants
|
||||||
|
}: {
|
||||||
|
iconData: Icon
|
||||||
|
icon: string
|
||||||
|
availableFormats: string[]
|
||||||
|
handleCopy: (url: string, variantKey: string, event?: React.MouseEvent) => void
|
||||||
|
handleDownload: (event: React.MouseEvent, url: string, filename: string) => void
|
||||||
|
copiedVariants: Record<string, boolean>
|
||||||
|
}) {
|
||||||
|
if (!iconData.wordmark) return null;
|
||||||
|
|
||||||
|
const isStringWordmark = typeof iconData.wordmark === 'string';
|
||||||
|
const hasLightDarkVariants = !isStringWordmark && (iconData.wordmark.light || iconData.wordmark.dark);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
<Type className="w-4 h-4 text-green-500" />
|
||||||
|
Wordmark variants
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{(isStringWordmark || !hasLightDarkVariants) && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg">
|
||||||
|
{availableFormats.map((format) => (
|
||||||
|
<IconVariant
|
||||||
|
key={format}
|
||||||
|
format={format}
|
||||||
|
iconName={icon}
|
||||||
|
isWordmark={true}
|
||||||
|
iconData={iconData}
|
||||||
|
onCopy={handleCopy}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
copiedVariants={copiedVariants}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasLightDarkVariants && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{iconData.wordmark.light && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md font-medium flex items-center gap-2 ml-4 mb-2">
|
||||||
|
<Sun className="w-3 h-3 text-amber-500" />
|
||||||
|
Light
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg">
|
||||||
|
{availableFormats.map((format) => (
|
||||||
|
<IconVariant
|
||||||
|
key={format}
|
||||||
|
format={format}
|
||||||
|
iconName={icon}
|
||||||
|
theme="light"
|
||||||
|
isWordmark={true}
|
||||||
|
iconData={iconData}
|
||||||
|
onCopy={handleCopy}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
copiedVariants={copiedVariants}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{iconData.wordmark.dark && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md font-medium flex items-center gap-2 ml-4 mb-2">
|
||||||
|
<Moon className="w-3 h-3 text-indigo-500" />
|
||||||
|
Dark
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg">
|
||||||
|
{availableFormats.map((format) => (
|
||||||
|
<IconVariant
|
||||||
|
key={format}
|
||||||
|
format={format}
|
||||||
|
iconName={icon}
|
||||||
|
theme="dark"
|
||||||
|
isWordmark={true}
|
||||||
|
iconData={iconData}
|
||||||
|
onCopy={handleCopy}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
copiedVariants={copiedVariants}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
export function IconDetails({ icon, iconData, authorData }: 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":
|
||||||
@ -46,7 +343,6 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
const availableFormats = getAvailableFormats()
|
const availableFormats = getAvailableFormats()
|
||||||
const [copiedVariants, setCopiedVariants] = useState<Record<string, boolean>>({})
|
const [copiedVariants, setCopiedVariants] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
// Launch confetti from the pointer position
|
|
||||||
const launchConfetti = useCallback((originX?: number, originY?: number) => {
|
const launchConfetti = useCallback((originX?: number, originY?: number) => {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
startVelocity: 15,
|
startVelocity: 15,
|
||||||
@ -57,7 +353,6 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
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,
|
||||||
@ -68,7 +363,6 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Default to center of screen
|
|
||||||
confetti({
|
confetti({
|
||||||
...defaults,
|
...defaults,
|
||||||
particleCount: 50,
|
particleCount: 50,
|
||||||
@ -90,7 +384,6 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
}))
|
}))
|
||||||
}, 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 {
|
||||||
@ -104,27 +397,18 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@ -141,120 +425,9 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderVariant = (format: string, iconName: string, theme?: "light" | "dark") => {
|
|
||||||
const variantName = theme && iconColorVariants?.[theme] ? iconColorVariants[theme] : iconName
|
|
||||||
const imageUrl = `${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 (
|
|
||||||
<TooltipProvider key={variantKey} delayDuration={500}>
|
|
||||||
<MagicCard className="p-0 rounded-md">
|
|
||||||
<div className="flex flex-col items-center p-4 transition-all">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<motion.div
|
|
||||||
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)}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 border-2 border-transparent group-hover:border-primary/20 rounded-xl z-10 transition-colors" />
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 bg-primary/10 flex items-center justify-center z-20 rounded-xl"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: isCopied ? 1 : 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 0.5, opacity: 0 }}
|
|
||||||
animate={{
|
|
||||||
scale: isCopied ? 1 : 0.5,
|
|
||||||
opacity: isCopied ? 1 : 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 300,
|
|
||||||
damping: 20,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Check className="w-8 h-8 text-primary" />
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<Image
|
|
||||||
src={imageUrl}
|
|
||||||
alt={`${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`}
|
|
||||||
fill
|
|
||||||
className="object-contain p-4"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Click to copy direct URL to clipboard</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<p className="text-sm font-medium">{format.toUpperCase()}</p>
|
|
||||||
|
|
||||||
<div className="flex gap-2 mt-3 w-full justify-center">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 rounded-lg cursor-pointer"
|
|
||||||
onClick={(e) => handleDownload(e, imageUrl, `${iconName}.${format}`)}
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
</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)}
|
|
||||||
>
|
|
||||||
{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">
|
|
||||||
<Github className="w-4 h-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>View on GitHub</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MagicCard>
|
|
||||||
</TooltipProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto pt-12 pb-14">
|
<div className="container mx-auto pt-12 pb-14">
|
||||||
<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">
|
||||||
@ -287,7 +460,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
<AvatarImage src={authorData.avatar_url} alt={authorName} />
|
<AvatarImage src={authorData.avatar_url} alt={authorName} />
|
||||||
<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"
|
||||||
@ -296,7 +469,8 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
>
|
>
|
||||||
{authorName}
|
{authorName}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
)}
|
||||||
|
{!authorData.html_url && (
|
||||||
<span className="text-sm">{authorName}</span>
|
<span className="text-sm">{authorName}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -348,11 +522,15 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
<div className="text-xs text-muted-foreground space-y-2">
|
<div className="text-xs text-muted-foreground space-y-2">
|
||||||
<p>
|
<p>
|
||||||
Available in{" "}
|
Available in{" "}
|
||||||
{availableFormats.length > 1
|
{availableFormats.length > 1 && (
|
||||||
? `${availableFormats.length} formats (${availableFormats.map((f) => f.toUpperCase()).join(", ")})`
|
`${availableFormats.length} formats (${availableFormats.map((f) => f.toUpperCase()).join(", ")})`
|
||||||
: `${availableFormats[0].toUpperCase()} format`}{" "}
|
)}
|
||||||
|
{availableFormats.length === 1 && (
|
||||||
|
`${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>
|
||||||
Use the {icon} icon in your web applications, dashboards, or documentation to enhance visual communication and user
|
Use the {icon} icon in your web applications, dashboards, or documentation to enhance visual communication and user
|
||||||
@ -365,7 +543,6 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
</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>
|
||||||
@ -373,37 +550,61 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
<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">
|
<IconVariantsSection
|
||||||
{availableFormats.map((format) => renderVariant(format, icon))}
|
availableFormats={availableFormats}
|
||||||
</div>
|
icon={icon}
|
||||||
) : (
|
iconData={iconData}
|
||||||
<div className="space-y-10">
|
handleCopy={handleCopy}
|
||||||
<div>
|
handleDownload={handleDownload}
|
||||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
copiedVariants={copiedVariants}
|
||||||
<Sun className="w-4 h-4 text-amber-500" />
|
title="Default"
|
||||||
Light theme
|
iconElement={<FileType className="w-4 h-4 text-blue-500" />}
|
||||||
</h3>
|
/>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg ">
|
|
||||||
{availableFormats.map((format) => renderVariant(format, icon, "light"))}
|
{iconData.colors && (
|
||||||
</div>
|
<>
|
||||||
</div>
|
<IconVariantsSection
|
||||||
<div>
|
availableFormats={availableFormats}
|
||||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
icon={icon}
|
||||||
<Moon className="w-4 h-4 text-indigo-500" />
|
theme="light"
|
||||||
Dark theme
|
iconData={iconData}
|
||||||
</h3>
|
handleCopy={handleCopy}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 p-3 rounded-lg ">
|
handleDownload={handleDownload}
|
||||||
{availableFormats.map((format) => renderVariant(format, icon, "dark"))}
|
copiedVariants={copiedVariants}
|
||||||
</div>
|
title="Light theme"
|
||||||
</div>
|
iconElement={<Sun className="w-4 h-4 text-amber-500" />}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
|
<IconVariantsSection
|
||||||
|
availableFormats={availableFormats}
|
||||||
|
icon={icon}
|
||||||
|
theme="dark"
|
||||||
|
iconData={iconData}
|
||||||
|
handleCopy={handleCopy}
|
||||||
|
handleDownload={handleDownload}
|
||||||
|
copiedVariants={copiedVariants}
|
||||||
|
title="Dark theme"
|
||||||
|
iconElement={<Moon className="w-4 h-4 text-indigo-500" />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{iconData.wordmark && (
|
||||||
|
<WordmarkSection
|
||||||
|
iconData={iconData}
|
||||||
|
icon={icon}
|
||||||
|
availableFormats={availableFormats}
|
||||||
|
handleCopy={handleCopy}
|
||||||
|
handleDownload={handleDownload}
|
||||||
|
copiedVariants={copiedVariants}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
@ -445,6 +646,28 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
</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">Source</h3>
|
<h3 className="text-sm font-semibold text-muted-foreground">Source</h3>
|
||||||
<Button variant="outline" className="w-full" asChild>
|
<Button variant="outline" className="w-full" asChild>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const BASE_URL = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons"
|
export const BASE_URL = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@feat/text-icons"
|
||||||
export const REPO_PATH = "https://github.com/homarr-labs/dashboard-icons"
|
export const REPO_PATH = "https://github.com/homarr-labs/dashboard-icons"
|
||||||
// TODO: Change back before merge
|
// TODO: Change back before merge
|
||||||
export const METADATA_URL = "https://raw.githubusercontent.com/homarr-labs/dashboard-icons/refs/heads/feat/text-icons/metadata.json"
|
export const METADATA_URL = "https://raw.githubusercontent.com/homarr-labs/dashboard-icons/refs/heads/feat/text-icons/metadata.json"
|
||||||
|
3568
web/src/lib/isvg.ts
Normal file
3568
web/src/lib/isvg.ts
Normal file
File diff suppressed because it is too large
Load Diff
39
web/src/scripts/README.md
Normal file
39
web/src/scripts/README.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Icon Management Scripts
|
||||||
|
|
||||||
|
This directory contains scripts for managing the dashboard icons.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
### `merge-icons.ts`
|
||||||
|
|
||||||
|
This script merges data from `src/lib/isvg.ts` into the metadata.json file located at the repository root. It adds wordmark information to existing icons and creates new entries for icons that don't exist yet.
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using the npm script
|
||||||
|
npm run merge-icons
|
||||||
|
|
||||||
|
# Or with pnpm
|
||||||
|
pnpm merge-icons
|
||||||
|
```
|
||||||
|
|
||||||
|
### `download-icons.ts`
|
||||||
|
|
||||||
|
This script downloads icons from the [svgl](https://github.com/pheralb/svgl) repository to the `svg` folder. It skips existing icons (except for wordmark icons which are always downloaded).
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using the npm script
|
||||||
|
npm run download-icons
|
||||||
|
|
||||||
|
# Or with pnpm
|
||||||
|
pnpm download-icons
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `merge-icons.ts` script will add new icons to the metadata.json file with the author set to "ajnart" and a timestamp of "2025-04-20T12:00:00Z".
|
||||||
|
- The `download-icons.ts` script will create the `svg` directory if it doesn't exist.
|
||||||
|
- Both scripts log their progress to the console.
|
121
web/src/scripts/cleanup-icons.ts
Normal file
121
web/src/scripts/cleanup-icons.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
// Define the file paths - this script is in /web/src/scripts
|
||||||
|
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
|
||||||
|
const METADATA_PATH = path.join(SCRIPT_DIR, '../../../metadata.json');
|
||||||
|
const SVG_DIR = path.join(SCRIPT_DIR, '../../../svg');
|
||||||
|
|
||||||
|
interface MetadataItem {
|
||||||
|
base: string;
|
||||||
|
aliases: string[];
|
||||||
|
categories: string[];
|
||||||
|
update: {
|
||||||
|
timestamp: string;
|
||||||
|
author: {
|
||||||
|
id: number | string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
wordmark?: string | {
|
||||||
|
light: string;
|
||||||
|
dark: string;
|
||||||
|
};
|
||||||
|
colors?: {
|
||||||
|
dark: string;
|
||||||
|
light: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Metadata {
|
||||||
|
[key: string]: MetadataItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to normalize a name by removing light/dark suffixes and replacing underscores
|
||||||
|
function normalizeName(name: string): string {
|
||||||
|
return name.replace(/_/g, '-').replace(/-(light|dark)(-wordmark)?$/i, '$2');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to collect all valid icon names from metadata
|
||||||
|
function collectValidIconNames(metadata: Metadata): Set<string> {
|
||||||
|
const validNames = new Set<string>();
|
||||||
|
|
||||||
|
for (const [key, data] of Object.entries(metadata)) {
|
||||||
|
// Add the main key (icon name)
|
||||||
|
validNames.add(key.replace(/_/g, '-'));
|
||||||
|
|
||||||
|
// Add wordmark if it exists
|
||||||
|
if (data.wordmark) {
|
||||||
|
if (typeof data.wordmark === 'string') {
|
||||||
|
validNames.add(data.wordmark.replace(/_/g, '-'));
|
||||||
|
} else {
|
||||||
|
if (data.wordmark.light) validNames.add(data.wordmark.light.replace(/_/g, '-'));
|
||||||
|
if (data.wordmark.dark) validNames.add(data.wordmark.dark.replace(/_/g, '-'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add colors if they exist
|
||||||
|
if (data.colors) {
|
||||||
|
if (data.colors.light) validNames.add(data.colors.light.replace(/_/g, '-'));
|
||||||
|
if (data.colors.dark) validNames.add(data.colors.dark.replace(/_/g, '-'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Starting cleanup of unused SVG files...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read and parse metadata.json
|
||||||
|
console.log(`Reading metadata from ${METADATA_PATH}`);
|
||||||
|
const metadataContent = await fs.promises.readFile(METADATA_PATH, 'utf8');
|
||||||
|
const metadata: Metadata = JSON.parse(metadataContent);
|
||||||
|
|
||||||
|
// Collect all valid icon names
|
||||||
|
const validIconNames = collectValidIconNames(metadata);
|
||||||
|
console.log(`Found ${validIconNames.size} valid icon names in metadata`);
|
||||||
|
|
||||||
|
// Read all SVG files in the directory
|
||||||
|
const svgFiles = await fs.promises.readdir(SVG_DIR);
|
||||||
|
console.log(`Found ${svgFiles.length} SVG files in directory`);
|
||||||
|
|
||||||
|
// Files to delete
|
||||||
|
const filesToDelete: string[] = [];
|
||||||
|
|
||||||
|
// Check each SVG file
|
||||||
|
for (const file of svgFiles) {
|
||||||
|
if (!file.endsWith('.svg')) continue;
|
||||||
|
|
||||||
|
// Get the base name without extension
|
||||||
|
const baseName = file.replace('.svg', '');
|
||||||
|
|
||||||
|
// Convert any underscores to hyphens for comparison with metadata
|
||||||
|
const normalizedBaseName = baseName.replace(/_/g, '-');
|
||||||
|
|
||||||
|
// If not in valid names, mark for deletion
|
||||||
|
if (!validIconNames.has(normalizedBaseName)) {
|
||||||
|
filesToDelete.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete unused files
|
||||||
|
console.log(`Found ${filesToDelete.length} unused SVG files to delete`);
|
||||||
|
for (const file of filesToDelete) {
|
||||||
|
const filePath = path.join(SVG_DIR, file);
|
||||||
|
await fs.promises.unlink(filePath);
|
||||||
|
console.log(`Deleted: ${file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Cleanup completed successfully!');
|
||||||
|
console.log(`Deleted ${filesToDelete.length} unused SVG files`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during cleanup:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
274
web/src/scripts/download-icons.ts
Normal file
274
web/src/scripts/download-icons.ts
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import https from 'node:https';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
// Import the SVG data
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { svgs } = require('../lib/isvg') as { svgs: ISVG[] };
|
||||||
|
|
||||||
|
// Define the ISVG interface
|
||||||
|
interface ISVG {
|
||||||
|
title: string;
|
||||||
|
category: string | string[];
|
||||||
|
route: string | {
|
||||||
|
light?: string;
|
||||||
|
dark?: string;
|
||||||
|
};
|
||||||
|
wordmark?: string | {
|
||||||
|
light?: string;
|
||||||
|
dark?: string;
|
||||||
|
};
|
||||||
|
url?: string;
|
||||||
|
brandUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base URL for GitHub raw content
|
||||||
|
const BASE_URL = 'https://raw.githubusercontent.com/pheralb/svgl/refs/heads/main/static';
|
||||||
|
// Target directory (relative to the script location)
|
||||||
|
const TARGET_DIR = path.join(__dirname, '../../..', 'svg');
|
||||||
|
|
||||||
|
// Create the target directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(TARGET_DIR)) {
|
||||||
|
fs.mkdirSync(TARGET_DIR, { recursive: true });
|
||||||
|
console.log(`Created target directory: ${TARGET_DIR}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a list of existing files in the target directory
|
||||||
|
const existingFiles = fs.readdirSync(TARGET_DIR);
|
||||||
|
console.log(`Found ${existingFiles.length} existing files in ${TARGET_DIR}`);
|
||||||
|
|
||||||
|
// Helper function to download a file
|
||||||
|
async function downloadFile(url: string, destPath: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fileStream = fs.createWriteStream(destPath);
|
||||||
|
https.get(url, (response) => {
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
fs.unlinkSync(destPath); // Remove the file if download failed
|
||||||
|
reject(new Error(`Failed to download ${url}: ${response.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.pipe(fileStream);
|
||||||
|
|
||||||
|
fileStream.on('finish', () => {
|
||||||
|
fileStream.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
fileStream.on('error', (err) => {
|
||||||
|
fs.unlinkSync(destPath); // Remove the file if there was an error
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
}).on('error', (err) => {
|
||||||
|
fs.unlinkSync(destPath); // Remove the file if there was an error
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get base icon name from route
|
||||||
|
function getBaseIconName(route: string): string {
|
||||||
|
// Extract basename, remove -icon/-logo, replace underscores with hyphens
|
||||||
|
const filename = path.basename(route, '.svg')
|
||||||
|
.replace(/(-icon|-logo)$/g, '')
|
||||||
|
.replace(/_/g, '-');
|
||||||
|
|
||||||
|
// Remove light/dark suffixes to get the base name
|
||||||
|
return filename.replace(/-(light|dark)$/i, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map to store icon keys to their base names
|
||||||
|
const iconKeyMap = new Map<string, string>();
|
||||||
|
|
||||||
|
// First pass to build the icon key map
|
||||||
|
function buildIconKeyMap() {
|
||||||
|
for (const svg of svgs) {
|
||||||
|
let routePath: string;
|
||||||
|
|
||||||
|
if (typeof svg.route === 'string') {
|
||||||
|
routePath = svg.route;
|
||||||
|
} else if (svg.route.light) {
|
||||||
|
routePath = svg.route.light;
|
||||||
|
} else if (svg.route.dark) {
|
||||||
|
routePath = svg.route.dark || '';
|
||||||
|
} else {
|
||||||
|
routePath = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseIconName = getBaseIconName(routePath);
|
||||||
|
iconKeyMap.set(svg.title, baseIconName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the key map before processing
|
||||||
|
buildIconKeyMap();
|
||||||
|
|
||||||
|
// Helper function to check if a file already exists
|
||||||
|
function fileExists(filename: string): boolean {
|
||||||
|
return existingFiles.includes(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format URL correctly by joining BASE_URL with the route path
|
||||||
|
function getFullUrl(route: string): string {
|
||||||
|
// Remove leading slash if present
|
||||||
|
const cleanRoute = route.startsWith('/') ? route.substring(1) : route;
|
||||||
|
return `${BASE_URL}/${cleanRoute}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each SVG icon
|
||||||
|
async function processIcons() {
|
||||||
|
let downloadCount = 0;
|
||||||
|
let skipCount = 0;
|
||||||
|
const failedDownloads: string[] = [];
|
||||||
|
|
||||||
|
for (const svg of svgs) {
|
||||||
|
try {
|
||||||
|
// Get base name for this icon
|
||||||
|
const baseIconName = iconKeyMap.get(svg.title) || '';
|
||||||
|
|
||||||
|
// Process the main route
|
||||||
|
if (typeof svg.route === 'string') {
|
||||||
|
// Simple string route - use base name
|
||||||
|
const filename = `${baseIconName}.svg`;
|
||||||
|
if (!fileExists(filename)) {
|
||||||
|
const url = getFullUrl(svg.route);
|
||||||
|
const destPath = path.join(TARGET_DIR, filename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadFile(url, destPath);
|
||||||
|
console.log(`Downloaded: ${filename} (from ${path.basename(svg.route)})`);
|
||||||
|
downloadCount++;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`Error downloading ${filename}: ${errorMessage}`);
|
||||||
|
failedDownloads.push(filename);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Skipping existing file: ${filename}`);
|
||||||
|
skipCount++;
|
||||||
|
}
|
||||||
|
} else if (typeof svg.route === 'object') {
|
||||||
|
// Object with light/dark variants
|
||||||
|
if (svg.route.light) {
|
||||||
|
const filename = `${baseIconName}-light.svg`;
|
||||||
|
if (!fileExists(filename)) {
|
||||||
|
const url = getFullUrl(svg.route.light);
|
||||||
|
const destPath = path.join(TARGET_DIR, filename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadFile(url, destPath);
|
||||||
|
console.log(`Downloaded: ${filename} (from ${path.basename(svg.route.light)})`);
|
||||||
|
downloadCount++;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`Error downloading ${filename}: ${errorMessage}`);
|
||||||
|
failedDownloads.push(filename);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Skipping existing file: ${filename}`);
|
||||||
|
skipCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (svg.route.dark) {
|
||||||
|
const filename = `${baseIconName}-dark.svg`;
|
||||||
|
if (!fileExists(filename)) {
|
||||||
|
const url = getFullUrl(svg.route.dark);
|
||||||
|
const destPath = path.join(TARGET_DIR, filename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadFile(url, destPath);
|
||||||
|
console.log(`Downloaded: ${filename} (from ${path.basename(svg.route.dark)})`);
|
||||||
|
downloadCount++;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`Error downloading ${filename}: ${errorMessage}`);
|
||||||
|
failedDownloads.push(filename);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Skipping existing file: ${filename}`);
|
||||||
|
skipCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process wordmark if present
|
||||||
|
if (svg.wordmark) {
|
||||||
|
if (typeof svg.wordmark === 'string') {
|
||||||
|
// Simple string wordmark
|
||||||
|
const filename = `${baseIconName}-wordmark.svg`;
|
||||||
|
|
||||||
|
// Download even if it exists because we want all wordmarks
|
||||||
|
const url = getFullUrl(svg.wordmark);
|
||||||
|
const destPath = path.join(TARGET_DIR, filename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadFile(url, destPath);
|
||||||
|
console.log(`Downloaded wordmark: ${filename} (from ${path.basename(svg.wordmark)})`);
|
||||||
|
downloadCount++;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`Error downloading wordmark ${filename}: ${errorMessage}`);
|
||||||
|
failedDownloads.push(filename);
|
||||||
|
}
|
||||||
|
} else if (typeof svg.wordmark === 'object') {
|
||||||
|
// Object with light/dark variants
|
||||||
|
if (svg.wordmark.light) {
|
||||||
|
const filename = `${baseIconName}-wordmark-light.svg`;
|
||||||
|
|
||||||
|
// Download even if it exists because we want all wordmarks
|
||||||
|
const url = getFullUrl(svg.wordmark.light);
|
||||||
|
const destPath = path.join(TARGET_DIR, filename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadFile(url, destPath);
|
||||||
|
console.log(`Downloaded wordmark: ${filename} (from ${path.basename(svg.wordmark.light)})`);
|
||||||
|
downloadCount++;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`Error downloading wordmark ${filename}: ${errorMessage}`);
|
||||||
|
failedDownloads.push(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (svg.wordmark.dark) {
|
||||||
|
const filename = `${baseIconName}-wordmark-dark.svg`;
|
||||||
|
|
||||||
|
// Download even if it exists because we want all wordmarks
|
||||||
|
const url = getFullUrl(svg.wordmark.dark);
|
||||||
|
const destPath = path.join(TARGET_DIR, filename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadFile(url, destPath);
|
||||||
|
console.log(`Downloaded wordmark: ${filename} (from ${path.basename(svg.wordmark.dark)})`);
|
||||||
|
downloadCount++;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`Error downloading wordmark ${filename}: ${errorMessage}`);
|
||||||
|
failedDownloads.push(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`Error processing ${svg.title}: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nSummary:');
|
||||||
|
console.log(`Total icons processed: ${svgs.length}`);
|
||||||
|
console.log(`Downloaded: ${downloadCount}`);
|
||||||
|
console.log(`Skipped: ${skipCount}`);
|
||||||
|
|
||||||
|
if (failedDownloads.length > 0) {
|
||||||
|
console.log(`Failed downloads: ${failedDownloads.length}`);
|
||||||
|
console.log(`Failed files: ${failedDownloads.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
processIcons().catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
190
web/src/scripts/merge-icons.ts
Normal file
190
web/src/scripts/merge-icons.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
// Define the types
|
||||||
|
interface ISVG {
|
||||||
|
title: string;
|
||||||
|
category: string | string[];
|
||||||
|
route: string | {
|
||||||
|
light?: string;
|
||||||
|
dark?: string;
|
||||||
|
};
|
||||||
|
wordmark?: string | {
|
||||||
|
light?: string;
|
||||||
|
dark?: string;
|
||||||
|
};
|
||||||
|
url?: string;
|
||||||
|
brandUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetadataItem {
|
||||||
|
base: string;
|
||||||
|
aliases: string[];
|
||||||
|
categories: string[];
|
||||||
|
update: {
|
||||||
|
timestamp: string;
|
||||||
|
author: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
wordmark?: string | {
|
||||||
|
light: string;
|
||||||
|
dark: string;
|
||||||
|
};
|
||||||
|
colors?: {
|
||||||
|
dark: string;
|
||||||
|
light: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Metadata {
|
||||||
|
[key: string]: MetadataItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import the SVG data
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { svgs } = require('../lib/isvg') as { svgs: ISVG[] };
|
||||||
|
|
||||||
|
// Path to the metadata file (relative to the current script)
|
||||||
|
const metadataPath = path.join(__dirname, '../../..', 'metadata.json');
|
||||||
|
|
||||||
|
// Read existing metadata
|
||||||
|
let metadata: Metadata = {};
|
||||||
|
try {
|
||||||
|
const metadataContent = fs.readFileSync(metadataPath, 'utf8');
|
||||||
|
metadata = JSON.parse(metadataContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading metadata file:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The author details
|
||||||
|
const author = {
|
||||||
|
id: 49837342,
|
||||||
|
name: "ajnart"
|
||||||
|
};
|
||||||
|
|
||||||
|
// The timestamp
|
||||||
|
const timestamp = "2025-04-25T12:00:00Z";
|
||||||
|
|
||||||
|
// Helper function to convert the author ID to string, since that's the expected type
|
||||||
|
function getAuthorWithStringId(authorObj: { id: number | string; name: string }): { id: string; name: string } {
|
||||||
|
return {
|
||||||
|
...authorObj,
|
||||||
|
id: String(authorObj.id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert iSVG format to metadata format
|
||||||
|
for (const svg of svgs) {
|
||||||
|
// Extract the key from the route path
|
||||||
|
let routePath: string;
|
||||||
|
|
||||||
|
if (typeof svg.route === 'string') {
|
||||||
|
routePath = svg.route;
|
||||||
|
} else if (svg.route.light) {
|
||||||
|
routePath = svg.route.light;
|
||||||
|
} else if (svg.route.dark) {
|
||||||
|
routePath = svg.route.dark || '';
|
||||||
|
} else {
|
||||||
|
routePath = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the filename without extension and remove suffixes
|
||||||
|
// Also replace underscores with hyphens
|
||||||
|
let key = path.basename(routePath, '.svg')
|
||||||
|
.replace(/(-icon|-logo)$/g, '')
|
||||||
|
.replace(/_/g, '-');
|
||||||
|
|
||||||
|
// Remove -light or -dark suffixes for the base key
|
||||||
|
key = key.replace(/-(light|dark)$/i, '');
|
||||||
|
|
||||||
|
// Check if the icon already exists
|
||||||
|
if (metadata[key]) {
|
||||||
|
console.log(`Icon "${svg.title}" already exists in metadata as "${key}"`);
|
||||||
|
|
||||||
|
// Update the existing entry with wordmark if it doesn't have one
|
||||||
|
if (svg.wordmark && !metadata[key].wordmark) {
|
||||||
|
console.log(`Adding wordmark to existing icon "${key}"`);
|
||||||
|
|
||||||
|
if (typeof svg.wordmark === 'string') {
|
||||||
|
// Instead of using the original wordmark name, use standardized naming
|
||||||
|
metadata[key].wordmark = `${key}-wordmark`;
|
||||||
|
} else if (typeof svg.wordmark === 'object') {
|
||||||
|
// Object with light/dark variants - use standardized naming
|
||||||
|
metadata[key].wordmark = {
|
||||||
|
light: svg.wordmark.light ? `${key}-wordmark-light` : '',
|
||||||
|
dark: svg.wordmark.dark ? `${key}-wordmark-dark` : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the timestamp and author
|
||||||
|
metadata[key].update = {
|
||||||
|
timestamp,
|
||||||
|
author: getAuthorWithStringId(author)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If route contains light/dark values, update colors with standardized naming
|
||||||
|
if (typeof svg.route === 'object' && (svg.route.light || svg.route.dark)) {
|
||||||
|
if (!metadata[key].colors) {
|
||||||
|
metadata[key].colors = {
|
||||||
|
dark: svg.route.dark ? `${key}-dark` : '',
|
||||||
|
light: svg.route.light ? `${key}-light` : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create a new metadata entry
|
||||||
|
const newEntry: MetadataItem = {
|
||||||
|
base: "svg", // Assuming SVG format
|
||||||
|
aliases: [],
|
||||||
|
categories: [],
|
||||||
|
update: {
|
||||||
|
timestamp,
|
||||||
|
author: getAuthorWithStringId(author)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle categories
|
||||||
|
if (Array.isArray(svg.category)) {
|
||||||
|
newEntry.categories = svg.category;
|
||||||
|
} else if (typeof svg.category === 'string') {
|
||||||
|
newEntry.categories = [svg.category];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle wordmark if present with standardized naming
|
||||||
|
if (svg.wordmark) {
|
||||||
|
if (typeof svg.wordmark === 'string') {
|
||||||
|
newEntry.wordmark = `${key}-wordmark`;
|
||||||
|
} else if (typeof svg.wordmark === 'object') {
|
||||||
|
newEntry.wordmark = {
|
||||||
|
light: svg.wordmark.light ? `${key}-wordmark-light` : '',
|
||||||
|
dark: svg.wordmark.dark ? `${key}-wordmark-dark` : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle colors from route with standardized naming
|
||||||
|
if (typeof svg.route === 'object' && (svg.route.light || svg.route.dark)) {
|
||||||
|
newEntry.colors = {
|
||||||
|
dark: svg.route.dark ? `${key}-dark` : '',
|
||||||
|
light: svg.route.light ? `${key}-light` : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to metadata
|
||||||
|
metadata[key] = newEntry;
|
||||||
|
console.log(`Added new icon "${svg.title}" as "${key}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the updated metadata back to the file
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 4));
|
||||||
|
console.log(`Successfully updated metadata file at ${metadataPath}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing metadata file:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
@ -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 = {
|
||||||
|
14
web/src/types/svg.ts
Normal file
14
web/src/types/svg.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export type iSVG = {
|
||||||
|
title: string
|
||||||
|
category: string | string[]
|
||||||
|
route: string | {
|
||||||
|
light?: string
|
||||||
|
dark?: string
|
||||||
|
}
|
||||||
|
wordmark?: string | {
|
||||||
|
light?: string
|
||||||
|
dark?: string
|
||||||
|
}
|
||||||
|
url?: string
|
||||||
|
brandUrl?: string
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user