mirror of
https://github.com/walkxcode/dashboard-icons.git
synced 2025-11-18 09:37:30 +01:00
refactor(icons): improve icon detail components
- Update icon details, actions, and editable components - Enhance icon name combobox functionality - Improve user interaction with icon metadata
This commit is contained in:
@@ -2,19 +2,7 @@
|
|||||||
|
|
||||||
import confetti from "canvas-confetti"
|
import confetti from "canvas-confetti"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
import {
|
import { ArrowRight, Check, FileType, Github, Moon, PaletteIcon, Plus, Sun, Type, Upload, X } from "lucide-react"
|
||||||
ArrowRight,
|
|
||||||
Check,
|
|
||||||
FileType,
|
|
||||||
Github,
|
|
||||||
Moon,
|
|
||||||
PaletteIcon,
|
|
||||||
Plus,
|
|
||||||
Sun,
|
|
||||||
Type,
|
|
||||||
Upload,
|
|
||||||
X,
|
|
||||||
} from "lucide-react"
|
|
||||||
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 type React from "react"
|
||||||
@@ -28,11 +16,12 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
import { BASE_URL, REPO_PATH } from "@/constants"
|
import { BASE_URL, REPO_PATH } from "@/constants"
|
||||||
|
import { pb } from "@/lib/pb"
|
||||||
import { formatIconName } from "@/lib/utils"
|
import { formatIconName } from "@/lib/utils"
|
||||||
|
import { revalidateAllSubmissions } from "@/app/actions/submissions"
|
||||||
import { MagicCard } from "./magicui/magic-card"
|
import { MagicCard } from "./magicui/magic-card"
|
||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
import { Dropzone, DropzoneContent, DropzoneEmptyState } from "./ui/shadcn-io/dropzone"
|
import { Dropzone, DropzoneContent, DropzoneEmptyState } from "./ui/shadcn-io/dropzone"
|
||||||
import { pb } from "@/lib/pb"
|
|
||||||
|
|
||||||
interface VariantFile {
|
interface VariantFile {
|
||||||
file: File
|
file: File
|
||||||
@@ -128,13 +117,7 @@ function AddVariantCard({ onAddVariant, existingTypes }: AddVariantCardProps) {
|
|||||||
{label}
|
{label}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button type="button" variant="ghost" size="sm" className="w-full" onClick={() => setShowOptions(false)}>
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => setShowOptions(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,12 +155,7 @@ function VariantCard({ variant, onRemove, canRemove }: VariantCardProps) {
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="relative w-28 h-28 mb-3 rounded-xl overflow-hidden">
|
<div className="relative w-28 h-28 mb-3 rounded-xl overflow-hidden">
|
||||||
<div className="absolute inset-0 border-2 border-primary/20 rounded-xl z-10" />
|
<div className="absolute inset-0 border-2 border-primary/20 rounded-xl z-10" />
|
||||||
<Image
|
<Image src={variant.preview} alt={`${variant.label} preview`} fill className="object-contain p-4" />
|
||||||
src={variant.preview}
|
|
||||||
alt={`${variant.label} preview`}
|
|
||||||
fill
|
|
||||||
className="object-contain p-4"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -186,9 +164,7 @@ function VariantCard({ variant, onRemove, canRemove }: VariantCardProps) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<p className="text-sm font-medium">{variant.label}</p>
|
<p className="text-sm font-medium">{variant.label}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">{variant.file.name.split(".").pop()?.toUpperCase()}</p>
|
||||||
{variant.file.name.split(".").pop()?.toUpperCase()}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</MagicCard>
|
</MagicCard>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -268,11 +244,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleCategory = (category: string) => {
|
const toggleCategory = (category: string) => {
|
||||||
setCategories(
|
setCategories(categories.includes(category) ? categories.filter((c) => c !== category) : [...categories, category])
|
||||||
categories.includes(category)
|
|
||||||
? categories.filter((c) => c !== category)
|
|
||||||
: [...categories, category]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddAlias = () => {
|
const handleAddAlias = () => {
|
||||||
@@ -365,13 +337,16 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
const submissionData = {
|
const submissionData = {
|
||||||
name: iconName,
|
name: iconName,
|
||||||
assets: assetFiles,
|
assets: assetFiles,
|
||||||
created_by: pb.authStore.model?.id,
|
created_by: pb.authStore.record?.id,
|
||||||
status: "pending",
|
status: "pending",
|
||||||
extras: extras,
|
extras: extras,
|
||||||
}
|
}
|
||||||
|
|
||||||
await pb.collection("submissions").create(submissionData)
|
await pb.collection("submissions").create(submissionData)
|
||||||
|
|
||||||
|
// Revalidate Next.js cache for community pages
|
||||||
|
await revalidateAllSubmissions()
|
||||||
|
|
||||||
launchConfetti()
|
launchConfetti()
|
||||||
toast.success("Icon submitted!", {
|
toast.success("Icon submitted!", {
|
||||||
description: `Your icon "${iconName}" has been submitted for review`,
|
description: `Your icon "${iconName}" has been submitted for review`,
|
||||||
@@ -443,10 +418,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
<Label htmlFor="icon-name" className="text-sm font-medium mb-2 block">
|
<Label htmlFor="icon-name" className="text-sm font-medium mb-2 block">
|
||||||
Icon Name
|
Icon Name
|
||||||
</Label>
|
</Label>
|
||||||
<IconNameCombobox
|
<IconNameCombobox value={iconName} onValueChange={setIconName} />
|
||||||
value={iconName}
|
|
||||||
onValueChange={setIconName}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -470,9 +442,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{categories.length === 0 && (
|
{categories.length === 0 && <p className="text-xs text-destructive mt-2">At least one category required</p>}
|
||||||
<p className="text-xs text-destructive mt-2">At least one category required</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Aliases */}
|
{/* Aliases */}
|
||||||
@@ -498,17 +468,9 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
{aliases.length > 0 && (
|
{aliases.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{aliases.map((alias) => (
|
{aliases.map((alias) => (
|
||||||
<Badge
|
<Badge key={alias} variant="secondary" className="inline-flex items-center px-2.5 py-1 text-xs">
|
||||||
key={alias}
|
|
||||||
variant="secondary"
|
|
||||||
className="inline-flex items-center px-2.5 py-1 text-xs"
|
|
||||||
>
|
|
||||||
{alias}
|
{alias}
|
||||||
<button
|
<button type="button" onClick={() => handleRemoveAlias(alias)} className="ml-1 hover:text-destructive">
|
||||||
type="button"
|
|
||||||
onClick={() => handleRemoveAlias(alias)}
|
|
||||||
className="ml-1 hover:text-destructive"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -519,9 +481,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
|
|
||||||
{/* About */}
|
{/* About */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">About this icon</h3>
|
||||||
About this icon
|
|
||||||
</h3>
|
|
||||||
<div className="text-xs text-muted-foreground space-y-2">
|
<div className="text-xs text-muted-foreground space-y-2">
|
||||||
<p>
|
<p>
|
||||||
{variants.length > 0
|
{variants.length > 0
|
||||||
@@ -560,9 +520,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
<FileType className="w-4 h-4 text-blue-500" />
|
<FileType className="w-4 h-4 text-blue-500" />
|
||||||
Icon Variants
|
Icon Variants
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
<p className="text-sm text-muted-foreground mb-4">Upload your icon files. Base icon is required.</p>
|
||||||
Upload your icon files. Base icon is required.
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
{variants.map((variant, index) => (
|
{variants.map((variant, index) => (
|
||||||
<VariantCard
|
<VariantCard
|
||||||
@@ -572,10 +530,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
canRemove={variant.type !== "base" || variants.length > 1}
|
canRemove={variant.type !== "base" || variants.length > 1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<AddVariantCard
|
<AddVariantCard onAddVariant={handleAddVariant} existingTypes={variants.map((v) => v.type)} />
|
||||||
onAddVariant={handleAddVariant}
|
|
||||||
existingTypes={variants.map((v) => v.type)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -615,24 +570,17 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FileType className="w-4 h-4 text-blue-500" />
|
<FileType className="w-4 h-4 text-blue-500" />
|
||||||
<div className="px-3 py-1.5 border border-border rounded-lg text-sm font-medium">
|
<div className="px-3 py-1.5 border border-border rounded-lg text-sm font-medium">
|
||||||
{baseVariant
|
{baseVariant ? baseVariant.file.name.split(".").pop()?.toUpperCase() : "N/A"}
|
||||||
? baseVariant.file.name.split(".").pop()?.toUpperCase()
|
|
||||||
: "N/A"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{availableFormats.length > 0 && (
|
{availableFormats.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Available Formats</h3>
|
||||||
Available Formats
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{availableFormats.map((format) => (
|
{availableFormats.map((format) => (
|
||||||
<div
|
<div key={format} className="px-3 py-1.5 border border-border rounded-lg text-xs font-medium">
|
||||||
key={format}
|
|
||||||
className="px-3 py-1.5 border border-border rounded-lg text-xs font-medium"
|
|
||||||
>
|
|
||||||
{format.toUpperCase()}
|
{format.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -642,21 +590,15 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
|
|
||||||
{variants.some((v) => v.type === "light" || v.type === "dark") && (
|
{variants.some((v) => v.type === "light" || v.type === "dark") && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Color Variants</h3>
|
||||||
Color Variants
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{variants
|
{variants
|
||||||
.filter((v) => v.type === "light" || v.type === "dark")
|
.filter((v) => v.type === "light" || v.type === "dark")
|
||||||
.map((variant, index) => (
|
.map((variant, index) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={index} className="flex items-center gap-2">
|
||||||
<PaletteIcon className="w-4 h-4 text-purple-500" />
|
<PaletteIcon className="w-4 h-4 text-purple-500" />
|
||||||
<span className="capitalize font-medium text-sm">
|
<span className="capitalize font-medium text-sm">{variant.type}:</span>
|
||||||
{variant.type}:
|
<code className="border border-border px-2 py-0.5 rounded-lg text-xs">{variant.file.name}</code>
|
||||||
</span>
|
|
||||||
<code className="border border-border px-2 py-0.5 rounded-lg text-xs">
|
|
||||||
{variant.file.name}
|
|
||||||
</code>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -665,21 +607,15 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
|
|
||||||
{variants.some((v) => v.type.startsWith("wordmark")) && (
|
{variants.some((v) => v.type.startsWith("wordmark")) && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Wordmark Variants</h3>
|
||||||
Wordmark Variants
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{variants
|
{variants
|
||||||
.filter((v) => v.type.startsWith("wordmark"))
|
.filter((v) => v.type.startsWith("wordmark"))
|
||||||
.map((variant, index) => (
|
.map((variant, index) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={index} className="flex items-center gap-2">
|
||||||
<Type className="w-4 h-4 text-green-500" />
|
<Type className="w-4 h-4 text-green-500" />
|
||||||
<span className="capitalize font-medium text-sm">
|
<span className="capitalize font-medium text-sm">{variant.type.replace("wordmark-", "")}:</span>
|
||||||
{variant.type.replace("wordmark-", "")}:
|
<code className="border border-border px-2 py-0.5 rounded-lg text-xs">{variant.file.name}</code>
|
||||||
</span>
|
|
||||||
<code className="border border-border px-2 py-0.5 rounded-lg text-xs">
|
|
||||||
{variant.file.name}
|
|
||||||
</code>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -714,6 +650,3 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
|
|||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -91,18 +91,20 @@ export function IconActions({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* View on GitHub Button */}
|
{/* View on GitHub Button */}
|
||||||
<Tooltip>
|
{githubUrl && (
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg" asChild>
|
<TooltipTrigger asChild>
|
||||||
<Link href={githubUrl} target="_blank" rel="noopener noreferrer" aria-label={`View ${iconName} ${format} file on GitHub`}>
|
<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg" asChild>
|
||||||
<Github className="w-4 h-4" />
|
<Link href={githubUrl} target="_blank" rel="noopener noreferrer" aria-label={`View ${iconName} ${format} file on GitHub`}>
|
||||||
</Link>
|
<Github className="w-4 h-4" />
|
||||||
</Button>
|
</Link>
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipContent>
|
</TooltipTrigger>
|
||||||
<p>View on GitHub</p>
|
<TooltipContent>
|
||||||
</TooltipContent>
|
<p>View on GitHub</p>
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -121,9 +121,12 @@ export type IconDetailsProps = {
|
|||||||
iconData: Icon
|
iconData: Icon
|
||||||
authorData: AuthorData
|
authorData: AuthorData
|
||||||
allIcons: IconFile
|
allIcons: IconFile
|
||||||
|
status?: string
|
||||||
|
statusDisplayName?: string
|
||||||
|
statusColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetailsProps) {
|
export function IconDetails({ icon, iconData, authorData, allIcons, status, statusDisplayName, statusColor }: 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 _iconWordmarkVariants = iconData.wordmark
|
||||||
@@ -133,7 +136,24 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
year: "numeric",
|
year: "numeric",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isCommunityIcon = !!(iconData as any).mainIconUrl || (typeof iconData.base === "string" && iconData.base.startsWith("http"))
|
||||||
|
const mainIconUrl = (iconData as any).mainIconUrl || (isCommunityIcon ? iconData.base : null)
|
||||||
|
const assetUrls = (iconData as any).assetUrls || []
|
||||||
|
|
||||||
const getAvailableFormats = () => {
|
const getAvailableFormats = () => {
|
||||||
|
if (isCommunityIcon) {
|
||||||
|
if (assetUrls.length > 0) {
|
||||||
|
return assetUrls.map((url: string) => {
|
||||||
|
const ext = url.split(".").pop()?.toLowerCase() || "svg"
|
||||||
|
return ext === "svg" ? "svg" : ext === "png" ? "png" : "webp"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (mainIconUrl) {
|
||||||
|
const ext = mainIconUrl.split(".").pop()?.toLowerCase() || "svg"
|
||||||
|
return [ext === "svg" ? "svg" : ext === "png" ? "png" : "webp"]
|
||||||
|
}
|
||||||
|
return ["svg"]
|
||||||
|
}
|
||||||
switch (iconData.base) {
|
switch (iconData.base) {
|
||||||
case "svg":
|
case "svg":
|
||||||
return ["svg", "png", "webp"]
|
return ["svg", "png", "webp"]
|
||||||
@@ -299,8 +319,19 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderVariant = (format: string, iconName: string, theme?: "light" | "dark") => {
|
const renderVariant = (format: string, iconName: string, theme?: "light" | "dark") => {
|
||||||
const imageUrl = `${BASE_URL}/${format}/${iconName}.${format}`
|
let imageUrl: string
|
||||||
const githubUrl = `${REPO_PATH}/tree/main/${format}/${iconName}.${format}`
|
let githubUrl: string
|
||||||
|
|
||||||
|
if (isCommunityIcon && mainIconUrl) {
|
||||||
|
const formatExt = format === "svg" ? "svg" : format === "png" ? "png" : "webp"
|
||||||
|
const matchingUrl = assetUrls.find((url: string) => url.toLowerCase().endsWith(`.${formatExt}`))
|
||||||
|
imageUrl = matchingUrl || mainIconUrl
|
||||||
|
githubUrl = ""
|
||||||
|
} else {
|
||||||
|
imageUrl = `${BASE_URL}/${format}/${iconName}.${format}`
|
||||||
|
githubUrl = `${REPO_PATH}/tree/main/${format}/${iconName}.${format}`
|
||||||
|
}
|
||||||
|
|
||||||
const variantKey = `${format}-${theme || "default"}`
|
const variantKey = `${format}-${theme || "default"}`
|
||||||
const isCopied = copiedVariants[variantKey] || false
|
const isCopied = copiedVariants[variantKey] || false
|
||||||
|
|
||||||
@@ -387,7 +418,11 @@ 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}/${iconData.colors?.light || icon}.${iconData.base}`}
|
src={
|
||||||
|
isCommunityIcon && mainIconUrl
|
||||||
|
? mainIconUrl
|
||||||
|
: `${BASE_URL}/${iconData.base}/${iconData.colors?.light || icon}.${iconData.base}`
|
||||||
|
}
|
||||||
priority
|
priority
|
||||||
width={96}
|
width={96}
|
||||||
height={96}
|
height={96}
|
||||||
@@ -478,7 +513,7 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
<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: string) => f.toUpperCase()).join(", ")}) `
|
||||||
: `${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."}
|
||||||
@@ -575,6 +610,14 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{status && statusDisplayName && statusColor && (
|
||||||
|
<div className="">
|
||||||
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Status</h3>
|
||||||
|
<Badge variant="outline" className={statusColor}>
|
||||||
|
{statusDisplayName}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="">
|
<div className="">
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Base format</h3>
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Base format</h3>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -586,7 +629,7 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
<div className="">
|
<div className="">
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Available formats</h3>
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Available formats</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{availableFormats.map((format) => (
|
{availableFormats.map((format: string) => (
|
||||||
<div key={format} className="px-3 py-1.5 border border-border rounded-lg text-xs font-medium">
|
<div key={format} className="px-3 py-1.5 border border-border rounded-lg text-xs font-medium">
|
||||||
{format.toUpperCase()}
|
{format.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
@@ -631,15 +674,17 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="">
|
{!isCommunityIcon && (
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Source</h3>
|
<div className="">
|
||||||
<Button variant="outline" className="w-full" asChild>
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Source</h3>
|
||||||
<Link href={`${REPO_PATH}/blob/main/meta/${icon}.json`} target="_blank" rel="noopener noreferrer">
|
<Button variant="outline" className="w-full" asChild>
|
||||||
<Github className="w-4 h-4 mr-2" />
|
<Link href={`${REPO_PATH}/blob/main/meta/${icon}.json`} target="_blank" rel="noopener noreferrer">
|
||||||
View on GitHub
|
<Github className="w-4 h-4 mr-2" />
|
||||||
</Link>
|
View on GitHub
|
||||||
</Button>
|
</Link>
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<Carbon />
|
<Carbon />
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { AlertCircle } from "lucide-react"
|
|||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "@/components/ui/command"
|
import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "@/components/ui/command"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { useExistingIconNames } from "@/hooks/use-submissions"
|
import { useExistingIconNames } from "@/hooks/use-submissions"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
interface IconNameComboboxProps {
|
interface IconNameComboboxProps {
|
||||||
value: string
|
value: string
|
||||||
@@ -38,12 +38,9 @@ export function IconNameCombobox({ value, onValueChange, error, isInvalid }: Ico
|
|||||||
const filteredIcons = useMemo(() => {
|
const filteredIcons = useMemo(() => {
|
||||||
const searchTerm = rawInput || value
|
const searchTerm = rawInput || value
|
||||||
if (!searchTerm || !existingIcons.length) return []
|
if (!searchTerm || !existingIcons.length) return []
|
||||||
|
|
||||||
const lowerSearch = searchTerm.toLowerCase()
|
const lowerSearch = searchTerm.toLowerCase()
|
||||||
return existingIcons.filter((icon) =>
|
return existingIcons.filter((icon) => icon.value.toLowerCase().includes(lowerSearch) || icon.label.toLowerCase().includes(lowerSearch))
|
||||||
icon.value.toLowerCase().includes(lowerSearch) ||
|
|
||||||
icon.label.toLowerCase().includes(lowerSearch)
|
|
||||||
)
|
|
||||||
}, [rawInput, value, existingIcons])
|
}, [rawInput, value, existingIcons])
|
||||||
|
|
||||||
const showSuggestions = isFocused && (rawInput || value) && filteredIcons.length > 0
|
const showSuggestions = isFocused && (rawInput || value) && filteredIcons.length > 0
|
||||||
@@ -69,10 +66,7 @@ export function IconNameCombobox({ value, onValueChange, error, isInvalid }: Ico
|
|||||||
setTimeout(() => setIsFocused(false), 200)
|
setTimeout(() => setIsFocused(false), 200)
|
||||||
}}
|
}}
|
||||||
placeholder="Type new icon ID (e.g., my-app)..."
|
placeholder="Type new icon ID (e.g., my-app)..."
|
||||||
className={cn(
|
className={cn("font-mono", isInvalid && "border-destructive focus-visible:ring-destructive/50")}
|
||||||
"font-mono",
|
|
||||||
isInvalid && "border-destructive focus-visible:ring-destructive/50"
|
|
||||||
)}
|
|
||||||
aria-invalid={isInvalid}
|
aria-invalid={isInvalid}
|
||||||
aria-describedby={error ? "icon-name-error" : undefined}
|
aria-describedby={error ? "icon-name-error" : undefined}
|
||||||
/>
|
/>
|
||||||
@@ -115,9 +109,7 @@ export function IconNameCombobox({ value, onValueChange, error, isInvalid }: Ico
|
|||||||
|
|
||||||
{/* Helper text when no error */}
|
{/* Helper text when no error */}
|
||||||
{!error && value && (
|
{!error && value && (
|
||||||
<p className="text-sm text-muted-foreground mt-1.5">
|
<p className="text-sm text-muted-foreground mt-1.5">{loading ? "Checking availability..." : "✓ Available icon ID"}</p>
|
||||||
{loading ? "Checking availability..." : "✓ Available icon ID"}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user