"use client" 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 { 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 { MultiSelect, type MultiSelectOption } from "@/components/ui/multi-select" import { Dropzone, DropzoneContent, DropzoneEmptyState } from "@/components/ui/shadcn-io/dropzone" import { Textarea } from "@/components/ui/textarea" import { pb } from "@/lib/pb" import { useExistingIconNames } from "@/hooks/use-submissions" import { useState } from "react" 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", }, ] // Convert VARIANTS to MultiSelect options const VARIANT_OPTIONS: MultiSelectOption[] = VARIANTS.map((variant) => ({ label: variant.label, value: variant.id, icon: variant.id === "base" ? FileImage : FileType, disabled: variant.id === "base", // Base is always required })) const AVAILABLE_CATEGORIES = [ "automation", "cloud", "database", "development", "entertainment", "finance", "gaming", "home-automation", "media", "monitoring", "network", "security", "social", "storage", "tools", "utility", "other", ] interface FormData { iconName: string selectedVariants: string[] files: Record filePreviews: Record aliases: string[] aliasInput: string categories: string[] description: string } export function AdvancedIconSubmissionFormTanStack() { const [filePreviews, setFilePreviews] = useState>({}) const { data: existingIcons = [] } = useExistingIconNames() const form = useForm({ defaultValues: { iconName: "", selectedVariants: ["base"], // Base is always selected by default files: {}, filePreviews: {}, aliases: [], aliasInput: "", categories: [], description: "", } as FormData, onSubmit: async ({ value }) => { if (!pb.authStore.isValid) { toast.error("You must be logged in to submit an icon") return } try { const assetFiles: File[] = [] // Add base file if (value.files.base?.[0]) { assetFiles.push(value.files.base[0]) } // Build extras object const extras: any = { aliases: value.aliases, categories: value.categories, base: value.files.base[0]?.name.split(".").pop() || "svg", } // Add color variants if present if (value.files.dark?.[0] || value.files.light?.[0]) { extras.colors = {} if (value.files.dark?.[0]) { extras.colors.dark = value.files.dark[0].name assetFiles.push(value.files.dark[0]) } if (value.files.light?.[0]) { extras.colors.light = value.files.light[0].name assetFiles.push(value.files.light[0]) } } // Add wordmark variants if present if (value.files.wordmark?.[0] || value.files.wordmark_dark?.[0]) { extras.wordmark = {} if (value.files.wordmark?.[0]) { extras.wordmark.light = value.files.wordmark[0].name assetFiles.push(value.files.wordmark[0]) } if (value.files.wordmark_dark?.[0]) { extras.wordmark.dark = value.files.wordmark_dark[0].name assetFiles.push(value.files.wordmark_dark[0]) } } // Create submission const submissionData = { name: value.iconName, assets: assetFiles, created_by: pb.authStore.model?.id, status: "pending", extras: extras, } await pb.collection("submissions").create(submissionData) toast.success("Icon submitted!", { description: `Your icon "${value.iconName}" has been submitted for review`, }) // Reset form form.reset() setFilePreviews({}) } catch (error: any) { console.error("Submission error:", error) toast.error("Failed to submit icon", { description: error?.message || "Please try again later", }) } }, }) const handleRemoveVariant = (variantId: string) => { if (variantId !== "base") { // Remove from selected variants const currentVariants = form.getFieldValue("selectedVariants") form.setFieldValue("selectedVariants", currentVariants.filter((v) => v !== variantId)) // Remove files const currentFiles = form.getFieldValue("files") const newFiles = { ...currentFiles } delete newFiles[variantId] form.setFieldValue("files", newFiles) // Remove previews const newPreviews = { ...filePreviews } delete newPreviews[variantId] setFilePreviews(newPreviews) } } const handleVariantSelectionChange = (selectedValues: string[]) => { // Ensure base is always included const finalValues = selectedValues.includes("base") ? selectedValues : ["base", ...selectedValues] return finalValues } const handleFileDrop = (variantId: string, droppedFiles: File[]) => { const currentFiles = form.getFieldValue("files") form.setFieldValue("files", { ...currentFiles, [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 aliasInput = form.getFieldValue("aliasInput") const trimmedAlias = aliasInput.trim() if (trimmedAlias) { const currentAliases = form.getFieldValue("aliases") if (!currentAliases.includes(trimmedAlias)) { form.setFieldValue("aliases", [...currentAliases, trimmedAlias]) } form.setFieldValue("aliasInput", "") } } const handleRemoveAlias = (alias: string) => { const currentAliases = form.getFieldValue("aliases") form.setFieldValue("aliases", currentAliases.filter((a) => a !== alias)) } const toggleCategory = (category: string) => { const currentCategories = form.getFieldValue("categories") if (currentCategories.includes(category)) { form.setFieldValue("categories", currentCategories.filter((c) => c !== category)) } else { form.setFieldValue("categories", [...currentCategories, category]) } } return (
{ e.preventDefault() e.stopPropagation() form.handleSubmit() }} > Submit an Icon Fill in the details below to submit your icon for review {/* Icon Name Section */}

Icon Identification

Choose a unique identifier for your icon

{ if (!value) return "Icon name is required" if (!/^[a-z0-9-]+$/.test(value)) { return "Icon name must contain only lowercase letters, numbers, and hyphens" } // Check if icon already exists const iconExists = existingIcons.some((icon) => icon.value === value) if (iconExists) { return "This icon already exists. Icon updates are not yet supported. Please choose a different name." } return undefined }, }} > {(field) => (

Use lowercase letters, numbers, and hyphens only

)}
{/* Icon Preview Section */} {Object.keys(filePreviews).length > 0 && ( ({ iconName: state.values.iconName, categories: state.values.categories })}> {(state) => (

Icon Preview

How your icon will appear

{Object.entries(filePreviews).map(([variantId, preview]) => (
{`${variantId}

{state.iconName || "preview"}

{variantId}

))}
)}
)} {/* Icon Variants Section */}

Icon Variants

Select which variants you want to upload

{(field) => ( <>
{ const finalValues = handleVariantSelectionChange(values) field.handleChange(finalValues) }} placeholder="Select icon variants..." maxCount={5} searchable={false} hideSelectAll={true} resetOnDefaultValueChange={true} />

Base icon is required and cannot be removed. Select additional variants as needed.

{/* Upload zones for selected variants - using field.state.value for reactivity */}
{field.state.value.map((variantId) => { const variant = VARIANTS.find((v) => v.id === variantId) if (!variant) return null const hasFile = form.getFieldValue("files")[variant.id]?.length > 0 const isBase = variant.id === "base" return ( {/* Remove button at top-right corner */} {!isBase && ( )}

{variant.label}

{isBase && Required} {hasFile && ( Uploaded )}

{variant.description}

handleFileDrop(variant.id, droppedFiles)} onError={(error) => toast.error(error.message)} src={form.getFieldValue("files")[variant.id]} > {filePreviews[variant.id] && (
{`${variant.label}
)}
) })}
)}
{/* Metadata Section */}

Icon Metadata

Provide additional information about your icon

{/* Categories */} {(field) => (
{AVAILABLE_CATEGORIES.map((category) => ( toggleCategory(category)} > {category.replace(/-/g, " ")} ))}

Select all categories that apply to your icon

{!field.state.meta.isValid && field.state.meta.isTouched && (

{field.state.meta.errors.join(", ")}

)}
)}
{/* Aliases */}
{(field) => (
field.handleChange(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() handleAddAlias() } }} />
)}
{(field) => ( <> {field.state.value.length > 0 && (
{field.state.value.map((alias) => ( {alias} ))}
)} )}

Alternative names that users might search for

{/* Description */} {(field) => (