"use client" import { AlertCircle, Check, Plus, X } from "lucide-react" import { useForm } from "@tanstack/react-form" 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" 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", }, ] 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 isExistingIcon: boolean activeVariants: string[] files: Record filePreviews: Record aliases: string[] aliasInput: string categories: string[] description: string } export function AdvancedIconSubmissionFormTanStack() { const [filePreviews, setFilePreviews] = useState>({}) const form = useForm({ defaultValues: { iconName: "", isExistingIcon: false, activeVariants: ["base"], files: {}, filePreviews: {}, aliases: [], aliasInput: "", categories: [], description: "", }, validators: { onChange: ({ value }) => { const errors: Partial> = {} if (!value.iconName.trim()) { errors.iconName = "Icon name is required" } else if (!/^[a-z0-9-]+$/.test(value.iconName)) { errors.iconName = "Icon name must contain only lowercase letters, numbers, and hyphens" } 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 }, }, 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(value.isExistingIcon ? "Icon update submitted!" : "Icon submitted!", { description: value.isExistingIcon ? `Your update for "${value.iconName}" has been submitted for review` : `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 handleAddVariant = (variantId: string) => { const currentVariants = form.getFieldValue("activeVariants") if (!currentVariants.includes(variantId)) { form.setFieldValue("activeVariants", [...currentVariants, variantId]) } } const handleRemoveVariant = (variantId: string) => { if (variantId !== "base") { const currentVariants = form.getFieldValue("activeVariants") form.setFieldValue("activeVariants", currentVariants.filter((id) => id !== variantId)) const currentFiles = form.getFieldValue("files") const newFiles = { ...currentFiles } delete newFiles[variantId] form.setFieldValue("files", newFiles) const newPreviews = { ...filePreviews } delete newPreviews[variantId] setFilePreviews(newPreviews) } } 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() }} > {/* 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" } return undefined }, }} > {(field) => (
form.setFieldValue("isExistingIcon", isExisting)} />

Use lowercase letters, numbers, and hyphens only

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

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

)}
)}
{(field) => ( <> {field.state.value && ( This icon ID already exists. Your submission will be treated as an update to the existing icon. )} {form.getFieldValue("iconName") && !field.state.value && ( This is a new icon submission. )} )}
{/* Icon Variants Section */}
Icon Variants Upload different versions of your icon
{(field) => ( <> {field.state.value.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={form.getFieldValue("files")[variantId]} > {filePreviews[variantId] && (
Preview
)}
) })} )}

Add variant:

{VARIANTS.filter((v) => !form.getFieldValue("activeVariants").includes(v.id)).map((variant) => ( ))}
{/* 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) => (