From 2114e5f91fa3c4a0f518699fcef03db1fcf706c4 Mon Sep 17 00:00:00 2001 From: pinkhoodie <36429880+pinkhoodie@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:57:20 +0000 Subject: [PATCH] Add text input option to airdrop setup (#7238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add manual text input alternative to CSV upload for airdrop addresses - Support multiple input formats: space, comma, equals, and tab separated - Parse text input and convert to CSV format for existing validation system - Reuse all existing functionality: ENS resolution, address validation, duplicate removal - Maintain same validation flow and error handling as CSV upload - Change button text from 'Upload CSV' to 'Set up Airdrop' for clarity ![image](https://github.com/user-attachments/assets/e43afdd8-eed8-491a-ac93-9f02eb6ee703) ![image](https://github.com/user-attachments/assets/42522fa1-3886-4d68-a113-079d58b3da96) --- ## PR-Codex overview This PR enhances the CSV upload functionality in the `TokenAirdropSection` component. It introduces a new method for processing CSV data, allows manual entry of addresses and amounts, and updates UI labels to reflect the new functionality. ### Detailed summary - Added `processData` function in `useCsvUpload` for processing parsed CSV data. - Updated `TokenAirdropSection` UI labels from "CSV File Uploaded" to "Airdrop List Set". - Changed button text from "View CSV" to "View List". - Introduced `parseTextInput` function for manual input parsing. - Added text input for entering addresses and amounts. - Implemented handling for text input submission. - Updated the layout to separate CSV upload and manual entry sections. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit - **New Features** - Added the ability to manually enter airdrop addresses and amounts using a text area, supporting multiple input formats. - Users can now choose between uploading a CSV file or entering addresses and amounts directly. - **UI Updates** - Updated labels and button texts to reflect support for both CSV and manual list input. - Improved layout with a visual divider and clear instructions for manual entry. - Enhanced reset and validation behaviors for both input methods. --- .../create/distribution/token-airdrop.tsx | 258 ++++++++++++------ apps/dashboard/src/hooks/useCsvUpload.ts | 15 + 2 files changed, 196 insertions(+), 77 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/distribution/token-airdrop.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/distribution/token-airdrop.tsx index 61b3a0b2239..0c485759c3b 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/distribution/token-airdrop.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/distribution/token-airdrop.tsx @@ -23,6 +23,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { useCsvUpload } from "hooks/useCsvUpload"; import { @@ -92,7 +93,7 @@ export function TokenAirdropSection(props: {
{/* left */}
-

CSV File Uploaded

+

Airdrop List Set

{airdropAddresses.length} @@ -109,14 +110,14 @@ export function TokenAirdropSection(props: { - Airdrop CSV + Airdrop List - Airdrop CSV File + Set up Airdrop - Upload a CSV file to airdrop tokens to a list of - addresses + Upload a CSV file or enter comma-separated addresses and + amounts to airdrop tokens - Upload CSV + Set up Airdrop

)} @@ -193,14 +194,43 @@ type AirdropUploadProps = { client: ThirdwebClient; }; -// CSV parser for airdrop data -const csvParser = (items: AirdropAddressInput[]): AirdropAddressInput[] => { - return items - .map(({ address, quantity }) => ({ - address: (address || "").trim(), - quantity: (quantity || "1").trim(), - })) - .filter(({ address }) => address !== ""); +// Parse text input and convert to CSV-like format +const parseTextInput = (text: string): AirdropAddressInput[] => { + const lines = text + .split("\n") + .map((line) => line.trim()) + .filter((line) => line !== ""); + const result: AirdropAddressInput[] = []; + + for (const line of lines) { + let parts: string[] = []; + + if (line.includes("=")) { + parts = line.split("="); + } else if (line.includes(",")) { + parts = line.split(","); + } else if (line.includes("\t")) { + parts = line.split("\t"); + } else { + parts = line.split(/\s+/); + } + + parts = parts.map((part) => part.trim()).filter((part) => part !== ""); + + if (parts.length >= 1) { + const address = parts[0]; + const quantity = parts[1] || "1"; + + if (address) { + result.push({ + address: address.trim(), + quantity: quantity.trim(), + }); + } + } + } + + return result; }; const AirdropUpload: React.FC = ({ @@ -208,6 +238,8 @@ const AirdropUpload: React.FC = ({ onClose, client, }) => { + const [textInput, setTextInput] = useState(""); + const { normalizeQuery, getInputProps, @@ -216,11 +248,30 @@ const AirdropUpload: React.FC = ({ noCsv, reset, removeInvalid, - } = useCsvUpload({ csvParser, client }); + processData, + } = useCsvUpload({ + csvParser: (items: AirdropAddressInput[]) => { + return items + .map(({ address, quantity }) => ({ + address: (address || "").trim(), + quantity: (quantity || "1").trim(), + })) + .filter(({ address }) => address !== ""); + }, + client, + }); const normalizeData = normalizeQuery.data; - if (!normalizeData) { + // Handle text input - directly process the parsed data + const handleTextSubmit = () => { + if (!textInput.trim()) return; + + const parsedData = parseTextInput(textInput); + processData(parsedData); + }; + + if (!normalizeData && rawData.length > 0) { return (
@@ -229,6 +280,8 @@ const AirdropUpload: React.FC = ({ } const handleContinue = () => { + if (!normalizeData) return; + setAirdrop( normalizeData.result.map((o) => ({ address: o.resolvedAddress || o.address, @@ -239,9 +292,16 @@ const AirdropUpload: React.FC = ({ onClose(); }; + const handleReset = () => { + reset(); + setTextInput(""); + }; + return (
- {normalizeData.result.length && rawData.length > 0 ? ( + {normalizeData && + normalizeData.result.length > 0 && + rawData.length > 0 ? (
{normalizeQuery.data.invalidFound && (

@@ -253,19 +313,12 @@ const AirdropUpload: React.FC = ({ className="rounded-b-none" />

- {normalizeQuery.data.invalidFound ? ( ) : ( - )}
) : ( -
-
-
- -
- {!noCsv && ( -
-
- -
-

- Upload CSV File -

-

- Drag and drop your file or click here to upload -

-
+
+ {/* CSV Upload Section - First */} +
+ + +
+
+ +
+ {!noCsv && ( +
+
+ +
+

+ Upload CSV File +

+

+ Drag and drop your file or click here to upload +

+
+ )} + + {noCsv && ( +
+
+ +
+

+ Invalid CSV +

+

+ Your CSV does not contain the "address" & "quantity" + columns +

- {noCsv && ( -
-
- +
-

- Invalid CSV -

-

- Your CSV does not contain the "address" & "quantity" - columns -

- - -
- )} + )} +
+
+
+
+ + {/* Divider */} +
+
+ +
+
+ + Or enter manually + +
+
+ + {/* Text Input Section - Second */} +
+
+

+ Enter Addresses and Amounts +

+

+ Enter one address and amount on each line. Supports various + formats. (space, comma, or =) +

+
+