From 0c6d75dfc6024606b3218029c1518ddf638e1ce4 Mon Sep 17 00:00:00 2001 From: MananTank Date: Mon, 28 Jul 2025 00:06:30 +0000 Subject: [PATCH] Dashboard: Migrate Claim condition components from chakra to tailwind, UI improvements (#7731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on enhancing the user interface and functionality of the claim conditions forms in the dashboard application. It includes updates to component structures, styling improvements, and the introduction of new features for better user experience. ### Detailed summary - Removed unused CSV files from the assets. - Updated `variant` prop in `tx-button` to include "outline". - Adjusted styles in `drop-zone` for improved layout. - Enhanced text styles in `price-preview` and `claim-conditions` components. - Replaced `CustomFormControl` with `FormFieldSetup` for better form handling. - Added new components for displaying and managing claim conditions. - Improved error handling and user feedback in various input components. - Refactored `ClaimPriceInput` and other input components to streamline functionality. - Introduced a new `SnapshotDataTable` for better display of CSV data. - Enhanced snapshot upload functionality with clearer requirements and examples. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **New Features** * Added support for an "outline" button style variant in transaction buttons. * **Refactor** * Replaced Chakra UI components with a custom UI library and Tailwind CSS across claim conditions forms and related components. * Simplified and reorganized the layout of claim conditions phases, downloadable code blocks, and snapshot upload views for improved clarity and usability. * Updated CSV upload and data table experience with enhanced error display and a new code example download feature. * **Style** * Improved spacing, typography, and overall visual consistency throughout claim conditions and related dashboard components. * **Chores** * Removed unused or redundant components and files related to previous form controls and CSV tables. --- .../examples/snapshot-with-maxclaimable.csv | 3 - .../examples/snapshot-with-overrides.csv | 3 - .../public/assets/examples/snapshot.csv | 3 - .../blocks/code/downloadable-code.tsx | 53 ++- .../components/blocks/drop-zone/drop-zone.tsx | 2 +- .../src/@/components/tx-button/index.tsx | 2 +- .../Inputs/ClaimPriceInput.tsx | 45 +- .../Inputs/ClaimerSelection.tsx | 104 +++-- .../Inputs/CreatorInput.tsx | 19 +- .../Inputs/MaxClaimablePerWalletInput.tsx | 19 +- .../Inputs/MaxClaimableSupplyInput.tsx | 12 +- .../Inputs/PhaseNameInput.tsx | 10 +- .../Inputs/PhaseStartTimeInput.tsx | 16 +- .../claim-conditions-form/common.tsx | 49 --- .../claim-conditions-form/index.tsx | 249 +++++------ .../claim-conditions-form/phase.tsx | 224 +++++----- .../claim-conditions/claim-conditions.tsx | 6 +- .../claim-conditions/price-input.tsx | 36 +- .../claim-conditions/price-preview.tsx | 8 +- .../quantity-input-with-unlimited.tsx | 28 +- .../reset-claim-eligibility.tsx | 90 ++-- .../claim-conditions/snapshot-upload.tsx | 416 +++++++++--------- .../_components/csv-data-table.tsx | 161 ------- 23 files changed, 648 insertions(+), 910 deletions(-) delete mode 100644 apps/dashboard/public/assets/examples/snapshot-with-maxclaimable.csv delete mode 100644 apps/dashboard/public/assets/examples/snapshot-with-overrides.csv delete mode 100644 apps/dashboard/public/assets/examples/snapshot.csv delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/common.tsx delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/csv-data-table.tsx diff --git a/apps/dashboard/public/assets/examples/snapshot-with-maxclaimable.csv b/apps/dashboard/public/assets/examples/snapshot-with-maxclaimable.csv deleted file mode 100644 index 6db267cebd0..00000000000 --- a/apps/dashboard/public/assets/examples/snapshot-with-maxclaimable.csv +++ /dev/null @@ -1,3 +0,0 @@ -address,maxClaimable -0x0000000000000000000000000000000000000000,2 -0x000000000000000000000000000000000000dEaD,5 \ No newline at end of file diff --git a/apps/dashboard/public/assets/examples/snapshot-with-overrides.csv b/apps/dashboard/public/assets/examples/snapshot-with-overrides.csv deleted file mode 100644 index 29c581674f2..00000000000 --- a/apps/dashboard/public/assets/examples/snapshot-with-overrides.csv +++ /dev/null @@ -1,3 +0,0 @@ -address,maxClaimable,price,currencyAddress -0x0000000000000000000000000000000000000000,2,0.1,0x0000000000000000000000000000000000000000 -0x000000000000000000000000000000000000dEaD,5,2.5,0x0000000000000000000000000000000000000000 \ No newline at end of file diff --git a/apps/dashboard/public/assets/examples/snapshot.csv b/apps/dashboard/public/assets/examples/snapshot.csv deleted file mode 100644 index 7a2c24783ce..00000000000 --- a/apps/dashboard/public/assets/examples/snapshot.csv +++ /dev/null @@ -1,3 +0,0 @@ -address -0x0000000000000000000000000000000000000000 -0x000000000000000000000000000000000000dEaD \ No newline at end of file diff --git a/apps/dashboard/src/@/components/blocks/code/downloadable-code.tsx b/apps/dashboard/src/@/components/blocks/code/downloadable-code.tsx index bd25fb51343..b72009b2743 100644 --- a/apps/dashboard/src/@/components/blocks/code/downloadable-code.tsx +++ b/apps/dashboard/src/@/components/blocks/code/downloadable-code.tsx @@ -1,5 +1,5 @@ "use client"; -import { ArrowDownToLineIcon } from "lucide-react"; +import { ArrowDownToLineIcon, FileTextIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { CodeClient } from "@/components/ui/code/code.client"; import { handleDownload } from "../download-file-button"; @@ -10,27 +10,36 @@ export function DownloadableCode(props: { fileNameWithExtension: string; }) { return ( -
- - - +
+

+ + + {props.fileNameWithExtension} + + +

+
+ +
); } diff --git a/apps/dashboard/src/@/components/blocks/drop-zone/drop-zone.tsx b/apps/dashboard/src/@/components/blocks/drop-zone/drop-zone.tsx index 519e456254e..054d60f41a2 100644 --- a/apps/dashboard/src/@/components/blocks/drop-zone/drop-zone.tsx +++ b/apps/dashboard/src/@/components/blocks/drop-zone/drop-zone.tsx @@ -26,7 +26,7 @@ export function DropZone(props: { return (
& { transactionCount: number | undefined; // support for unknown number of tx count isPending: boolean; txChainID: number; - variant?: "destructive" | "primary" | "default"; + variant?: "destructive" | "primary" | "default" | "outline"; isLoggedIn: boolean; checkBalance?: boolean; client: ThirdwebClient; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimPriceInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimPriceInput.tsx index ff2a74a28ad..c7a974bdcde 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimPriceInput.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimPriceInput.tsx @@ -1,47 +1,40 @@ -import { Box, Flex } from "@chakra-ui/react"; import { NATIVE_TOKEN_ADDRESS } from "thirdweb"; import { CurrencySelector } from "@/components/blocks/CurrencySelector"; +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { cn } from "@/lib/utils"; import { PriceInput } from "../../price-input"; import { useClaimConditionsFormContext } from ".."; -import { CustomFormControl } from "../common"; /** * Allows the user to select how much they want to charge to claim each NFT */ export const ClaimPriceInput = (props: { contractChainId: number }) => { - const { - formDisabled, - isErc20, - form, - phaseIndex, - field, - isColumn, - claimConditionType, - } = useClaimConditionsFormContext(); + const { formDisabled, isErc20, form, phaseIndex, field, claimConditionType } = + useClaimConditionsFormContext(); if (claimConditionType === "creator") { return null; } return ( - - - - form.setValue(`phases.${phaseIndex}.price`, val)} - value={field.price?.toString() || ""} - w="full" - /> - - +
+ form.setValue(`phases.${phaseIndex}.price`, val)} + value={field.price?.toString() || ""} + disabled={formDisabled} + className="max-w-48" + /> +
{ } value={field?.currencyAddress || NATIVE_TOKEN_ADDRESS} /> - - - +
+
+ ); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx index 932e065a672..09685ce1210 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx @@ -1,8 +1,15 @@ -import { Box, Flex, Select } from "@chakra-ui/react"; import { UploadIcon } from "lucide-react"; +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { Button } from "@/components/ui/button"; -import { useClaimConditionsFormContext } from ".."; -import { CustomFormControl } from "../common"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import { useClaimConditionsFormContext } from "../index"; /** * Allows the user to @@ -19,12 +26,11 @@ export const ClaimerSelection = () => { isErc20, setOpenSnapshotIndex: setOpenIndex, isAdmin, - isColumn, claimConditionType, } = useClaimConditionsFormContext(); - const handleClaimerChange = (e: React.ChangeEvent) => { - const val = e.currentTarget.value as "any" | "specific" | "overrides"; + const handleClaimerChange = (value: string) => { + const val = value as "any" | "specific" | "overrides"; if (val === "any") { form.setValue(`phases.${phaseIndex}.snapshot`, undefined); @@ -80,79 +86,69 @@ export const ClaimerSelection = () => { : `Who can claim ${isErc20 ? "tokens" : "NFTs"} during this phase?`; return ( - - +
{claimConditionType === "overrides" || claimConditionType === "specific" ? null : ( )} {/* Edit or See Snapshot */} {field.snapshot ? ( - +
{/* disable the "Edit" button when form is disabled, but not when it's a "See" button */} - -

- ●{" "} - - {field.snapshot?.length} address - {field.snapshot?.length === 1 ? "" : "es"} - {" "} - in snapshot -

-
- - ) : ( - - )} - - +
+ + {field.snapshot?.length}{" "} + {field.snapshot?.length === 1 ? "address" : "addresses"} in + snapshot + +
+
+ ) : null} +
+ ); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/CreatorInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/CreatorInput.tsx index b5510fcea4b..e4c220a49b8 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/CreatorInput.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/CreatorInput.tsx @@ -1,6 +1,6 @@ import { useActiveAccount } from "thirdweb/react"; +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { Input } from "@/components/ui/input"; -import { CustomFormControl } from "../common"; import { useClaimConditionsFormContext } from "../index"; /** @@ -14,8 +14,7 @@ interface CreatorInputProps { export const CreatorInput: React.FC = ({ creatorAddress, }) => { - const { formDisabled, claimConditionType, isAdmin } = - useClaimConditionsFormContext(); + const { claimConditionType, isAdmin } = useClaimConditionsFormContext(); const walletAddress = useActiveAccount()?.address; if (claimConditionType !== "creator") { @@ -23,8 +22,9 @@ export const CreatorInput: React.FC = ({ } return ( - This wallet address will be able to indefinitely claim.{" "} @@ -34,7 +34,12 @@ export const CreatorInput: React.FC = ({ } label="Creator address" > - - + + ); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimablePerWalletInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimablePerWalletInput.tsx index 50b6362804e..c9dc75a2f03 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimablePerWalletInput.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimablePerWalletInput.tsx @@ -1,6 +1,6 @@ -import Link from "next/link"; +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { UnderlineLink } from "@/components/ui/UnderlineLink"; import { QuantityInputWithUnlimited } from "../../quantity-input-with-unlimited"; -import { CustomFormControl } from "../common"; import { useClaimConditionsFormContext } from "../index"; /** @@ -23,13 +23,13 @@ export const MaxClaimablePerWalletInput: React.FC = () => { } return ( - @@ -39,14 +39,13 @@ export const MaxClaimablePerWalletInput: React.FC = () => { : ". "} Limits are set per wallets and not per user, sophisticated actors could get around wallet restrictions.{" "} - Learn more - + . } @@ -66,6 +65,6 @@ export const MaxClaimablePerWalletInput: React.FC = () => { } value={field?.maxClaimablePerWallet?.toString() || "0"} /> - + ); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimableSupplyInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimableSupplyInput.tsx index 5af987c32ef..bd751bf67ad 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimableSupplyInput.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimableSupplyInput.tsx @@ -1,6 +1,6 @@ +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { QuantityInputWithUnlimited } from "../../quantity-input-with-unlimited"; import { useClaimConditionsFormContext } from ".."; -import { CustomFormControl } from "../common"; /** * Allows the user to select how many NFTs will be dropped in a phase @@ -21,13 +21,13 @@ export const MaxClaimableSupplyInput: React.FC = () => { } return ( - { } value={field.maxClaimableSupply?.toString() || "0"} /> - + ); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/PhaseNameInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/PhaseNameInput.tsx index 0ee9fe6647e..c2051363c60 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/PhaseNameInput.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/PhaseNameInput.tsx @@ -1,5 +1,5 @@ +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { Input } from "@/components/ui/input"; -import { CustomFormControl } from "../common"; import { useClaimConditionsFormContext } from "../index"; /** @@ -12,13 +12,15 @@ export const PhaseNameInput: React.FC = () => { const inputValue = field.metadata?.name; return ( - { form.setValue(`phases.${phaseIndex}.metadata.name`, e.target.value); }} @@ -26,6 +28,6 @@ export const PhaseNameInput: React.FC = () => { type="text" value={inputValue} /> - + ); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/PhaseStartTimeInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/PhaseStartTimeInput.tsx index f5abb36f93e..5ede81c888f 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/PhaseStartTimeInput.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/PhaseStartTimeInput.tsx @@ -1,6 +1,6 @@ -import { Input } from "@chakra-ui/react"; +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { Input } from "@/components/ui/input"; import { toDateTimeLocal } from "@/utils/date-utils"; -import { CustomFormControl } from "../common"; import { useClaimConditionsFormContext } from "../index"; /** @@ -10,16 +10,18 @@ export const PhaseStartTimeInput: React.FC = () => { const { form, phaseIndex, field, formDisabled } = useClaimConditionsFormContext(); return ( - form.setValue( `phases.${phaseIndex}.startTime`, @@ -29,6 +31,6 @@ export const PhaseStartTimeInput: React.FC = () => { type="datetime-local" value={toDateTimeLocal(field.startTime)} /> - + ); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/common.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/common.tsx deleted file mode 100644 index 3ca2568a5f1..00000000000 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/common.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Flex, FormControl } from "@chakra-ui/react"; -import { FormErrorMessage, FormHelperText, FormLabel } from "chakra/form"; -import type { FieldError } from "react-hook-form"; -import type { ComponentWithChildren } from "@/types/component-with-children"; -import { useClaimConditionsFormContext } from "."; - -interface CustomFormControlProps { - disabled: boolean; - label: string; - error?: FieldError; - helperText?: React.ReactNode; -} - -export const CustomFormControl: ComponentWithChildren< - CustomFormControlProps -> = (props) => { - return ( - - {/* label */} - {props.label} - - {/* input */} - {props.children} - - {/* error message */} - {props.error && ( - {props.error.message} - )} - - {/* helper text */} - {props.helperText && {props.helperText}} - - ); -}; - -export const CustomFormGroup: ComponentWithChildren = ({ children }) => { - const { isColumn } = useClaimConditionsFormContext(); - return ( - - {children} - - ); -}; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx index dd54ade9fc7..bf6ff4f71c7 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx @@ -1,21 +1,11 @@ "use client"; import { - Alert, - AlertDescription, - AlertIcon, - AlertTitle, - Box, - Flex, - Menu, - MenuButton, - MenuItem, - MenuList, -} from "@chakra-ui/react"; -import { Button } from "chakra/button"; -import { Heading } from "chakra/heading"; -import { Text } from "chakra/text"; -import { CircleHelpIcon, PlusIcon } from "lucide-react"; + ArrowDownToLineIcon, + CircleAlertIcon, + CircleHelpIcon, + PlusIcon, +} from "lucide-react"; import { createContext, Fragment, useContext, useMemo, useState } from "react"; import { type UseFieldArrayReturn, @@ -38,8 +28,16 @@ import { import invariant from "tiny-invariant"; import * as z from "zod"; import { ZodError } from "zod"; -import { AdminOnly } from "@/components/contracts/roles/admin-only"; import { TransactionButton } from "@/components/tx-button"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Form } from "@/components/ui/form"; import { Spinner } from "@/components/ui/Spinner/Spinner"; import { ToolTipLabel } from "@/components/ui/tooltip"; import { useIsAdmin } from "@/hooks/useContractRoles"; @@ -286,7 +284,7 @@ export const ClaimConditionsForm: React.FC = ({ values: { phases: transformedQueryData }, }); - const { append, remove, fields } = useFieldArray({ + const formFields = useFieldArray({ control: form.control, name: "phases", }); @@ -296,14 +294,14 @@ export const ClaimConditionsForm: React.FC = ({ switch (type) { case "public": - append({ + formFields.append({ ...DEFAULT_PHASE, metadata: { name }, }); break; case "specific": - append({ + formFields.append({ ...DEFAULT_PHASE, maxClaimablePerWallet: "0", metadata: { name }, @@ -312,7 +310,7 @@ export const ClaimConditionsForm: React.FC = ({ break; case "overrides": - append({ + formFields.append({ ...DEFAULT_PHASE, maxClaimablePerWallet: "1", metadata: { name }, @@ -321,7 +319,7 @@ export const ClaimConditionsForm: React.FC = ({ break; case "creator": - append({ + formFields.append({ ...DEFAULT_PHASE, maxClaimablePerWallet: "0", maxClaimableSupply: "unlimited", @@ -340,19 +338,15 @@ export const ClaimConditionsForm: React.FC = ({ break; default: - append({ + formFields.append({ ...DEFAULT_PHASE, metadata: { name }, }); } }; - const removePhase = (index: number) => { - remove(index); - }; - const phases = form.watch("phases"); - const controlledFields = fields.map((field, index) => { + const controlledFields = formFields.fields.map((field, index) => { return { ...field, ...phases[index], @@ -428,25 +422,6 @@ export const ClaimConditionsForm: React.FC = ({ return phaseId; }, [controlledFields]); - const { hasAddedPhases, hasRemovedPhases } = useMemo(() => { - const initialPhases = claimConditionsQuery.data || []; - const currentPhases = controlledFields; - - const _hasAddedPhases = - currentPhases.length > initialPhases.length && - claimConditionsQuery?.data?.length === 0 && - controlledFields?.length > 0; - const _hasRemovedPhases = - currentPhases.length < initialPhases.length && - isMultiPhase && - controlledFields?.length === 0; - - return { - hasAddedPhases: _hasAddedPhases, - hasRemovedPhases: _hasRemovedPhases, - }; - }, [claimConditionsQuery.data, controlledFields, isMultiPhase]); - if (claimConditionsQuery.isPending) { return (
@@ -465,12 +440,11 @@ export const ClaimConditionsForm: React.FC = ({ } return ( - - +
+ {/* Show the reason why the form is disabled */} - {!isAdmin && ( - Connect with admin wallet to edit claim conditions. - )} + {!isAdmin &&

Connect with admin wallet to edit claim conditions.

} + {controlledFields.map((field, index) => { const dropType: DropType = field.snapshot ? field.maxClaimablePerWallet?.toString() === "0" @@ -535,7 +509,7 @@ export const ClaimConditionsForm: React.FC = ({ contract={contract} isPending={sendTx.isPending} onRemove={() => { - removePhase(index); + formFields.remove(index); }} /> @@ -544,45 +518,42 @@ export const ClaimConditionsForm: React.FC = ({ })} {phases?.length === 0 && ( - - -
- - {isMultiPhase - ? "Missing Claim Phases" - : "Missing Claim Conditions"} - - - {isMultiPhase - ? "You need to set at least one claim phase for people to claim this drop." - : "You need to set claim conditions for people to claim this drop."} - -
+ + + + {isMultiPhase + ? "Missing Claim Phases" + : "Missing Claim Conditions"} + + + {isMultiPhase + ? "You need to set at least one claim phase for people to claim this drop." + : "You need to set claim conditions for people to claim this drop."} + )} - -
- - - 0) - } - leftIcon={} - size="sm" - variant={phases?.length > 0 ? "outline" : "solid"} + {isAdmin && ( +
+
+ + + + + - Add {isMultiPhase ? "Phase" : "Claim Conditions"} - - {Object.keys(ClaimConditionTypeData).map((key) => { const type = key as ClaimConditionType; @@ -591,7 +562,7 @@ export const ClaimConditionsForm: React.FC = ({ } return ( - { addPhase(type); @@ -600,72 +571,54 @@ export const ClaimConditionsForm: React.FC = ({ >
{ClaimConditionTypeData[type].name} - + {ClaimConditionTypeData[type].description} - +

} - /> + > + +
-
+ ); })} -
-
-
- - {controlledFields.some((field) => field.fromSdk) && ( - - )} -
+ + +
-
- }> - - {(hasRemovedPhases || hasAddedPhases) && ( - - You have unsaved changes - - )} - {controlledFields.length > 0 || - hasRemovedPhases || - !isMultiPhase ? ( - - {claimConditionsQuery.isPending - ? "Saving Phases" - : "Save Phases"} - - ) : null} - - +
+ + + {claimConditionsQuery.isPending + ? "Saving Phases" + : "Save Phases"} + + + {controlledFields.some((field) => field.fromSdk) && ( + + )} +
-
-
- - ); -}; - -const TooltipBox: React.FC<{ - content: React.ReactNode; -}> = ({ content }) => { - return ( - {content}
}> - - + )} + + ); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx index 9b6ffce6500..0ab37408e33 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx @@ -1,12 +1,12 @@ -import { ChevronDownIcon, ChevronUpIcon, XIcon } from "lucide-react"; +import { formatDate } from "date-fns"; +import { ChevronDownIcon, XIcon } from "lucide-react"; import type { ThirdwebContract } from "thirdweb"; -import { AdminOnly } from "@/components/contracts/roles/admin-only"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; +import { DynamicHeight } from "@/components/ui/DynamicHeight"; +import { cn } from "@/lib/utils"; import { PricePreview } from "../price-preview"; import { ClaimConditionTypeData, useClaimConditionsFormContext } from "."; -import { CustomFormGroup } from "./common"; import { ClaimerSelection } from "./Inputs/ClaimerSelection"; import { ClaimPriceInput } from "./Inputs/ClaimPriceInput"; import { CreatorInput } from "./Inputs/CreatorInput"; @@ -42,114 +42,130 @@ export const ClaimConditionsPhase: React.FC = ({ }; return ( - -
- - + + {field.isEditing && ( +
+ {/* Phase Name Input / Form Title */} + {isMultiPhase ? : null} + + + + + + + + {claimConditionType === "specific" || + claimConditionType === "creator" ? null : ( + + )} + + +
+ )} +
+ +
- -
-
-
-

- {ClaimConditionTypeData[claimConditionType].name} -

- {isActive && ( - - Currently active - + {isAdmin && ( + )}
- -

- {ClaimConditionTypeData[claimConditionType].description} -

- - {!field.isEditing ? ( -
-
-

Phase start

-

- {field.startTime?.toLocaleString()} -

-
-
-

- {isErc20 ? "Tokens" : "NFTs"} to drop -

-

- {field.maxClaimableSupply} -

-
- -
-

Limit per wallet

- {claimConditionType === "specific" ? ( -

Set in the snapshot

- ) : claimConditionType === "creator" ? ( -

Unlimited

- ) : ( -

- {field.maxClaimablePerWallet} -

- )} -
-
- ) : ( - <> - - {/* Phase Name Input / Form Title */} - {isMultiPhase ? : null} - - - - - - - - - - - {claimConditionType === "specific" || - claimConditionType === "creator" ? null : ( - - - - )} - - - - )} -
+ ); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions.tsx index dfac6793bb2..56a7c4c2558 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions.tsx @@ -20,14 +20,14 @@ export const ClaimConditions: React.FC = ({ isMultiphase, }) => { return ( -
+
-

+

Set Claim Conditions

Control when the {isERC20 ? "tokens" : "NFTs"} get dropped, how much - they cost, and more. + they cost, and more

diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/price-input.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/price-input.tsx index d1aaf1fa26f..6e2610350f3 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/price-input.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/price-input.tsx @@ -1,15 +1,9 @@ -import { - InputGroup, - NumberInput, - NumberInputField, - type NumberInputProps, -} from "@chakra-ui/react"; +import { DecimalInput } from "@/components/ui/decimal-input"; + +type InputProps = React.ComponentProps; interface PriceInputProps - extends Omit< - NumberInputProps, - "onChange" | "value" | "onBlur" | "max" | "min" - > { + extends Omit { value: string; onChange: (value: string) => void; } @@ -20,17 +14,15 @@ export const PriceInput: React.FC = ({ ...restInputProps }) => { return ( - - - { - if (e.target.value === "" || Number(e.target.value) < 0) { - return onChange("0"); - } - onChange(e.target.value); - }} - /> - - + { + if (value === "" || Number(value) < 0) { + return onChange("0"); + } + onChange(value); + }} + value={value} + /> ); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/price-preview.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/price-preview.tsx index 4fb90e1b152..c368858b1b6 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/price-preview.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/price-preview.tsx @@ -24,12 +24,12 @@ export const PricePreview: React.FC = ({ ); return ( -
-

Default price

+
+

Default price

{Number(price) === 0 ? ( -

Free

+

Free

) : ( -

+

{price}{" "} {foundCurrency ? foundCurrency.symbol diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/quantity-input-with-unlimited.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/quantity-input-with-unlimited.tsx index e8a31f23d0b..5d22a43762c 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/quantity-input-with-unlimited.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/quantity-input-with-unlimited.tsx @@ -2,25 +2,23 @@ import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -interface QuantityInputWithUnlimitedProps { +export function QuantityInputWithUnlimited(props: { value: string; onChange: (value: string) => void; hideMaxButton?: true; decimals?: number; isDisabled: boolean; isRequired: boolean; -} +}) { + const { + value = "0", + onChange, + hideMaxButton, + isDisabled, + isRequired, + decimals, + } = props; -export const QuantityInputWithUnlimited: React.FC< - QuantityInputWithUnlimitedProps -> = ({ - value = "0", - onChange, - hideMaxButton, - isDisabled, - isRequired, - decimals, -}) => { const [stringValue, setStringValue] = useState( Number.isNaN(Number(value)) ? "0" : value.toString(), ); @@ -45,7 +43,7 @@ export const QuantityInputWithUnlimited: React.FC< }; return ( -

+
{hideMaxButton ? null : (
); -}; +} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/reset-claim-eligibility.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/reset-claim-eligibility.tsx index c91b14460be..44233ca0454 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/reset-claim-eligibility.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/reset-claim-eligibility.tsx @@ -1,31 +1,28 @@ "use client"; -import { CircleHelpIcon } from "lucide-react"; +import { CircleHelpIcon, RefreshCcwIcon } from "lucide-react"; import type { ThirdwebContract } from "thirdweb"; import * as ERC20Ext from "thirdweb/extensions/erc20"; import * as ERC721Ext from "thirdweb/extensions/erc721"; import * as ERC1155Ext from "thirdweb/extensions/erc1155"; import { useSendAndConfirmTransaction } from "thirdweb/react"; -import { AdminOnly } from "@/components/contracts/roles/admin-only"; import { TransactionButton } from "@/components/tx-button"; import { ToolTipLabel } from "@/components/ui/tooltip"; import { useTxNotifications } from "@/hooks/useTxNotifications"; -interface ResetClaimEligibilityProps { - isErc20: boolean; - contract: ThirdwebContract; - tokenId?: string; - isLoggedIn: boolean; - isMultiphase: boolean; -} - -export const ResetClaimEligibility: React.FC = ({ +export function ResetClaimEligibility({ contract, tokenId, isErc20, isLoggedIn, isMultiphase, -}) => { +}: { + isErc20: boolean; + contract: ThirdwebContract; + tokenId?: string; + isLoggedIn: boolean; + isMultiphase: boolean; +}) { const sendTxMutation = useSendAndConfirmTransaction(); const txNotification = useTxNotifications( @@ -70,38 +67,39 @@ export const ResetClaimEligibility: React.FC = ({ } return ( - }> - - {sendTxMutation.isPending ? ( - "Resetting Eligibility" - ) : ( -
- Reset Eligibility - - This {`contract's`} claim eligibility stores who has already - claimed {isErc20 ? "tokens" : "NFTs"} from this contract and - carries across claim phases. Resetting claim eligibility will - reset this state permanently, and wallets that have already - claimed to their limit will be able to claim again. - - } - > - - -
- )} -
-
+ + {sendTxMutation.isPending ? ( + "Resetting Eligibility" + ) : ( +
+ + Reset Eligibility + + This {`contract's`} claim eligibility stores who has already + claimed {isErc20 ? "tokens" : "NFTs"} from this contract and + carries across claim phases. Resetting claim eligibility will + reset this state permanently, and wallets that have already + claimed to their limit will be able to claim again. + + } + > + + +
+ )} +
); -}; +} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx index c2773aad0c6..552ad8a6953 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx @@ -1,11 +1,14 @@ -import { CircleAlertIcon, DownloadIcon, UploadIcon } from "lucide-react"; -import { useRef } from "react"; -import { useDropzone } from "react-dropzone"; -import type { Column } from "react-table"; +import { + ArrowRightIcon, + CircleAlertIcon, + CircleSlashIcon, + RotateCcwIcon, +} from "lucide-react"; import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb"; +import { DownloadableCode } from "@/components/blocks/code/downloadable-code"; +import { DropZone } from "@/components/blocks/drop-zone/drop-zone"; import { Button } from "@/components/ui/button"; import { InlineCode } from "@/components/ui/inline-code"; -import { UnorderedList } from "@/components/ui/List/List"; import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Sheet, @@ -13,10 +16,17 @@ import { SheetHeader, SheetTitle, } from "@/components/ui/sheet"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { ToolTipLabel } from "@/components/ui/tooltip"; import { useCsvUpload } from "@/hooks/useCsvUpload"; -import { cn } from "@/lib/utils"; -import { CsvDataTable } from "../csv-data-table"; interface SnapshotAddressInput { address: string; @@ -45,6 +55,72 @@ const csvParser = (items: SnapshotAddressInput[]): SnapshotAddressInput[] => { .filter(({ address }) => address !== ""); }; +function SnapshotDataTable({ data }: { data: SnapshotAddressInput[] }) { + return ( + + + + + Address + Max claimable + Price + Currency Address + + + + {data.map((item) => ( + + + {item.isValid ? ( + item.address + ) : ( + +
+ +
+ {item.address} +
+
+
+ )} +
+ + {item.maxClaimable === "0" || !item.maxClaimable + ? "Default" + : item.maxClaimable === "unlimited" + ? "Unlimited" + : item.maxClaimable} + + + {item.price === "0" + ? "Free" + : !item.price || item.price === "unlimited" + ? "Default" + : item.price} + + + {item.currencyAddress === + "0x0000000000000000000000000000000000000000" || + !item.currencyAddress + ? "Default" + : item.currencyAddress} + +
+ ))} +
+
+
+ ); +} + const SnapshotViewerSheetContent: React.FC = ({ setSnapshot, dropType, @@ -59,11 +135,6 @@ const SnapshotViewerSheetContent: React.FC = ({ defaultRawData: value, }); - const dropzone = useDropzone({ - onDrop: csvUpload.setFiles, - }); - - const paginationPortalRef = useRef(null); const normalizeData = csvUpload.normalizeQuery.data; if (!normalizeData) { @@ -88,174 +159,122 @@ const SnapshotViewerSheetContent: React.FC = ({ onClose(); }; - const columns = [ - { - accessor: ({ address, isValid }) => { - if (isValid) { - return address; - } - return ( - -
- -
- {address} -
-
-
- ); - }, - Header: "Address", - }, - { - accessor: ({ maxClaimable }) => { - return maxClaimable === "0" || !maxClaimable - ? "Default" - : maxClaimable === "unlimited" - ? "Unlimited" - : maxClaimable; - }, - Header: "Max claimable", - }, - { - accessor: ({ price }) => { - return price === "0" - ? "Free" - : !price || price === "unlimited" - ? "Default" - : price; - }, - Header: "Price", - }, - { - accessor: ({ currencyAddress }) => { - return currencyAddress === - "0x0000000000000000000000000000000000000000" || !currencyAddress - ? "Default" - : currencyAddress; - }, - Header: "Currency Address", - }, - ] as Column[]; - return ( -
+
{csvUpload.rawData.length > 0 ? (
- - columns={columns} - data={csvUpload.normalizeQuery.data.result} - portalRef={paginationPortalRef} - /> -
- ) : ( -
-
-
+
+
+ + Reset + + + {csvUpload.normalizeQuery.data?.invalidFound ? ( + + ) : ( + + )}
-
-

Requirements

- +
+ ) : ( +
+ csvUpload.reset() }} + accept=".csv" + /> + +
+

Requirements

+
{dropType === "specific" ? ( <> -
  • +

    Files must contain one .csv file with a list of addresses and their . (amount each wallet is allowed to claim) -
    - - Example - snapshot - -

  • -
  • +

    + + + +

    You may optionally add and overrides as well. This lets you override the currency and price you would like to charge per wallet you specified -
    - - Example - snapshot - -

  • +

    + + ) : ( <> -
  • +

    Files must contain one .csv file with a list of addresses. -
    - - Example - snapshot - -

  • -
  • +

    + + + +

    You may optionally add a column override. (amount each wallet is allowed to claim) If not specified, the default value is the one you have set on your claim phase. -
    - - Example - snapshot - -

  • -
  • +

    + + + +

    You may optionally add and overrides. This lets you override the currency and price you would like to charge @@ -264,67 +283,27 @@ const SnapshotViewerSheetContent: React.FC = ({ When defining a custom currency address, you must also define a price override. -
    - - Example - snapshot - -

  • +

    + + )} -
  • +

    Repeated addresses will be removed and only the first found will be kept. -

  • -
  • +

    +

    The limit you set is for the maximum amount of NFTs a wallet can claim, not how many they can receive in total. -

  • - +

    +
    )} -
    -
    - {!isDisabled && ( -
    - - {csvUpload.normalizeQuery.data?.invalidFound ? ( - - ) : ( - - )} -
    - )} -
    ); }; @@ -343,8 +322,8 @@ export function SnapshotViewerSheet( }} open={props.isOpen} > - - + + Snapshot @@ -352,3 +331,18 @@ export function SnapshotViewerSheet( ); } + +const snapshotWithMaxClaimable = `\ +address,maxClaimable +0x0000000000000000000000000000000000000000,2 +0x000000000000000000000000000000000000dEaD,5`; + +const snapshotWithOverrides = `\ +address,maxClaimable,price,currencyAddress +0x0000000000000000000000000000000000000000,2,0.1,0x0000000000000000000000000000000000000000 +0x000000000000000000000000000000000000dEaD,5,2.5,0x0000000000000000000000000000000000000000`; + +const snapshotCSV = `\ +address +0x0000000000000000000000000000000000000000 +0x000000000000000000000000000000000000dEaD`; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/csv-data-table.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/csv-data-table.tsx deleted file mode 100644 index d11f1506011..00000000000 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/csv-data-table.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { - IconButton, - Portal, - Select, - Table, - Tbody, - Td, - Th, - Thead, - Tr, -} from "@chakra-ui/react"; -import { - ChevronFirstIcon, - ChevronLastIcon, - ChevronLeftIcon, - ChevronRightIcon, -} from "lucide-react"; -import { type Column, usePagination, useTable } from "react-table"; -import { TableContainer } from "@/components/ui/table"; - -interface CsvDataTableProps { - data: T[]; - portalRef: React.RefObject; - columns: Column[]; -} -/** - * Display the data uploaded from useCsvUpload, using react-table - */ -export function CsvDataTable({ - data, - portalRef, - columns, -}: CsvDataTableProps) { - const { - getTableProps, - getTableBodyProps, - headerGroups, - prepareRow, - // Instead of using 'rows', we'll use page, - page, - // which has only the rows for the active page - // The rest of these things are super handy, too ;) - canPreviousPage, - canNextPage, - pageOptions, - pageCount, - gotoPage, - nextPage, - previousPage, - setPageSize, - state: { pageIndex, pageSize }, - } = useTable( - { - columns, - data, - initialState: { - pageIndex: 0, - pageSize: 50, - }, - }, - // old package: this will be removed - // eslint-disable-next-line react-compiler/react-compiler - usePagination, - ); - return ( - <> - - - - {headerGroups.map((headerGroup, headerGroupIndex) => ( - // biome-ignore lint/suspicious/noArrayIndexKey: FIXME - - {headerGroup.headers.map((column, columnIndex) => ( - - ))} - - ))} - - - {page.map((row, rowIndex) => { - prepareRow(row); - return ( - // biome-ignore lint/suspicious/noArrayIndexKey: FIXME - - {row.cells.map((cell, cellIndex) => ( - - ))} - - ); - })} - -
    -

    - {column.render("Header")} -

    -
    - {cell.render("Cell")} -
    -
    - {/* Only need to show the Pagination components if we have more than 25 records */} - {data.length > 0 && ( - -
    -
    - } - isDisabled={!canPreviousPage} - onClick={() => gotoPage(0)} - /> - } - isDisabled={!canPreviousPage} - onClick={() => previousPage()} - /> -

    - Page {pageIndex + 1} of{" "} - {pageOptions.length} -

    - } - isDisabled={!canNextPage} - onClick={() => nextPage()} - /> - } - isDisabled={!canNextPage} - onClick={() => gotoPage(pageCount - 1)} - /> - -
    -
    -
    - )} - - ); -}