diff --git a/web/src/components/advanced-icon-submission-form-tanstack.tsx b/web/src/components/advanced-icon-submission-form-tanstack.tsx index cc41c023..195abf71 100644 --- a/web/src/components/advanced-icon-submission-form-tanstack.tsx +++ b/web/src/components/advanced-icon-submission-form-tanstack.tsx @@ -1,19 +1,20 @@ "use client" -import { AlertCircle, Check, Plus, X } from "lucide-react" +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 { Alert, AlertDescription } from "@/components/ui/alert" +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" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { Separator } from "@/components/ui/separator" +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 { @@ -56,6 +57,14 @@ const VARIANTS: VariantConfig[] = [ }, ] +// 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", @@ -78,8 +87,7 @@ const AVAILABLE_CATEGORIES = [ interface FormData { iconName: string - isExistingIcon: boolean - activeVariants: string[] + selectedVariants: string[] files: Record filePreviews: Record aliases: string[] @@ -90,12 +98,12 @@ interface FormData { export function AdvancedIconSubmissionFormTanStack() { const [filePreviews, setFilePreviews] = useState>({}) + const { data: existingIcons = [] } = useExistingIconNames() const form = useForm({ defaultValues: { iconName: "", - isExistingIcon: false, - activeVariants: ["base"], + selectedVariants: ["base"], // Base is always selected by default files: {}, filePreviews: {}, aliases: [], @@ -107,12 +115,6 @@ export function AdvancedIconSubmissionFormTanStack() { 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" } @@ -182,10 +184,8 @@ export function AdvancedIconSubmissionFormTanStack() { 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`, + toast.success("Icon submitted!", { + description: `Your icon "${value.iconName}" has been submitted for review`, }) // Reset form @@ -200,29 +200,33 @@ export function AdvancedIconSubmissionFormTanStack() { }, }) - 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)) + // 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", { @@ -272,7 +276,7 @@ export function AdvancedIconSubmissionFormTanStack() { } return ( -
+
{ e.preventDefault() @@ -280,290 +284,321 @@ export function AdvancedIconSubmissionFormTanStack() { form.handleSubmit() }} > - {/* Icon Name Section */} - Icon Identification - Choose a unique identifier for your icon + Submit an Icon + Fill in the details below to submit your icon for review - - { - 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 Name Section */} +
- Icon Variants - Upload different versions of your icon +

Icon Identification

+

Choose a unique identifier for 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 */} -
- - + + { + 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) => ( -
- field.handleChange(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault() - handleAddAlias() - } - }} +
+ + - +

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) => ( <> - {field.state.value.length > 0 && ( -
- {field.state.value.map((alias) => ( - - {alias} - - - ))} -
- )} +
+ + { + 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} +
+ )} +
+
+
+
+
+ ) + })} +
)}
-

Alternative names that users might search for

- + {/* Metadata Section */} +
+
+

Icon Metadata

+

Provide additional information about your icon

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