+
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.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 (
-
-
-
- );
- },
- 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 ? (
+
+ ) : (
+
+ )}
-
+ ) : (
+
+
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) => (
-
-
- {column.render("Header")}
-
- |
- ))}
-
- ))}
-
-
- {page.map((row, rowIndex) => {
- prepareRow(row);
- return (
- // biome-ignore lint/suspicious/noArrayIndexKey: FIXME
-
- {row.cells.map((cell, cellIndex) => (
-
- {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)}
- />
-
-
-
-
- )}
- >
- );
-}