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)} - /> - -
    -
    -
    - )} - - ); -}