From 938facc889e17c207a4d09ffac90561f7cdd6bd9 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 7 Nov 2025 08:11:09 +0100 Subject: [PATCH] refactor(icons): improve icon detail components - Update icon details, actions, and editable components - Enhance icon name combobox functionality - Improve user interaction with icon metadata --- web/src/components/editable-icon-details.tsx | 121 +++++-------------- web/src/components/icon-actions.tsx | 26 ++-- web/src/components/icon-details.tsx | 75 +++++++++--- web/src/components/icon-name-combobox.tsx | 18 +-- 4 files changed, 106 insertions(+), 134 deletions(-) diff --git a/web/src/components/editable-icon-details.tsx b/web/src/components/editable-icon-details.tsx index fc3eb73c..49cce36c 100644 --- a/web/src/components/editable-icon-details.tsx +++ b/web/src/components/editable-icon-details.tsx @@ -2,19 +2,7 @@ import confetti from "canvas-confetti" import { motion } from "framer-motion" -import { - ArrowRight, - Check, - FileType, - Github, - Moon, - PaletteIcon, - Plus, - Sun, - Type, - Upload, - X, -} from "lucide-react" +import { ArrowRight, Check, FileType, Github, Moon, PaletteIcon, Plus, Sun, Type, Upload, X } from "lucide-react" import Image from "next/image" import Link from "next/link" import type React from "react" @@ -28,11 +16,12 @@ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { BASE_URL, REPO_PATH } from "@/constants" +import { pb } from "@/lib/pb" import { formatIconName } from "@/lib/utils" +import { revalidateAllSubmissions } from "@/app/actions/submissions" import { MagicCard } from "./magicui/magic-card" import { Badge } from "./ui/badge" import { Dropzone, DropzoneContent, DropzoneEmptyState } from "./ui/shadcn-io/dropzone" -import { pb } from "@/lib/pb" interface VariantFile { file: File @@ -128,13 +117,7 @@ function AddVariantCard({ onAddVariant, existingTypes }: AddVariantCardProps) { {label} ))} - @@ -172,12 +155,7 @@ function VariantCard({ variant, onRemove, canRemove }: VariantCardProps) {
- {`${variant.label} + {`${variant.label}
@@ -186,9 +164,7 @@ function VariantCard({ variant, onRemove, canRemove }: VariantCardProps) {

{variant.label}

-

- {variant.file.name.split(".").pop()?.toUpperCase()} -

+

{variant.file.name.split(".").pop()?.toUpperCase()}

@@ -268,11 +244,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai } const toggleCategory = (category: string) => { - setCategories( - categories.includes(category) - ? categories.filter((c) => c !== category) - : [...categories, category] - ) + setCategories(categories.includes(category) ? categories.filter((c) => c !== category) : [...categories, category]) } const handleAddAlias = () => { @@ -365,13 +337,16 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai const submissionData = { name: iconName, assets: assetFiles, - created_by: pb.authStore.model?.id, + created_by: pb.authStore.record?.id, status: "pending", extras: extras, } await pb.collection("submissions").create(submissionData) + // Revalidate Next.js cache for community pages + await revalidateAllSubmissions() + launchConfetti() toast.success("Icon submitted!", { description: `Your icon "${iconName}" has been submitted for review`, @@ -443,10 +418,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai - + @@ -470,9 +442,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai ))} - {categories.length === 0 && ( -

At least one category required

- )} + {categories.length === 0 &&

At least one category required

} {/* Aliases */} @@ -498,17 +468,9 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai {aliases.length > 0 && (
{aliases.map((alias) => ( - + {alias} - @@ -519,9 +481,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai {/* About */}
-

- About this icon -

+

About this icon

{variants.length > 0 @@ -560,9 +520,7 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai Icon Variants -

- Upload your icon files. Base icon is required. -

+

Upload your icon files. Base icon is required.

{variants.map((variant, index) => ( 1} /> ))} - v.type)} - /> + v.type)} />
@@ -615,24 +570,17 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai
- {baseVariant - ? baseVariant.file.name.split(".").pop()?.toUpperCase() - : "N/A"} + {baseVariant ? baseVariant.file.name.split(".").pop()?.toUpperCase() : "N/A"}
{availableFormats.length > 0 && (
-

- Available Formats -

+

Available Formats

{availableFormats.map((format) => ( -
+
{format.toUpperCase()}
))} @@ -642,21 +590,15 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai {variants.some((v) => v.type === "light" || v.type === "dark") && (
-

- Color Variants -

+

Color Variants

{variants .filter((v) => v.type === "light" || v.type === "dark") .map((variant, index) => (
- - {variant.type}: - - - {variant.file.name} - + {variant.type}: + {variant.file.name}
))}
@@ -665,21 +607,15 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai {variants.some((v) => v.type.startsWith("wordmark")) && (
-

- Wordmark Variants -

+

Wordmark Variants

{variants .filter((v) => v.type.startsWith("wordmark")) .map((variant, index) => (
- - {variant.type.replace("wordmark-", "")}: - - - {variant.file.name} - + {variant.type.replace("wordmark-", "")}: + {variant.file.name}
))}
@@ -714,6 +650,3 @@ export function EditableIconDetails({ onSubmit, initialData }: EditableIconDetai ) } - - - diff --git a/web/src/components/icon-actions.tsx b/web/src/components/icon-actions.tsx index a44cff7b..b466eee1 100644 --- a/web/src/components/icon-actions.tsx +++ b/web/src/components/icon-actions.tsx @@ -91,18 +91,20 @@ export function IconActions({ {/* View on GitHub Button */} - - - - - -

View on GitHub

-
-
+ {githubUrl && ( + + + + + +

View on GitHub

+
+
+ )}
) diff --git a/web/src/components/icon-details.tsx b/web/src/components/icon-details.tsx index dc4b69d9..094bbc29 100644 --- a/web/src/components/icon-details.tsx +++ b/web/src/components/icon-details.tsx @@ -121,9 +121,12 @@ export type IconDetailsProps = { iconData: Icon authorData: AuthorData 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 _iconColorVariants = iconData.colors const _iconWordmarkVariants = iconData.wordmark @@ -133,7 +136,24 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail 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 = () => { + 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) { case "svg": 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 imageUrl = `${BASE_URL}/${format}/${iconName}.${format}` - const githubUrl = `${REPO_PATH}/tree/main/${format}/${iconName}.${format}` + let imageUrl: string + 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 isCopied = copiedVariants[variantKey] || false @@ -387,7 +418,11 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
Available in{" "} {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 `} with a base format of {iconData.base.toUpperCase()}. {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
+ {status && statusDisplayName && statusColor && ( +
+

Status

+ + {statusDisplayName} + +
+ )}

Base format

@@ -586,7 +629,7 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail

Available formats

- {availableFormats.map((format) => ( + {availableFormats.map((format: string) => (
{format.toUpperCase()}
@@ -631,15 +674,17 @@ export function IconDetails({ icon, iconData, authorData, allIcons }: IconDetail
)} -
-

Source

- -
+ {!isCommunityIcon && ( +
+

Source

+ +
+ )}
diff --git a/web/src/components/icon-name-combobox.tsx b/web/src/components/icon-name-combobox.tsx index 7ba58c47..76362efe 100644 --- a/web/src/components/icon-name-combobox.tsx +++ b/web/src/components/icon-name-combobox.tsx @@ -4,8 +4,8 @@ import { AlertCircle } from "lucide-react" import { useEffect, useMemo, useState } from "react" import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "@/components/ui/command" import { Input } from "@/components/ui/input" -import { cn } from "@/lib/utils" import { useExistingIconNames } from "@/hooks/use-submissions" +import { cn } from "@/lib/utils" interface IconNameComboboxProps { value: string @@ -38,12 +38,9 @@ export function IconNameCombobox({ value, onValueChange, error, isInvalid }: Ico const filteredIcons = useMemo(() => { const searchTerm = rawInput || value if (!searchTerm || !existingIcons.length) return [] - + const lowerSearch = searchTerm.toLowerCase() - return existingIcons.filter((icon) => - icon.value.toLowerCase().includes(lowerSearch) || - icon.label.toLowerCase().includes(lowerSearch) - ) + return existingIcons.filter((icon) => icon.value.toLowerCase().includes(lowerSearch) || icon.label.toLowerCase().includes(lowerSearch)) }, [rawInput, value, existingIcons]) const showSuggestions = isFocused && (rawInput || value) && filteredIcons.length > 0 @@ -69,10 +66,7 @@ export function IconNameCombobox({ value, onValueChange, error, isInvalid }: Ico setTimeout(() => setIsFocused(false), 200) }} placeholder="Type new icon ID (e.g., my-app)..." - className={cn( - "font-mono", - isInvalid && "border-destructive focus-visible:ring-destructive/50" - )} + className={cn("font-mono", isInvalid && "border-destructive focus-visible:ring-destructive/50")} aria-invalid={isInvalid} aria-describedby={error ? "icon-name-error" : undefined} /> @@ -115,9 +109,7 @@ export function IconNameCombobox({ value, onValueChange, error, isInvalid }: Ico {/* Helper text when no error */} {!error && value && ( -

- {loading ? "Checking availability..." : "✓ Available icon ID"} -

+

{loading ? "Checking availability..." : "✓ Available icon ID"}

)}
)