diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index af8254c1482..88e7161bd96 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -43,7 +43,6 @@ "next-themes": "^0.4.6", "nextjs-toploader": "^1.6.12", "nuqs": "^2.4.3", - "p-limit": "^6.2.0", "papaparse": "^5.5.3", "pluralize": "^8.0.0", "posthog-js": "1.256.1", diff --git a/apps/dashboard/src/@/hooks/useCsvUpload.ts b/apps/dashboard/src/@/hooks/useCsvUpload.ts index b3028734327..66abe914310 100644 --- a/apps/dashboard/src/@/hooks/useCsvUpload.ts +++ b/apps/dashboard/src/@/hooks/useCsvUpload.ts @@ -1,5 +1,4 @@ -import { useQuery } from "@tanstack/react-query"; -import pLimit from "p-limit"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import Papa from "papaparse"; import { useCallback, useState } from "react"; import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb"; @@ -95,6 +94,7 @@ export function useCsvUpload< // Always gonna need the wallet address T extends { address: string }, >(props: Props) { + const queryClient = useQueryClient(); const [rawData, setRawData] = useState< T[] | Array >(props.defaultRawData || []); @@ -132,16 +132,29 @@ export function useCsvUpload< [props.csvParser], ); + const [normalizeProgress, setNormalizeProgress] = useState({ + total: 0, + current: 0, + }); + const normalizeQuery = useQuery({ queryFn: async () => { - const limit = pLimit(50); - const results = await Promise.all( - rawData.map((item) => { - return limit(() => + const batchSize = 50; + const results = []; + for (let i = 0; i < rawData.length; i += batchSize) { + const batch = rawData.slice(i, i + batchSize); + setNormalizeProgress({ + total: rawData.length, + current: i, + }); + const batchResults = await Promise.all( + batch.map((item) => checkIsAddress({ item: item, thirdwebClient: props.client }), - ); - }), - ); + ), + ); + results.push(...batchResults); + } + return { invalidFound: !!results.find((item) => !item?.isValid), result: processAirdropData(results), @@ -153,12 +166,18 @@ export function useCsvUpload< const removeInvalid = useCallback(() => { const filteredData = normalizeQuery.data?.result.filter( - ({ isValid }) => isValid, + (d) => d.isValid && d.resolvedAddress !== ZERO_ADDRESS, ); - // 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]); + + if (filteredData && normalizeQuery.data) { + // Directly update the query result instead of setting new state to avoid triggering refetch + queryClient.setQueryData(["snapshot-check-isAddress", rawData], { + ...normalizeQuery.data, + result: filteredData, + invalidFound: false, // Since we removed all invalid items + }); + } + }, [normalizeQuery.data, queryClient, rawData]); const processData = useCallback( (data: T[]) => { @@ -181,5 +200,6 @@ export function useCsvUpload< removeInvalid, reset, setFiles, + normalizeProgress, }; } 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 09685ce1210..7b8f9293454 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 @@ -27,13 +27,15 @@ export const ClaimerSelection = () => { setOpenSnapshotIndex: setOpenIndex, isAdmin, claimConditionType, + phaseSnapshots, + setPhaseSnapshot, } = useClaimConditionsFormContext(); const handleClaimerChange = (value: string) => { const val = value as "any" | "specific" | "overrides"; if (val === "any") { - form.setValue(`phases.${phaseIndex}.snapshot`, undefined); + setPhaseSnapshot(phaseIndex, undefined); } else { if (val === "specific") { form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 0); @@ -41,7 +43,7 @@ export const ClaimerSelection = () => { if (val === "overrides" && field.maxClaimablePerWallet !== 1) { form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 1); } - form.setValue(`phases.${phaseIndex}.snapshot`, []); + setPhaseSnapshot(phaseIndex, []); setOpenIndex(phaseIndex); } }; @@ -49,6 +51,7 @@ export const ClaimerSelection = () => { let helperText: React.ReactNode; const disabledSnapshotButton = isAdmin && formDisabled; + const snapshot = phaseSnapshots[phaseIndex]; if (dropType === "specific") { helperText = ( @@ -87,10 +90,7 @@ export const ClaimerSelection = () => { return ( { )} {/* Edit or See Snapshot */} - {field.snapshot ? ( + {snapshot ? (
{/* disable the "Edit" button when form is disabled, but not when it's a "See" button */} - - {csvUpload.normalizeQuery.data?.invalidFound ? ( + ({ + address: x.resolvedAddress, + ensName: x.address.startsWith("0x") ? undefined : x.address, + maxClaimable: x.maxClaimable, + currencyAddress: x.currencyAddress, + price: x.price, + }))} + /> + +
+ {csvUpload.normalizeQuery.data.invalidFound && ( +

+ Invalid addresses found, please remove from the list to continue +

+ )} + +
- ) : ( - - )} + + {csvUpload.normalizeQuery.data?.invalidFound ? ( + + ) : ( + + )} +
) : ( @@ -308,6 +355,23 @@ const SnapshotViewerSheetContent: React.FC = ({ ); }; +function ViewSnapshot(props: { + data: (SnapshotEntry & { ensName?: string })[]; + onReset: () => void; +}) { + return ( +
+ +
+ +
+
+ ); +} + export function SnapshotViewerSheet( props: SnapshotUploadProps & { isOpen: boolean; @@ -322,7 +386,7 @@ export function SnapshotViewerSheet( }} open={props.isOpen} > - + Snapshot @@ -332,6 +396,33 @@ export function SnapshotViewerSheet( ); } +function SnapshotViewerSheetContent(props: SnapshotUploadProps) { + const [showEditMode, setShowEditMode] = useState(false); + + if (showEditMode) { + return ( + + ); + } + + if (props.value && props.value.length > 0) { + return ( + setShowEditMode(true)} + /> + ); + } + + return ; +} + const snapshotWithMaxClaimable = `\ address,maxClaimable 0x0000000000000000000000000000000000000000,2 diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx index ab16732526d..85f6f96a3d0 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx @@ -21,12 +21,29 @@ export function AirdropUpload(props: { client: props.client, csvParser, }); + + if (csvUpload.normalizeQuery.isPending) { + return ( +
+ +

Resolving ENS

+

+ {csvUpload.normalizeProgress.current} /{" "} + {csvUpload.normalizeProgress.total} +

+
+ ); + } + const normalizeData = csvUpload.normalizeQuery.data; if (!normalizeData) { return ( -
- +
+

Failed to resolve ENS

+

+ {String(csvUpload.normalizeQuery.error)} +

); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx index e715c313f9d..b26b36cc962 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx @@ -262,8 +262,13 @@ const AirdropUpload: React.FC = ({ if (csvUpload.normalizeQuery.isPending) { return ( -
+
+

Resolving ENS

+

+ {csvUpload.normalizeProgress.current} /{" "} + {csvUpload.normalizeProgress.total} +

); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f7d52a839a..e27e44f580f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,9 +190,6 @@ importers: nuqs: specifier: ^2.4.3 version: 2.4.3(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) - p-limit: - specifier: ^6.2.0 - version: 6.2.0 papaparse: specifier: ^5.5.3 version: 5.5.3 @@ -12371,10 +12368,6 @@ 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'} @@ -15717,10 +15710,10 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/client-sso-oidc': 3.592.0 '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -15763,10 +15756,10 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/client-sso-oidc': 3.592.0 '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -15809,10 +15802,10 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/client-sso-oidc': 3.592.0 '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -15856,7 +15849,7 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0)': + '@aws-sdk/client-sso-oidc@3.592.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 @@ -15899,7 +15892,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: - - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso-oidc@3.840.0': @@ -16036,7 +16028,7 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/client-sso-oidc': 3.592.0 '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16155,24 +16147,6 @@ snapshots: '@smithy/util-stream': 4.2.3 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0)': - dependencies: - '@aws-sdk/client-sts': 3.592.0 - '@aws-sdk/credential-provider-env': 3.587.0 - '@aws-sdk/credential-provider-http': 3.587.0 - '@aws-sdk/credential-provider-process': 3.587.0 - '@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0) - '@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.592.0) - '@aws-sdk/types': 3.577.0 - '@smithy/credential-provider-imds': 3.2.8 - '@smithy/property-provider': 3.1.11 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - aws-crt - '@aws-sdk/credential-provider-ini@3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0)': dependencies: '@aws-sdk/client-sts': 3.592.0 @@ -16227,25 +16201,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0)': - dependencies: - '@aws-sdk/credential-provider-env': 3.587.0 - '@aws-sdk/credential-provider-http': 3.587.0 - '@aws-sdk/credential-provider-ini': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) - '@aws-sdk/credential-provider-process': 3.587.0 - '@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0) - '@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.592.0) - '@aws-sdk/types': 3.577.0 - '@smithy/credential-provider-imds': 3.2.8 - '@smithy/property-provider': 3.1.11 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - '@aws-sdk/client-sts' - - aws-crt - '@aws-sdk/credential-provider-node@3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0)': dependencies: '@aws-sdk/credential-provider-env': 3.587.0 @@ -16519,7 +16474,7 @@ snapshots: '@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.592.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/client-sso-oidc': 3.592.0 '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.11 '@smithy/shared-ini-file-loader': 3.1.12 @@ -32476,10 +32431,6 @@ snapshots: dependencies: yocto-queue: 1.1.1 - p-limit@6.2.0: - dependencies: - yocto-queue: 1.1.1 - p-locate@3.0.0: dependencies: p-limit: 2.3.0