From 427475591cf73db01753e688d13a7cb7e86ee7c3 Mon Sep 17 00:00:00 2001 From: Sem <931684+sembrestels@users.noreply.github.com> Date: Wed, 25 Oct 2023 00:40:04 +0200 Subject: [PATCH] Allow import from CSV --- .../src/components/ImportCSVButton.tsx | 25 ++++++++++ .../components/dao-steps/ConfigureToken.tsx | 35 ++++++++++++- apps/abclaunch/src/utils/csv-utils.ts | 49 +++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 apps/abclaunch/src/components/ImportCSVButton.tsx create mode 100644 apps/abclaunch/src/utils/csv-utils.ts diff --git a/apps/abclaunch/src/components/ImportCSVButton.tsx b/apps/abclaunch/src/components/ImportCSVButton.tsx new file mode 100644 index 0000000..a6de9d8 --- /dev/null +++ b/apps/abclaunch/src/components/ImportCSVButton.tsx @@ -0,0 +1,25 @@ +import { Button, Input } from "@chakra-ui/react" +import { useRef } from "react" +import { readFile, removeCSVHeaders } from "../utils/csv-utils" + +export default function ImportButton ({ onImport, children, ...props }: { onImport: (csv: string) => void, children: React.ReactNode, [x: string]: any }) { + const fileInput = useRef(null) + async function handleChange (file: File) { + const csv = removeCSVHeaders(await readFile(file)) + onImport(csv) + } + const handleClick = () => fileInput.current && fileInput.current.click() + return ( + <> + + ((e.target as HTMLInputElement).value = '')} + onChange={e => e.target.files && handleChange(e.target.files[0])} + /> + + ) + } \ No newline at end of file diff --git a/apps/abclaunch/src/components/dao-steps/ConfigureToken.tsx b/apps/abclaunch/src/components/dao-steps/ConfigureToken.tsx index b491484..ea98163 100644 --- a/apps/abclaunch/src/components/dao-steps/ConfigureToken.tsx +++ b/apps/abclaunch/src/components/dao-steps/ConfigureToken.tsx @@ -1,9 +1,11 @@ import { Divider, Button, FormControl, FormLabel, HStack, InputGroup, InputRightElement, Text, VStack, Tooltip } from "@chakra-ui/react"; -import { InfoOutlineIcon, DeleteIcon, AddIcon } from '@chakra-ui/icons'; +import { InfoOutlineIcon, DeleteIcon, AttachmentIcon, AddIcon } from '@chakra-ui/icons'; import React from 'react'; import { useRecoilState, useRecoilValue } from "recoil"; import { newDaoTokenState, newDaoTokenSupplyState } from "../../recoil"; -import { isAddress } from "viem"; +import ImportCSVButton from "../ImportCSVButton"; +import { csvStringToArray } from "../../utils/csv-utils"; +import { formatUnits, isAddress, parseUnits } from "viem"; import { Input, NumberInput, NumberInputField } from "commons-ui/src/components/Input"; export default function ConfigureToken() { @@ -45,6 +47,28 @@ export default function ConfigureToken() { }); } + function handlePaste(e: React.ClipboardEvent) { + const tokenHolders = tokenSettings.tokenHolders; + const isEmpty = tokenHolders.length === 1 && tokenHolders[0][0] === '' && tokenHolders[0][1] === '' + if (isEmpty) { // Only paste a CSV on a blank state, otherwise paste normally + e.preventDefault(); + handleImportCSV(e.clipboardData.getData('Text')); + } + } + + function handleImportCSV(csv: string) { + if (!csv) return; + const array = csvStringToArray(csv); + const processAddress = (address: string) => address.trim() || ''; + const processBalance = (balance: string) => !isNaN(Number(balance)) && formatUnits(parseUnits(balance, 18), 18) || ''; + const tokenHolders: [string, string][] = array.map(([address, balance]) => [processAddress(address), processBalance(balance)]); + setTokenSettings(settings => ({ ...settings, tokenHolders })); + } + + function deleteAll() { + setTokenSettings(settings => ({ ...settings, tokenHolders: [['', '']] })); + } + return ( Token @@ -117,6 +141,7 @@ export default function ConfigureToken() { onChange={(e: React.ChangeEvent) => handleHolderChange(i, e.target.value, true) } + onPaste={handlePaste} /> handleRemoveHolder(i)} > @@ -145,6 +170,12 @@ export default function ConfigureToken() { + } onImport={handleImportCSV}> + Import CSV + + { + return new Promise((resolve, reject) => { + const fr = new FileReader() + fr.onload = () => { + resolve(fr.result as string) // Ensure the result is treated as a string + } + fr.onerror = (error) => { + reject(error) // Handle the error event + } + fr.readAsText(file) + }) +} + +export const removeCSVHeaders = (text: string): string => text.substring(text.indexOf('\n') + 1)