From 56289820f0728f2df925e5dbd456487cc5537738 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 7 Nov 2025 08:11:14 +0100 Subject: [PATCH] refactor(submissions): improve submission forms and data table - Update icon submission forms with better validation - Enhance advanced submission form with TanStack - Improve submissions data table functionality - Update submissions hook for better data management --- ...advanced-icon-submission-form-tanstack.tsx | 63 ++-- web/src/components/icon-submission-form.tsx | 18 +- web/src/components/submissions-data-table.tsx | 279 +++++++++--------- web/src/hooks/use-submissions.ts | 13 +- 4 files changed, 191 insertions(+), 182 deletions(-) diff --git a/web/src/components/advanced-icon-submission-form-tanstack.tsx b/web/src/components/advanced-icon-submission-form-tanstack.tsx index 97895c2b..3ac760f1 100644 --- a/web/src/components/advanced-icon-submission-form-tanstack.tsx +++ b/web/src/components/advanced-icon-submission-form-tanstack.tsx @@ -1,7 +1,8 @@ "use client" -import { Check, FileImage, FileType, Plus, X } from "lucide-react" import { useForm } from "@tanstack/react-form" +import { Check, FileImage, FileType, Plus, X } from "lucide-react" +import { useState } from "react" import { toast } from "sonner" import { IconNameCombobox } from "@/components/icon-name-combobox" import { Badge } from "@/components/ui/badge" @@ -12,9 +13,9 @@ 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" +import { pb } from "@/lib/pb" +import { revalidateAllSubmissions } from "@/app/actions/submissions" interface VariantConfig { id: string @@ -168,6 +169,9 @@ export function AdvancedIconSubmissionFormTanStack() { 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`, }) @@ -188,14 +192,17 @@ export function AdvancedIconSubmissionFormTanStack() { if (variantId !== "base") { // Remove from selected variants const currentVariants = form.getFieldValue("selectedVariants") - form.setFieldValue("selectedVariants", currentVariants.filter((v) => v !== variantId)) - + 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] @@ -205,9 +212,7 @@ export function AdvancedIconSubmissionFormTanStack() { const handleVariantSelectionChange = (selectedValues: string[]) => { // Ensure base is always included - const finalValues = selectedValues.includes("base") - ? selectedValues - : ["base", ...selectedValues] + const finalValues = selectedValues.includes("base") ? selectedValues : ["base", ...selectedValues] return finalValues } @@ -217,12 +222,12 @@ export function AdvancedIconSubmissionFormTanStack() { ...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') { + if (typeof e.target?.result === "string") { setFilePreviews({ ...filePreviews, [variantId]: e.target.result, @@ -247,13 +252,19 @@ export function AdvancedIconSubmissionFormTanStack() { const handleRemoveAlias = (alias: string) => { const currentAliases = form.getFieldValue("aliases") - form.setFieldValue("aliases", currentAliases.filter((a) => a !== alias)) + 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)) + form.setFieldValue( + "categories", + currentCategories.filter((c) => c !== category), + ) } else { form.setFieldValue("categories", [...currentCategories, category]) } @@ -280,7 +291,7 @@ export function AdvancedIconSubmissionFormTanStack() {

Icon Identification

Choose a unique identifier for your icon

- + (
- {`${variantId} + {`${variantId}

{state.iconName || "preview"}

@@ -386,11 +393,7 @@ export function AdvancedIconSubmissionFormTanStack() { return ( {/* Remove button at top-right corner */} {!isBase && ( @@ -410,7 +413,11 @@ export function AdvancedIconSubmissionFormTanStack() {

{variant.label}

- {isBase && Required} + {isBase && ( + + Required + + )} {hasFile && ( @@ -419,7 +426,7 @@ export function AdvancedIconSubmissionFormTanStack() { )}

{variant.description}

- + )} - + {(field) => ( <> diff --git a/web/src/components/icon-submission-form.tsx b/web/src/components/icon-submission-form.tsx index be5ef2d0..a8ab2cc3 100644 --- a/web/src/components/icon-submission-form.tsx +++ b/web/src/components/icon-submission-form.tsx @@ -51,7 +51,7 @@ export function IconSubmissionContent({ onClose }: { onClose?: () => void }) { if (files.length > 0) { const reader = new FileReader() reader.onload = (e) => { - if (typeof e.target?.result === 'string') { + if (typeof e.target?.result === "string") { setFilePreview(e.target?.result) } } @@ -65,7 +65,7 @@ export function IconSubmissionContent({ onClose }: { onClose?: () => void }) {

Upload Icon Files

void }) { {filePreview && (
- Preview + Preview
)}
- {files && files.length > 0 && ( -
- {files.length} file(s) selected -
- )} + {files && files.length > 0 &&
{files.length} file(s) selected
}
{/* Divider */} @@ -121,7 +113,7 @@ export function IconSubmissionContent({ onClose }: { onClose?: () => void }) {
) } -export function IconSubmissionForm({ trigger, onClick }: { trigger?: React.ReactNode, onClick?: () => void }) { +export function IconSubmissionForm({ trigger, onClick }: { trigger?: React.ReactNode; onClick?: () => void }) { const [open, setOpen] = useState(false) return ( diff --git a/web/src/components/submissions-data-table.tsx b/web/src/components/submissions-data-table.tsx index aa61ea35..9248b188 100644 --- a/web/src/components/submissions-data-table.tsx +++ b/web/src/components/submissions-data-table.tsx @@ -140,149 +140,152 @@ export function SubmissionsDataTable({ [userFilter], ) - const columns: ColumnDef[] = React.useMemo(() => [ - { - id: "expander", - header: () => null, - cell: ({ row }) => { - return ( - - ) - }, - }, - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ) - }, - cell: ({ row }) =>
{row.getValue("name")}
, - }, - { - accessorKey: "status", - header: ({ column }) => { - return ( - - ) - }, - cell: ({ row }) => { - const status = row.getValue("status") as Submission["status"] - return ( - - {getStatusDisplayName(status)} - - ) - }, - }, - { - accessorKey: "created_by", - header: ({ column }) => { - return ( - - ) - }, - cell: ({ row }) => { - const submission = row.original - const expandedData = (submission as any).expand - const displayName = getDisplayName(submission, expandedData) - const userId = submission.created_by - - return ( -
- - {userFilter?.userId === userId && } -
- ) - }, - }, - { - accessorKey: "updated", - header: ({ column }) => { - return ( - - ) - }, - cell: ({ row }) => { - const date = row.getValue("updated") as string - return ( -
- {dayjs(date).fromNow()} -
- ) - }, - }, - { - accessorKey: "assets", - header: "Preview", - cell: ({ row }) => { - const assets = row.getValue("assets") as string[] - const name = row.getValue("name") as string - if (assets.length > 0) { + const columns: ColumnDef[] = React.useMemo( + () => [ + { + id: "expander", + header: () => null, + cell: ({ row }) => { return ( -
- {name} { + e.stopPropagation() + handleRowToggle(row.id, row.getIsExpanded()) + }} + className="flex items-center justify-center w-8 h-8 hover:bg-muted rounded transition-colors" + > + {row.getIsExpanded() ? ( + + ) : ( + + )} + + ) + }, + }, + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) =>
{row.getValue("name")}
, + }, + { + accessorKey: "status", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => { + const status = row.getValue("status") as Submission["status"] + return ( + + {getStatusDisplayName(status)} + + ) + }, + }, + { + accessorKey: "created_by", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => { + const submission = row.original + const expandedData = (submission as any).expand + const displayName = getDisplayName(submission, expandedData) + const userId = submission.created_by + + return ( +
+ + {userFilter?.userId === userId && }
) - } - return ( -
- -
- ) + }, }, - }, - ], [handleRowToggle, handleUserFilter, userFilter]) + { + accessorKey: "updated", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => { + const date = row.getValue("updated") as string + return ( +
+ {dayjs(date).fromNow()} +
+ ) + }, + }, + { + accessorKey: "assets", + header: "Preview", + cell: ({ row }) => { + const assets = row.getValue("assets") as string[] + const name = row.getValue("name") as string + if (assets.length > 0) { + return ( +
+ {name} +
+ ) + } + return ( +
+ +
+ ) + }, + }, + ], + [handleRowToggle, handleUserFilter, userFilter], + ) const table = useReactTable({ data: groupedData, diff --git a/web/src/hooks/use-submissions.ts b/web/src/hooks/use-submissions.ts index 9a295f54..53d656cf 100644 --- a/web/src/hooks/use-submissions.ts +++ b/web/src/hooks/use-submissions.ts @@ -1,7 +1,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" -import { pb, type Submission } from "@/lib/pb" import { getAllIcons } from "@/lib/api" +import { pb, type Submission } from "@/lib/pb" +import { revalidateAllSubmissions } from "@/app/actions/submissions" // Query key factory export const submissionKeys = { @@ -46,10 +47,13 @@ export function useApproveSubmission() { }, ) }, - onSuccess: (data) => { + onSuccess: async (data) => { // Invalidate and refetch submissions queryClient.invalidateQueries({ queryKey: submissionKeys.lists() }) + // Revalidate Next.js cache for community pages + await revalidateAllSubmissions() + toast.success("Submission approved", { description: "The submission has been approved successfully", }) @@ -82,10 +86,13 @@ export function useRejectSubmission() { }, ) }, - onSuccess: () => { + onSuccess: async () => { // Invalidate and refetch submissions queryClient.invalidateQueries({ queryKey: submissionKeys.lists() }) + // Revalidate Next.js cache for community pages + await revalidateAllSubmissions() + toast.success("Submission rejected", { description: "The submission has been rejected", })