mirror of
https://github.com/walkxcode/dashboard-icons.git
synced 2025-11-19 10:07:29 +01:00
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
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Check, FileImage, FileType, Plus, X } from "lucide-react"
|
|
||||||
import { useForm } from "@tanstack/react-form"
|
import { useForm } from "@tanstack/react-form"
|
||||||
|
import { Check, FileImage, FileType, Plus, X } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { IconNameCombobox } from "@/components/icon-name-combobox"
|
import { IconNameCombobox } from "@/components/icon-name-combobox"
|
||||||
import { Badge } from "@/components/ui/badge"
|
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 { MultiSelect, type MultiSelectOption } from "@/components/ui/multi-select"
|
||||||
import { Dropzone, DropzoneContent, DropzoneEmptyState } from "@/components/ui/shadcn-io/dropzone"
|
import { Dropzone, DropzoneContent, DropzoneEmptyState } from "@/components/ui/shadcn-io/dropzone"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { pb } from "@/lib/pb"
|
|
||||||
import { useExistingIconNames } from "@/hooks/use-submissions"
|
import { useExistingIconNames } from "@/hooks/use-submissions"
|
||||||
import { useState } from "react"
|
import { pb } from "@/lib/pb"
|
||||||
|
import { revalidateAllSubmissions } from "@/app/actions/submissions"
|
||||||
|
|
||||||
interface VariantConfig {
|
interface VariantConfig {
|
||||||
id: string
|
id: string
|
||||||
@@ -168,6 +169,9 @@ export function AdvancedIconSubmissionFormTanStack() {
|
|||||||
|
|
||||||
await pb.collection("submissions").create(submissionData)
|
await pb.collection("submissions").create(submissionData)
|
||||||
|
|
||||||
|
// Revalidate Next.js cache for community pages
|
||||||
|
await revalidateAllSubmissions()
|
||||||
|
|
||||||
toast.success("Icon submitted!", {
|
toast.success("Icon submitted!", {
|
||||||
description: `Your icon "${value.iconName}" has been submitted for review`,
|
description: `Your icon "${value.iconName}" has been submitted for review`,
|
||||||
})
|
})
|
||||||
@@ -188,7 +192,10 @@ export function AdvancedIconSubmissionFormTanStack() {
|
|||||||
if (variantId !== "base") {
|
if (variantId !== "base") {
|
||||||
// Remove from selected variants
|
// Remove from selected variants
|
||||||
const currentVariants = form.getFieldValue("selectedVariants")
|
const currentVariants = form.getFieldValue("selectedVariants")
|
||||||
form.setFieldValue("selectedVariants", currentVariants.filter((v) => v !== variantId))
|
form.setFieldValue(
|
||||||
|
"selectedVariants",
|
||||||
|
currentVariants.filter((v) => v !== variantId),
|
||||||
|
)
|
||||||
|
|
||||||
// Remove files
|
// Remove files
|
||||||
const currentFiles = form.getFieldValue("files")
|
const currentFiles = form.getFieldValue("files")
|
||||||
@@ -205,9 +212,7 @@ export function AdvancedIconSubmissionFormTanStack() {
|
|||||||
|
|
||||||
const handleVariantSelectionChange = (selectedValues: string[]) => {
|
const handleVariantSelectionChange = (selectedValues: string[]) => {
|
||||||
// Ensure base is always included
|
// Ensure base is always included
|
||||||
const finalValues = selectedValues.includes("base")
|
const finalValues = selectedValues.includes("base") ? selectedValues : ["base", ...selectedValues]
|
||||||
? selectedValues
|
|
||||||
: ["base", ...selectedValues]
|
|
||||||
return finalValues
|
return finalValues
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +227,7 @@ export function AdvancedIconSubmissionFormTanStack() {
|
|||||||
if (droppedFiles.length > 0) {
|
if (droppedFiles.length > 0) {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
if (typeof e.target?.result === 'string') {
|
if (typeof e.target?.result === "string") {
|
||||||
setFilePreviews({
|
setFilePreviews({
|
||||||
...filePreviews,
|
...filePreviews,
|
||||||
[variantId]: e.target.result,
|
[variantId]: e.target.result,
|
||||||
@@ -247,13 +252,19 @@ export function AdvancedIconSubmissionFormTanStack() {
|
|||||||
|
|
||||||
const handleRemoveAlias = (alias: string) => {
|
const handleRemoveAlias = (alias: string) => {
|
||||||
const currentAliases = form.getFieldValue("aliases")
|
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 toggleCategory = (category: string) => {
|
||||||
const currentCategories = form.getFieldValue("categories")
|
const currentCategories = form.getFieldValue("categories")
|
||||||
if (currentCategories.includes(category)) {
|
if (currentCategories.includes(category)) {
|
||||||
form.setFieldValue("categories", currentCategories.filter((c) => c !== category))
|
form.setFieldValue(
|
||||||
|
"categories",
|
||||||
|
currentCategories.filter((c) => c !== category),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
form.setFieldValue("categories", [...currentCategories, category])
|
form.setFieldValue("categories", [...currentCategories, category])
|
||||||
}
|
}
|
||||||
@@ -326,11 +337,7 @@ export function AdvancedIconSubmissionFormTanStack() {
|
|||||||
{Object.entries(filePreviews).map(([variantId, preview]) => (
|
{Object.entries(filePreviews).map(([variantId, preview]) => (
|
||||||
<div key={variantId} className="flex flex-col gap-2">
|
<div key={variantId} className="flex flex-col gap-2">
|
||||||
<div className="relative aspect-square rounded-lg border bg-card p-4 flex items-center justify-center">
|
<div className="relative aspect-square rounded-lg border bg-card p-4 flex items-center justify-center">
|
||||||
<img
|
<img alt={`${variantId} preview`} className="max-h-full max-w-full object-contain" src={preview} />
|
||||||
alt={`${variantId} preview`}
|
|
||||||
className="max-h-full max-w-full object-contain"
|
|
||||||
src={preview}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-xs font-mono text-muted-foreground">{state.iconName || "preview"}</p>
|
<p className="text-xs font-mono text-muted-foreground">{state.iconName || "preview"}</p>
|
||||||
@@ -386,11 +393,7 @@ export function AdvancedIconSubmissionFormTanStack() {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={variantId}
|
key={variantId}
|
||||||
className={`relative transition-all ${
|
className={`relative transition-all ${hasFile ? "border-primary bg-primary/5" : "border-border"}`}
|
||||||
hasFile
|
|
||||||
? "border-primary bg-primary/5"
|
|
||||||
: "border-border"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{/* Remove button at top-right corner */}
|
{/* Remove button at top-right corner */}
|
||||||
{!isBase && (
|
{!isBase && (
|
||||||
@@ -410,7 +413,11 @@ export function AdvancedIconSubmissionFormTanStack() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h4 className="text-sm font-semibold">{variant.label}</h4>
|
<h4 className="text-sm font-semibold">{variant.label}</h4>
|
||||||
{isBase && <Badge variant="secondary" className="text-xs">Required</Badge>}
|
{isBase && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
Required
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
{hasFile && (
|
{hasFile && (
|
||||||
<Badge variant="default" className="text-xs">
|
<Badge variant="default" className="text-xs">
|
||||||
<Check className="h-3 w-3 mr-1" />
|
<Check className="h-3 w-3 mr-1" />
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function IconSubmissionContent({ onClose }: { onClose?: () => void }) {
|
|||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
if (typeof e.target?.result === 'string') {
|
if (typeof e.target?.result === "string") {
|
||||||
setFilePreview(e.target?.result)
|
setFilePreview(e.target?.result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ export function IconSubmissionContent({ onClose }: { onClose?: () => void }) {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h3 className="text-sm font-medium">Upload Icon Files</h3>
|
<h3 className="text-sm font-medium">Upload Icon Files</h3>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
accept={{ 'image/*': ['.png', '.jpg', '.jpeg', '.svg', '.webp'] }}
|
accept={{ "image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"] }}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onError={console.error}
|
onError={console.error}
|
||||||
src={files}
|
src={files}
|
||||||
@@ -76,20 +76,12 @@ export function IconSubmissionContent({ onClose }: { onClose?: () => void }) {
|
|||||||
<DropzoneContent>
|
<DropzoneContent>
|
||||||
{filePreview && (
|
{filePreview && (
|
||||||
<div className="h-[102px] w-full">
|
<div className="h-[102px] w-full">
|
||||||
<img
|
<img alt="Preview" className="absolute top-0 left-0 h-full w-full object-cover" src={filePreview} />
|
||||||
alt="Preview"
|
|
||||||
className="absolute top-0 left-0 h-full w-full object-cover"
|
|
||||||
src={filePreview}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DropzoneContent>
|
</DropzoneContent>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
{files && files.length > 0 && (
|
{files && files.length > 0 && <div className="text-xs text-muted-foreground">{files.length} file(s) selected</div>}
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{files.length} file(s) selected
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
@@ -121,7 +113,7 @@ export function IconSubmissionContent({ onClose }: { onClose?: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
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)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -140,149 +140,152 @@ export function SubmissionsDataTable({
|
|||||||
[userFilter],
|
[userFilter],
|
||||||
)
|
)
|
||||||
|
|
||||||
const columns: ColumnDef<Submission>[] = React.useMemo(() => [
|
const columns: ColumnDef<Submission>[] = React.useMemo(
|
||||||
{
|
() => [
|
||||||
id: "expander",
|
{
|
||||||
header: () => null,
|
id: "expander",
|
||||||
cell: ({ row }) => {
|
header: () => null,
|
||||||
return (
|
cell: ({ row }) => {
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
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() ? (
|
|
||||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "name",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
className="h-auto p-0 font-semibold hover:bg-transparent"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
<SortDesc className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cell: ({ row }) => <div className="font-medium capitalize">{row.getValue("name")}</div>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "status",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
className="h-auto p-0 font-semibold hover:bg-transparent"
|
|
||||||
>
|
|
||||||
Status
|
|
||||||
<SortDesc className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.getValue("status") as Submission["status"]
|
|
||||||
return (
|
|
||||||
<Badge variant="outline" className={getStatusColor(status)}>
|
|
||||||
{getStatusDisplayName(status)}
|
|
||||||
</Badge>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "created_by",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
className="h-auto p-0 font-semibold hover:bg-transparent"
|
|
||||||
>
|
|
||||||
Submitted By
|
|
||||||
<SortDesc className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const submission = row.original
|
|
||||||
const expandedData = (submission as any).expand
|
|
||||||
const displayName = getDisplayName(submission, expandedData)
|
|
||||||
const userId = submission.created_by
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<UserDisplay
|
|
||||||
userId={userId}
|
|
||||||
avatar={expandedData.created_by.avatar}
|
|
||||||
displayName={displayName}
|
|
||||||
onClick={handleUserFilter}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
{userFilter?.userId === userId && <X className="h-3 w-3 text-muted-foreground" />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "updated",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
className="h-auto p-0 font-semibold hover:bg-transparent"
|
|
||||||
>
|
|
||||||
Updated
|
|
||||||
<SortDesc className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const date = row.getValue("updated") as string
|
|
||||||
return (
|
|
||||||
<div className="text-sm text-muted-foreground" title={dayjs(date).format("MMMM D, YYYY h:mm A")}>
|
|
||||||
{dayjs(date).fromNow()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 (
|
return (
|
||||||
<div className="w-12 h-12 rounded border flex items-center justify-center bg-background p-2">
|
<button
|
||||||
<img
|
onClick={(e) => {
|
||||||
src={`${pb.baseUrl}/api/files/submissions/${row.original.id}/${assets[0]}?thumb=100x100` || "/placeholder.svg"}
|
e.stopPropagation()
|
||||||
alt={name}
|
handleRowToggle(row.id, row.getIsExpanded())
|
||||||
className="w-full h-full object-contain"
|
}}
|
||||||
|
className="flex items-center justify-center w-8 h-8 hover:bg-muted rounded transition-colors"
|
||||||
|
>
|
||||||
|
{row.getIsExpanded() ? (
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
className="h-auto p-0 font-semibold hover:bg-transparent"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
<SortDesc className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => <div className="font-medium capitalize">{row.getValue("name")}</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
className="h-auto p-0 font-semibold hover:bg-transparent"
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
<SortDesc className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.getValue("status") as Submission["status"]
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className={getStatusColor(status)}>
|
||||||
|
{getStatusDisplayName(status)}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "created_by",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
className="h-auto p-0 font-semibold hover:bg-transparent"
|
||||||
|
>
|
||||||
|
Submitted By
|
||||||
|
<SortDesc className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const submission = row.original
|
||||||
|
const expandedData = (submission as any).expand
|
||||||
|
const displayName = getDisplayName(submission, expandedData)
|
||||||
|
const userId = submission.created_by
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<UserDisplay
|
||||||
|
userId={userId}
|
||||||
|
avatar={expandedData.created_by.avatar}
|
||||||
|
displayName={displayName}
|
||||||
|
onClick={handleUserFilter}
|
||||||
|
size="md"
|
||||||
/>
|
/>
|
||||||
|
{userFilter?.userId === userId && <X className="h-3 w-3 text-muted-foreground" />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
return (
|
|
||||||
<div className="w-12 h-12 rounded border flex items-center justify-center bg-muted">
|
|
||||||
<ImageIcon className="w-6 h-6 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
], [handleRowToggle, handleUserFilter, userFilter])
|
accessorKey: "updated",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
className="h-auto p-0 font-semibold hover:bg-transparent"
|
||||||
|
>
|
||||||
|
Updated
|
||||||
|
<SortDesc className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const date = row.getValue("updated") as string
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-muted-foreground" title={dayjs(date).format("MMMM D, YYYY h:mm A")}>
|
||||||
|
{dayjs(date).fromNow()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<div className="w-12 h-12 rounded border flex items-center justify-center bg-background p-2">
|
||||||
|
<img
|
||||||
|
src={`${pb.baseUrl}/api/files/submissions/${row.original.id}/${assets[0]}?thumb=100x100` || "/placeholder.svg"}
|
||||||
|
alt={name}
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="w-12 h-12 rounded border flex items-center justify-center bg-muted">
|
||||||
|
<ImageIcon className="w-6 h-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[handleRowToggle, handleUserFilter, userFilter],
|
||||||
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: groupedData,
|
data: groupedData,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { pb, type Submission } from "@/lib/pb"
|
|
||||||
import { getAllIcons } from "@/lib/api"
|
import { getAllIcons } from "@/lib/api"
|
||||||
|
import { pb, type Submission } from "@/lib/pb"
|
||||||
|
import { revalidateAllSubmissions } from "@/app/actions/submissions"
|
||||||
|
|
||||||
// Query key factory
|
// Query key factory
|
||||||
export const submissionKeys = {
|
export const submissionKeys = {
|
||||||
@@ -46,10 +47,13 @@ export function useApproveSubmission() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: async (data) => {
|
||||||
// Invalidate and refetch submissions
|
// Invalidate and refetch submissions
|
||||||
queryClient.invalidateQueries({ queryKey: submissionKeys.lists() })
|
queryClient.invalidateQueries({ queryKey: submissionKeys.lists() })
|
||||||
|
|
||||||
|
// Revalidate Next.js cache for community pages
|
||||||
|
await revalidateAllSubmissions()
|
||||||
|
|
||||||
toast.success("Submission approved", {
|
toast.success("Submission approved", {
|
||||||
description: "The submission has been approved successfully",
|
description: "The submission has been approved successfully",
|
||||||
})
|
})
|
||||||
@@ -82,10 +86,13 @@ export function useRejectSubmission() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
// Invalidate and refetch submissions
|
// Invalidate and refetch submissions
|
||||||
queryClient.invalidateQueries({ queryKey: submissionKeys.lists() })
|
queryClient.invalidateQueries({ queryKey: submissionKeys.lists() })
|
||||||
|
|
||||||
|
// Revalidate Next.js cache for community pages
|
||||||
|
await revalidateAllSubmissions()
|
||||||
|
|
||||||
toast.success("Submission rejected", {
|
toast.success("Submission rejected", {
|
||||||
description: "The submission has been rejected",
|
description: "The submission has been rejected",
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user