From 10849ee97e488bb1cb54a5337fd119dcf4b5f037 Mon Sep 17 00:00:00 2001 From: Stanislaw Date: Mon, 11 Mar 2024 22:23:57 +0100 Subject: [PATCH 1/2] Feat: Multiple tokens form Signed-off-by: Stanislaw --- package.json | 1 + pnpm-lock.yaml | 122 +++++++++++++++++++++++++++++++ src/App.tsx | 73 ++++++++++++++----- src/components/HoldersForm.tsx | 128 +++++++++++++++++++++------------ src/components/ui/progress.tsx | 26 +++++++ src/components/ui/switch.tsx | 27 +++++++ src/dictionary/en.json | 5 +- src/types/balances-return.ts | 1 + src/utils/formSchema.ts | 30 ++++---- 9 files changed, 337 insertions(+), 76 deletions(-) create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/switch.tsx diff --git a/package.json b/package.json index d90d5d6..7ee8f04 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@hookform/resolvers": "^3.3.4", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@tanstack/react-query": "5.24.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6772096..3eb28f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.60)(react@18.2.0) + '@radix-ui/react-switch': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.60)(react-dom@18.2.0)(react@18.2.0) '@tanstack/react-query': specifier: 5.24.1 version: 5.24.1(react@18.2.0) @@ -727,6 +730,12 @@ packages: requiresBuild: true optional: true + /@radix-ui/primitive@1.0.1: + resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} + dependencies: + '@babel/runtime': 7.23.9 + dev: false + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.60)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -741,6 +750,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-context@1.0.1(@types/react@18.2.60)(react@18.2.0): + resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@types/react': 18.2.60 + react: 18.2.0 + dev: false + /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.19)(@types/react@18.2.60)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} peerDependencies: @@ -798,6 +821,105 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.60)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.60)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.60)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.60)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.60)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.60)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.60)(react@18.2.0) + '@types/react': 18.2.60 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.60)(react@18.2.0): + resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@types/react': 18.2.60 + react: 18.2.0 + dev: false + + /@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.60)(react@18.2.0): + resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.60)(react@18.2.0) + '@types/react': 18.2.60 + react: 18.2.0 + dev: false + + /@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.60)(react@18.2.0): + resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@types/react': 18.2.60 + react: 18.2.0 + dev: false + + /@radix-ui/react-use-previous@1.0.1(@types/react@18.2.60)(react@18.2.0): + resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@types/react': 18.2.60 + react: 18.2.0 + dev: false + + /@radix-ui/react-use-size@1.0.1(@types/react@18.2.60)(react@18.2.0): + resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.60)(react@18.2.0) + '@types/react': 18.2.60 + react: 18.2.0 + dev: false + /@rollup/rollup-android-arm-eabi@4.12.0: resolution: {integrity: sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==} cpu: [arm] diff --git a/src/App.tsx b/src/App.tsx index 36caba2..c125850 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,13 +8,15 @@ import { Skeleton } from '@/components/ui/skeleton'; import { Textarea } from '@/components/ui/textarea'; import { toast } from 'sonner'; import dictionary from '@/dictionary/en.json'; -import { HoldersForm } from '@/components/HoldersForm'; +import { FormData, HoldersForm } from '@/components/HoldersForm'; +import { Switch } from '@/components/ui/switch'; const App = () => { - const [tokenId, setTokenId] = useState(''); - const [minAmount, setMinAmount] = useState(null); + const [formData, setFormData] = useState([]); const [data, setData] = useState([]); - const [shouldFetch, setShouldFetch] = useState(false); + const [responses, setResponses] = useState([]); + const [shouldFetch, setShouldFetch] = useState(false); + const [isAllConditionsRequired, setIsAllConditionsRequired] = useState(true); const copyToClipboard = async (textToCopy: string) => { if (navigator.clipboard && window.isSecureContext) { @@ -41,22 +43,50 @@ const App = () => { } }; - const fetchData = async (url: string) => { - try { - const response = await fetch(url); + const filterData = (responses: Balance[][], isAllConditionsRequired: boolean): Balance[] => { + let data = responses.flatMap((response) => response); - if (!response.ok) { - throw new Error(`${dictionary.httpError} ${response.status}`); - } + if (isAllConditionsRequired) { + return data.filter( + (balance, index, self) => + self.findIndex((b) => b.account === balance.account) === index && + responses.every((response) => response.some((b) => b.account === balance.account)), + ); + } else { + return data.filter((balance, index, self) => self.findIndex((b) => b.account === balance.account) === index); + } + }; + + const fetchData = async (url: string): Promise => { + const response = await fetch(url); - const data = await response.json(); + if (!response.ok) { + throw new Error(`${dictionary.httpError} ${response.status}`); + } - setData((prevData: Balance[]) => [...prevData, ...data?.balances]); + const data = await response.json(); - if (data.links.next) { - await fetchData(`${nodeUrl}${data.links.next}`); - } + let nextData: Balance[] = []; + if (data.links.next) { + nextData = await fetchData(`${nodeUrl}${data.links.next}`); + } + + return [...data.balances, ...nextData]; + }; + const fetchAllData = async () => { + try { + const responses = await Promise.all( + formData.map(async (item) => { + const tokenId = item.tokenId; + const minAmount = item.minAmount; + const url = `${nodeUrl}/api/v1/tokens/${tokenId}/balances?account.balance=gte:${minAmount}&limit=100`; + return fetchData(url); + }), + ); + + setResponses(responses); + setData(filterData(responses, isAllConditionsRequired)); return data; } catch (error) { throw error; @@ -68,7 +98,7 @@ const App = () => { retry: 0, throwOnError: false, queryKey: ['queryList'], - queryFn: () => fetchData(`${nodeUrl}/api/v1/tokens/${tokenId}/balances?account.balance=gte:${minAmount}&limit=100`), + queryFn: () => fetchAllData(), }); useEffect(() => { @@ -85,13 +115,22 @@ const App = () => { if (!isFetching && isFetched) setShouldFetch(false); }, [isFetched, isFetching]); + useEffect(() => { + setData(filterData(responses, isAllConditionsRequired)); + }, [isAllConditionsRequired]); + return (

{dictionary.title}

{dictionary.description}

+
+ + +
+
- +
{isFetched || isFetching ? ( diff --git a/src/components/HoldersForm.tsx b/src/components/HoldersForm.tsx index 6f18125..0d03912 100644 --- a/src/components/HoldersForm.tsx +++ b/src/components/HoldersForm.tsx @@ -4,71 +4,109 @@ import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Loader2 } from 'lucide-react'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; -import { z } from 'zod'; import { formSchema } from '@/utils/formSchema'; -import { useForm } from 'react-hook-form'; +import { useFieldArray, useForm, UseFormProps } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; type HoldersFormProps = { - setTokenId: (tokenId: string) => void; - setMinAmount: (minAmount: number) => void; + setFormData: (formData: FormData['formData']) => void; setData: (data: any) => void; setShouldFetch: (shouldFetch: boolean) => void; isFetching: boolean; }; -export const HoldersForm = ({ setTokenId, setMinAmount, setData, setShouldFetch, isFetching }: HoldersFormProps) => { - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - tokenId: '', - minAmount: '', +export type FormData = { + formData: { tokenId: string; minAmount: string }[]; +}; + +export const HoldersForm = ({ setFormData, setData, setShouldFetch, isFetching }: HoldersFormProps) => { + const useZodForm = ( + props: Omit, 'resolver'> & { + schema: TSchema; }, + ) => { + return useForm({ + ...props, + resolver: zodResolver(props.schema, undefined, { + raw: true, + }), + }); + }; + + const methods = useZodForm({ + schema: formSchema, + defaultValues: { formData: [{ tokenId: '', minAmount: '0' }] }, }); - const onSubmit = ({ tokenId, minAmount }: z.infer) => { - setTokenId(tokenId); - setMinAmount(Number(minAmount)); + const { control, handleSubmit } = methods; + + const { fields, append, remove } = useFieldArray({ + name: 'formData', + control, + }); + + const onSubmit = (data: FormData) => { + setFormData(data.formData); setData([]); setShouldFetch(true); }; return ( -
- -
-
- ( - - {dictionary.tokenId} - - - - - - )} - /> -
+ + + {fields.map((field, index) => ( +
+
+ ( + + {dictionary.tokenId} + + + + + + )} + /> +
-
- ( - - {dictionary.minAmount} - - - - - - )} - /> +
+ ( + + {dictionary.minAmount} + + + + + + )} + /> +
+
+ ))} +
+
+