diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 0f6dcdca315..ce0efc21bb3 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -74,6 +74,7 @@ "next-themes": "^0.4.4", "nextjs-toploader": "^1.6.12", "openapi-types": "^12.1.3", + "p-limit": "^6.2.0", "papaparse": "^5.4.1", "pluralize": "^8.0.0", "posthog-js": "1.67.1", diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx index fb4d896c75c..2e92ba78d9c 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx @@ -50,7 +50,7 @@ import { type SnapshotEntry, } from "../legacy-zod-schema"; import { ResetClaimEligibility } from "../reset-claim-eligibility"; -import { SnapshotUpload } from "../snapshot-upload"; +import { SnapshotViewerSheet } from "../snapshot-upload"; import { getClaimPhasesInLegacyFormat, setClaimPhasesTx } from "./hooks"; import { ClaimConditionsPhase } from "./phase"; @@ -511,11 +511,12 @@ export const ClaimConditionsForm: React.FC = ({ return ( - { + setOpenSnapshotIndex(-1); + }} value={snapshotValue} setSnapshot={(snapshot) => form.setValue(`phases.${index}.snapshot`, snapshot) diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx index 853ae7956a0..d73f66f1328 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx @@ -1,4 +1,5 @@ import { UnorderedList } from "@/components/ui/List/List"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Button } from "@/components/ui/button"; import { InlineCode } from "@/components/ui/inline-code"; import { @@ -11,7 +12,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { useCsvUpload } from "hooks/useCsvUpload"; import { CircleAlertIcon, DownloadIcon, UploadIcon } from "lucide-react"; -import { type Dispatch, type SetStateAction, useRef } from "react"; +import { useRef } from "react"; import type { Column } from "react-table"; import { ZERO_ADDRESS } from "thirdweb"; import { CsvDataTable } from "../csv-data-table"; @@ -25,12 +26,10 @@ interface SnapshotAddressInput { } interface SnapshotUploadProps { setSnapshot: (snapshot: SnapshotAddressInput[]) => void; - snapshotIndex: number; - setOpenSnapshotIndex: Dispatch>; - index: number; dropType: "specific" | "any" | "overrides"; isDisabled: boolean; value?: SnapshotAddressInput[] | undefined; + onClose: () => void; } const csvParser = (items: SnapshotAddressInput[]): SnapshotAddressInput[] => { @@ -44,14 +43,12 @@ const csvParser = (items: SnapshotAddressInput[]): SnapshotAddressInput[] => { .filter(({ address }) => address !== ""); }; -export const SnapshotUpload: React.FC = ({ +const SnapshotViewerSheetContent: React.FC = ({ setSnapshot, - snapshotIndex, - index, - setOpenSnapshotIndex, dropType, isDisabled, value, + onClose, }) => { const { normalizeQuery, @@ -65,19 +62,28 @@ export const SnapshotUpload: React.FC = ({ } = useCsvUpload({ csvParser, defaultRawData: value }); const paginationPortalRef = useRef(null); + const normalizeData = normalizeQuery.data; + + if (!normalizeData) { + return ( +
+ +
+ ); + } const onSave = () => { // Make sure we are not passing ENS values to the claim-condition extension // we should use the `resolvedAddress` value instead setSnapshot( - normalizeQuery.data.result.map((o) => ({ + normalizeData.result.map((o) => ({ address: o.resolvedAddress, maxClaimable: o.maxClaimable, price: o.price, currencyAddress: o.currencyAddress, })), ); - setOpenSnapshotIndex(-1); + onClose(); }; const columns = [ @@ -138,193 +144,205 @@ export const SnapshotUpload: React.FC = ({ }, ] as Column[]; - const handleOpenChange = (isOpen: boolean) => { - setOpenSnapshotIndex(isOpen ? snapshotIndex : -1); - }; - return ( - - - - - {rawData.length ? "Edit" : "Upload"} Snapshot - - -
- {rawData.length > 0 ? ( - - portalRef={paginationPortalRef} - data={normalizeQuery.data.result} - columns={columns} - /> - ) : ( -
-
-
+ {rawData.length > 0 ? ( +
+ + portalRef={paginationPortalRef} + data={normalizeQuery.data.result} + columns={columns} + /> +
+ ) : ( +
+
+
+ +
+ - -
- - {isDragActive ? ( -

Drop the files here

- ) : ( -

- {noCsv - ? `No valid CSV file found, make sure your CSV includes the "address" column.` - : "Drag & Drop a CSV file here"} -

- )} -
-
+ /> + {isDragActive ? ( +

Drop the files here

+ ) : ( +

+ {noCsv + ? `No valid CSV file found, make sure your CSV includes the "address" column.` + : "Drag & Drop a CSV file here"} +

+ )}
-
-

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 per wallet you specified.{" "} - - When defining a custom currency address, you must also - define a price override. - -
    - - Example - snapshot - -
  • - - )} +
    +
    +
    +

    Requirements

    + + {dropType === "specific" ? ( + <>
  • - Repeated addresses will be removed and only the first found - will be kept. + Files must contain one .csv file with a list of + addresses and their . + (amount each wallet is allowed to claim) +
    + + Example + snapshot +
  • - The limit you set is for the maximum amount of NFTs a wallet - can claim, not how many they can receive in total. + 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 +
  • -
    -
    -
    - )} -
    -
    - {!isDisabled && ( -
    - - {normalizeQuery.data.invalidFound ? ( - - ) : ( - - )} -
    - )} + + ) : ( + <> +
  • + 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 + per wallet you specified.{" "} + + 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 && ( +
    + + {normalizeQuery.data?.invalidFound ? ( + + ) : ( + + )} +
    + )} +
    +
    + ); +}; + +export function SnapshotViewerSheet( + props: SnapshotUploadProps & { + isOpen: boolean; + }, +) { + return ( + { + if (!open) { + props.onClose(); + } + }} + > + + + Snapshot + + ); -}; +} diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx index 9319ef48330..190359f61d6 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx @@ -1,4 +1,5 @@ import { UnorderedList } from "@/components/ui/List/List"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; import { ToolTipLabel } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { Link } from "@chakra-ui/react"; @@ -44,15 +45,8 @@ export const AirdropUpload: React.FC = ({ removeInvalid, } = useCsvUpload({ csvParser }); const paginationPortalRef = useRef(null); - const onSave = () => { - setAirdrop( - normalizeQuery.data.result.map((o) => ({ - address: o.resolvedAddress, - quantity: o.quantity, - })), - ); - onClose(); - }; + + const normalizeData = normalizeQuery.data; const columns = useMemo(() => { return [ @@ -91,10 +85,27 @@ export const AirdropUpload: React.FC = ({ ] as Column[]; }, []); + if (!normalizeData) { + return ( +
    + +
    + ); + } + + const onSave = () => { + setAirdrop( + normalizeData.result.map((o) => ({ + address: o.resolvedAddress, + quantity: o.quantity, + })), + ); + onClose(); + }; + return (
    - {normalizeQuery.pending &&
    Loading...
    } - {normalizeQuery.data?.result.length && rawData.length > 0 ? ( + {normalizeData.result.length && rawData.length > 0 ? ( <> portalRef={paginationPortalRef} diff --git a/apps/dashboard/src/hooks/useCsvUpload.ts b/apps/dashboard/src/hooks/useCsvUpload.ts index 14423a956eb..3cb2a9f5ac9 100644 --- a/apps/dashboard/src/hooks/useCsvUpload.ts +++ b/apps/dashboard/src/hooks/useCsvUpload.ts @@ -1,12 +1,12 @@ import { useThirdwebClient } from "@/constants/thirdweb.client"; -import { useQueries } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; +import pLimit from "p-limit"; import Papa from "papaparse"; import { useCallback, useState } from "react"; import { type DropzoneOptions, useDropzone } from "react-dropzone"; import { type ThirdwebClient, ZERO_ADDRESS, isAddress } from "thirdweb"; import { resolveAddress } from "thirdweb/extensions/ens"; import { csvMimeTypes } from "utils/batch"; - /** * Validate if the item.address is a valid ethereum address * Take in an { address: string, ...rest } object and return a new object, with 2 new props: `isValid` and `resolvedAddress` @@ -135,20 +135,21 @@ export function useCsvUpload< onDrop, }); - const normalizeQuery = useQueries({ - queries: rawData.map((item) => ({ - queryKey: ["snapshot-check-isAddress", item], - queryFn: () => checkIsAddress({ item: item, thirdwebClient }), - })), - combine: (results) => { + const normalizeQuery = useQuery({ + queryKey: ["snapshot-check-isAddress", rawData], + queryFn: async () => { + const limit = pLimit(50); + const results = await Promise.all( + rawData.map((item) => { + return limit(() => checkIsAddress({ item: item, thirdwebClient })); + }), + ); return { - data: { - result: processAirdropData(results.map((result) => result.data)), - invalidFound: !!results.find((o) => !o.data?.isValid), - }, - pending: results.some((result) => result.isPending), + result: processAirdropData(results), + invalidFound: !!results.find((item) => !item?.isValid), }; }, + retry: false, }); const removeInvalid = useCallback(() => { @@ -158,7 +159,7 @@ export function useCsvUpload< // double type assertion is save here because we don't really use this variable (only check for its length) // Also filteredData's type is the superset of T[] setRawData(filteredData as unknown as T[]); - }, [normalizeQuery.data.result]); + }, [normalizeQuery.data?.result]); return { normalizeQuery, getInputProps, diff --git a/apps/dashboard/src/utils/date-utils.ts b/apps/dashboard/src/utils/date-utils.ts index aa91c14026a..faba4cf11fc 100644 --- a/apps/dashboard/src/utils/date-utils.ts +++ b/apps/dashboard/src/utils/date-utils.ts @@ -5,7 +5,9 @@ const DATE_TIME_LOCAL_FORMAT = "yyyy-MM-dd HH:mm"; export function toDateTimeLocal(date?: Date | number | string) { let parsedDate: Date | undefined = undefined; - if (typeof date === "number") { + if (date instanceof Date) { + parsedDate = date; + } else if (typeof date === "number") { parsedDate = new Date(date * 1000); } else if (typeof date === "string") { parsedDate = new Date(date); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb71873f356..750350edfb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,6 +217,9 @@ importers: openapi-types: specifier: ^12.1.3 version: 12.1.3 + p-limit: + specifier: ^6.2.0 + version: 6.2.0 papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -11236,6 +11239,10 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-limit@6.2.0: + resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} + engines: {node: '>=18'} + p-locate@3.0.0: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} @@ -14233,6 +14240,10 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.2: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} @@ -28650,6 +28661,10 @@ snapshots: dependencies: yocto-queue: 1.0.0 + p-limit@6.2.0: + dependencies: + yocto-queue: 1.1.1 + p-locate@3.0.0: dependencies: p-limit: 2.3.0 @@ -32209,6 +32224,8 @@ snapshots: yocto-queue@1.0.0: {} + yocto-queue@1.1.1: {} + yoctocolors-cjs@2.1.2: {} yoga-wasm-web@0.3.3: {}