From 8c87e66918d01a52f2beca29fcbd915213f59956 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 13 Oct 2025 15:50:08 +0200 Subject: [PATCH] refactor: migrate to TanStack Form and fix IconNameCombobox integration - Remove old advanced-icon-submission-form.tsx (replaced by TanStack version) - Fix TanStack Form implementation: - Remove generic type argument and use type assertion instead - Fix form.Subscribe selector to return object instead of array - Remove unused IconCard import - Update editable-icon-details.tsx to use new IconNameCombobox API: - Remove deprecated onIsExisting prop - Remove isExistingIcon state management - Simplify form submission messages All components now use the updated IconNameCombobox with error/isInvalid props instead of the old onIsExisting callback pattern. --- ...advanced-icon-submission-form-tanstack.tsx | 31 +- .../advanced-icon-submission-form.tsx | 480 ------------ web/src/components/editable-icon-details.tsx | 719 ++++++++++++++++++ 3 files changed, 728 insertions(+), 502 deletions(-) delete mode 100644 web/src/components/advanced-icon-submission-form.tsx create mode 100644 web/src/components/editable-icon-details.tsx diff --git a/web/src/components/advanced-icon-submission-form-tanstack.tsx b/web/src/components/advanced-icon-submission-form-tanstack.tsx index 195abf71..97895c2b 100644 --- a/web/src/components/advanced-icon-submission-form-tanstack.tsx +++ b/web/src/components/advanced-icon-submission-form-tanstack.tsx @@ -4,7 +4,6 @@ import { Check, FileImage, FileType, Plus, X } from "lucide-react" import { useForm } from "@tanstack/react-form" import { toast } from "sonner" import { IconNameCombobox } from "@/components/icon-name-combobox" -import { IconCard } from "@/components/icon-card" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" @@ -100,7 +99,7 @@ export function AdvancedIconSubmissionFormTanStack() { const [filePreviews, setFilePreviews] = useState>({}) const { data: existingIcons = [] } = useExistingIconNames() - const form = useForm({ + const form = useForm({ defaultValues: { iconName: "", selectedVariants: ["base"], // Base is always selected by default @@ -110,22 +109,7 @@ export function AdvancedIconSubmissionFormTanStack() { aliasInput: "", categories: [], description: "", - }, - validators: { - onChange: ({ value }) => { - const errors: Partial> = {} - - if (!value.files.base || value.files.base.length === 0) { - errors.files = "At least the base icon is required" - } - - if (value.categories.length === 0) { - errors.categories = "At least one category is required" - } - - return Object.keys(errors).length > 0 ? errors : undefined - }, - }, + } as FormData, onSubmit: async ({ value }) => { if (!pb.authStore.isValid) { toast.error("You must be logged in to submit an icon") @@ -588,11 +572,14 @@ export function AdvancedIconSubmissionFormTanStack() { Clear Form [state.canSubmit, state.isSubmitting]} + selector={(state) => ({ + canSubmit: state.canSubmit, + isSubmitting: state.isSubmitting, + })} > - {([canSubmit, isSubmitting]: [boolean, boolean]) => ( - )} diff --git a/web/src/components/advanced-icon-submission-form.tsx b/web/src/components/advanced-icon-submission-form.tsx deleted file mode 100644 index 6b3df323..00000000 --- a/web/src/components/advanced-icon-submission-form.tsx +++ /dev/null @@ -1,480 +0,0 @@ -"use client" - -import { AlertCircle, Check, Plus, X } from "lucide-react" -import { useState } from "react" -import { toast } from "sonner" -import { IconNameCombobox } from "@/components/icon-name-combobox" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Separator } from "@/components/ui/separator" -import { Dropzone, DropzoneContent, DropzoneEmptyState } from "@/components/ui/shadcn-io/dropzone" -import { Textarea } from "@/components/ui/textarea" -import { pb } from "@/lib/pb" - -interface VariantConfig { - id: string - label: string - description: string - field: "base" | "dark" | "light" | "wordmark" | "wordmark_dark" -} - -const VARIANTS: VariantConfig[] = [ - { - id: "base", - label: "Base Icon", - description: "Main icon file (required)", - field: "base", - }, - { - id: "dark", - label: "Dark Variant", - description: "Icon optimized for dark backgrounds", - field: "dark", - }, - { - id: "light", - label: "Light Variant", - description: "Icon optimized for light backgrounds", - field: "light", - }, - { - id: "wordmark", - label: "Wordmark", - description: "Logo with text/wordmark", - field: "wordmark", - }, - { - id: "wordmark_dark", - label: "Wordmark Dark", - description: "Wordmark optimized for dark backgrounds", - field: "wordmark_dark", - }, -] - -const AVAILABLE_CATEGORIES = [ - "automation", - "cloud", - "database", - "development", - "entertainment", - "finance", - "gaming", - "home-automation", - "media", - "monitoring", - "network", - "security", - "social", - "storage", - "tools", - "utility", - "other", -] - -export function AdvancedIconSubmissionForm() { - const [iconName, setIconName] = useState("") - const [isExistingIcon, setIsExistingIcon] = useState(false) - const [activeVariants, setActiveVariants] = useState(["base"]) - const [files, setFiles] = useState>({}) - const [filePreviews, setFilePreviews] = useState>({}) - const [aliases, setAliases] = useState([]) - const [aliasInput, setAliasInput] = useState("") - const [categories, setCategories] = useState([]) - const [description, setDescription] = useState("") - const [isSubmitting, setIsSubmitting] = useState(false) - - const handleAddVariant = (variantId: string) => { - if (!activeVariants.includes(variantId)) { - setActiveVariants([...activeVariants, variantId]) - } - } - - const handleRemoveVariant = (variantId: string) => { - if (variantId !== "base") { - setActiveVariants(activeVariants.filter((id) => id !== variantId)) - const newFiles = { ...files } - const newPreviews = { ...filePreviews } - delete newFiles[variantId] - delete newPreviews[variantId] - setFiles(newFiles) - setFilePreviews(newPreviews) - } - } - - const handleFileDrop = (variantId: string, droppedFiles: File[]) => { - setFiles({ - ...files, - [variantId]: droppedFiles, - }) - - // Generate preview for the first file - if (droppedFiles.length > 0) { - const reader = new FileReader() - reader.onload = (e) => { - if (typeof e.target?.result === 'string') { - setFilePreviews({ - ...filePreviews, - [variantId]: e.target.result, - }) - } - } - reader.readAsDataURL(droppedFiles[0]) - } - } - - const handleAddAlias = () => { - const trimmedAlias = aliasInput.trim() - if (trimmedAlias && !aliases.includes(trimmedAlias)) { - setAliases([...aliases, trimmedAlias]) - setAliasInput("") - } - } - - const handleRemoveAlias = (alias: string) => { - setAliases(aliases.filter((a) => a !== alias)) - } - - const toggleCategory = (category: string) => { - if (categories.includes(category)) { - setCategories(categories.filter((c) => c !== category)) - } else { - setCategories([...categories, category]) - } - } - - const handleSubmit = async () => { - if (!iconName.trim()) { - toast.error("Please enter an icon name") - return - } - - if (!files.base || files.base.length === 0) { - toast.error("Please upload at least the base icon") - return - } - - if (categories.length === 0) { - toast.error("Please select at least one category") - return - } - - if (!pb.authStore.isValid) { - toast.error("You must be logged in to submit an icon") - return - } - - setIsSubmitting(true) - - try { - const assetFiles: File[] = [] - - // Add base file - if (files.base?.[0]) { - assetFiles.push(files.base[0]) - } - - // Build extras object - const extras: any = { - aliases: aliases, - categories: categories, - base: files.base[0]?.name.split(".").pop() || "svg", - } - - // Add color variants if present - if (files.dark?.[0] || files.light?.[0]) { - extras.colors = {} - if (files.dark?.[0]) { - extras.colors.dark = files.dark[0].name - assetFiles.push(files.dark[0]) - } - if (files.light?.[0]) { - extras.colors.light = files.light[0].name - assetFiles.push(files.light[0]) - } - } - - // Add wordmark variants if present - if (files.wordmark?.[0] || files.wordmark_dark?.[0]) { - extras.wordmark = {} - if (files.wordmark?.[0]) { - extras.wordmark.light = files.wordmark[0].name - assetFiles.push(files.wordmark[0]) - } - if (files.wordmark_dark?.[0]) { - extras.wordmark.dark = files.wordmark_dark[0].name - assetFiles.push(files.wordmark_dark[0]) - } - } - - // Create submission - const submissionData = { - name: iconName, - assets: assetFiles, - created_by: pb.authStore.model?.id, - status: "pending", - extras: extras, - } - - await pb.collection("submissions").create(submissionData) - - toast.success(isExistingIcon ? "Icon update submitted!" : "Icon submitted!", { - description: isExistingIcon - ? `Your update for "${iconName}" has been submitted for review` - : `Your icon "${iconName}" has been submitted for review`, - }) - - // Reset form - setIconName("") - setFiles({}) - setFilePreviews({}) - setActiveVariants(["base"]) - setAliases([]) - setCategories([]) - setDescription("") - } catch (error: any) { - console.error("Submission error:", error) - toast.error("Failed to submit icon", { - description: error?.message || "Please try again later", - }) - } finally { - setIsSubmitting(false) - } - } - - return ( -
- {/* Icon Name Section */} - - - Icon Identification - Choose a unique identifier for your icon - - -
- - -

Use lowercase letters, numbers, and hyphens only

-
- - {isExistingIcon && ( - - - - This icon ID already exists. Your submission will be treated as an update to the - existing icon. - - - )} - - {iconName && !isExistingIcon && ( - - - This is a new icon submission. - - - )} -
-
- - {/* Icon Variants Section */} - - -
-
- Icon Variants - Upload different versions of your icon -
-
-
- - {activeVariants.map((variantId) => { - const variant = VARIANTS.find((v) => v.id === variantId) - if (!variant) return null - - return ( -
-
-
-
- - {variant.id === "base" && Required} -
-

{variant.description}

-
- {variant.id !== "base" && ( - - )} -
- - handleFileDrop(variantId, droppedFiles)} - onError={(error) => toast.error(error.message)} - src={files[variantId]} - > - - - {filePreviews[variantId] && ( -
- Preview -
- )} -
-
-
- ) - })} - - - -
-

Add variant:

- {VARIANTS.filter((v) => !activeVariants.includes(v.id)).map((variant) => ( - - ))} -
-
-
- - {/* Metadata Section */} - - - Icon Metadata - Provide additional information about your icon - - - {/* Categories */} -
- -
- {AVAILABLE_CATEGORIES.map((category) => ( - toggleCategory(category)} - > - {category.replace(/-/g, " ")} - - ))} -
-

Select all categories that apply to your icon

-
- - - - {/* Aliases */} -
- -
- setAliasInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault() - handleAddAlias() - } - }} - /> - -
- {aliases.length > 0 && ( -
- {aliases.map((alias) => ( - - {alias} - - - ))} -
- )} -

Alternative names that users might search for

-
- - - - {/* Description */} -
- -