"use client" import { useForm } from "@tanstack/react-form" import { Check, FileImage, FileType, Plus, X } from "lucide-react" import { useState } from "react" import { toast } from "sonner" import { revalidateAllSubmissions } from "@/app/actions/submissions" import { ExperimentalWarning } from "@/components/experimental-warning" import { IconNameCombobox } from "@/components/icon-name-combobox" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" 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 { useExistingIconNames } from "@/hooks/use-submissions" 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", }, ] // 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 [showConfirmDialog, setShowConfirmDialog] = useState(false) 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 } setShowConfirmDialog(true) }, }) const handleConfirmedSubmit = async () => { const value = form.state.values setShowConfirmDialog(false) 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) // Revalidate Next.js cache for community pages await revalidateAllSubmissions() 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 (
Confirm Submission This icon submission form is a work-in-progress and is currently in an experimentation phase. Your submission will not be reviewed or processed at this time. We're using this to gather feedback and improve the experience.

Do you still want to proceed with submitting your icon?
Cancel Yes, Submit Anyway
{ 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) => (