diff --git a/components/forms/FlexMemberAttributes.hooks.ts b/components/forms/FlexMemberAttributes.hooks.ts new file mode 100644 index 0000000..3cc1dc8 --- /dev/null +++ b/components/forms/FlexMemberAttributes.hooks.ts @@ -0,0 +1,32 @@ +import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload' +import { useMemo, useState } from 'react' +import { uid } from 'utils/random' + +export function useFlexMemberAttributesState() { + const [record, setRecord] = useState>(() => ({})) + + const entries = useMemo(() => Object.entries(record), [record]) + const values = useMemo(() => Object.values(record), [record]) + + function add(attribute: WhitelistFlexMember = { address: '', mint_count: 0 }) { + setRecord((prev) => ({ ...prev, [uid()]: attribute })) + } + + function update(key: string, attribute = record[key]) { + setRecord((prev) => ({ ...prev, [key]: attribute })) + } + + function remove(key: string) { + return setRecord((prev) => { + const latest = { ...prev } + delete latest[key] + return latest + }) + } + + function reset() { + setRecord({}) + } + + return { entries, values, add, update, remove, reset } +} diff --git a/components/forms/FlexMemberAttributes.tsx b/components/forms/FlexMemberAttributes.tsx new file mode 100644 index 0000000..4934145 --- /dev/null +++ b/components/forms/FlexMemberAttributes.tsx @@ -0,0 +1,133 @@ +import { toUtf8 } from '@cosmjs/encoding' +import { FormControl } from 'components/FormControl' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload' +import { useWallet } from 'contexts/wallet' +import { useEffect, useId, useMemo } from 'react' +import toast from 'react-hot-toast' +import { FaMinus, FaPlus } from 'react-icons/fa' +import { SG721_NAME_ADDRESS } from 'utils/constants' +import { isValidAddress } from 'utils/isValidAddress' + +import { useInputState, useNumberInputState } from './FormInput.hooks' + +export interface FlexMemberAttributesProps { + title: string + subtitle?: string + isRequired?: boolean + attributes: [string, WhitelistFlexMember][] + onAdd: () => void + onChange: (key: string, attribute: WhitelistFlexMember) => void + onRemove: (key: string) => void +} + +export function FlexMemberAttributes(props: FlexMemberAttributesProps) { + const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props + + return ( + + {attributes.map(([id], i) => ( + + ))} + + ) +} + +export interface MemberAttributeProps { + id: string + isLast: boolean + onAdd: FlexMemberAttributesProps['onAdd'] + onChange: FlexMemberAttributesProps['onChange'] + onRemove: FlexMemberAttributesProps['onRemove'] + defaultAttribute: WhitelistFlexMember +} + +export function FlexMemberAttribute({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: MemberAttributeProps) { + const wallet = useWallet() + const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast]) + + const htmlId = useId() + + const addressState = useInputState({ + id: `ma-address-${htmlId}`, + name: `ma-address-${htmlId}`, + title: `Address`, + defaultValue: defaultAttribute.address, + }) + + const mintCountState = useNumberInputState({ + id: `mint-count-${htmlId}`, + name: `mint-count-${htmlId}`, + title: `Mint Count`, + defaultValue: defaultAttribute.mint_count, + }) + + useEffect(() => { + onChange(id, { address: addressState.value, mint_count: mintCountState.value }) + }, [addressState.value, mintCountState.value, id]) + + const resolveAddress = async (name: string) => { + if (!wallet.client) throw new Error('Wallet not connected') + await wallet.client + .queryContractRaw( + SG721_NAME_ADDRESS, + toUtf8( + Buffer.from( + `0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`, + 'hex', + ).toString(), + ), + ) + .then((res) => { + const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri + if (tokenUri && isValidAddress(tokenUri)) onChange(id, { address: tokenUri, mint_count: mintCountState.value }) + else { + toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`) + onChange(id, { address: '', mint_count: mintCountState.value }) + } + }) + .catch((err) => { + toast.error(`Error resolving address for the name: ${name}.stars`) + console.error(err) + onChange(id, { address: '', mint_count: mintCountState.value }) + }) + } + useEffect(() => { + if (addressState.value.endsWith('.stars')) { + void resolveAddress(addressState.value.split('.')[0]) + } else { + onChange(id, { + address: addressState.value, + mint_count: mintCountState.value, + }) + } + }, [addressState.value, id]) + + return ( +
+ + + +
+ +
+
+ ) +}