From 5b2a5153a5e27da182a8028303f60f38b4481325 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 7 Apr 2023 14:08:36 +0800 Subject: [PATCH 01/10] Add droposal form --- .../components/MediaPreview/MediaPreview.tsx | 0 .../SingleMediaUpload.css.ts | 23 + .../SingleMediaUpload/SingleMediaUpload.tsx | 169 +++++ apps/web/src/constants/addresses.ts | 5 + .../src/data/contract/abis/ZoraNFTCreator.ts | 600 ++++++++++++++++++ .../TransactionForm/Droposal/Droposal.css.ts | 14 + .../TransactionForm/Droposal/Droposal.tsx | 94 +++ .../Droposal/DroposalForm.schema.ts | 65 ++ .../TransactionForm/Droposal/DroposalForm.tsx | 306 +++++++++ .../TransactionForm/Droposal/index.ts | 1 + .../TransactionForm/TransactionForm.tsx | 3 + .../constants/transactionType.tsx | 7 + 12 files changed, 1287 insertions(+) create mode 100644 apps/web/src/components/MediaPreview/MediaPreview.tsx create mode 100644 apps/web/src/components/SingleMediaUpload/SingleMediaUpload.css.ts create mode 100644 apps/web/src/components/SingleMediaUpload/SingleMediaUpload.tsx create mode 100644 apps/web/src/data/contract/abis/ZoraNFTCreator.ts create mode 100644 apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/Droposal.css.ts create mode 100644 apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/Droposal.tsx create mode 100644 apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/DroposalForm.schema.ts create mode 100644 apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/DroposalForm.tsx create mode 100644 apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/index.ts diff --git a/apps/web/src/components/MediaPreview/MediaPreview.tsx b/apps/web/src/components/MediaPreview/MediaPreview.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/apps/web/src/components/SingleMediaUpload/SingleMediaUpload.css.ts b/apps/web/src/components/SingleMediaUpload/SingleMediaUpload.css.ts new file mode 100644 index 000000000..7e24f3958 --- /dev/null +++ b/apps/web/src/components/SingleMediaUpload/SingleMediaUpload.css.ts @@ -0,0 +1,23 @@ +import { style } from '@vanilla-extract/css' + +export const defaultUploadStyle = style({ + display: 'none', +}) + +export const uploadErrorBox = style({ + color: '#ff0015', + boxSizing: 'border-box', +}) + +export const singleMediaUploadWrapper = style({ + height: 64, + width: '100%', + borderRadius: 15, + background: '#F2F2F2', + overflow: 'hidden', + selectors: { + '&:hover': { + cursor: 'pointer', + }, + }, +}) diff --git a/apps/web/src/components/SingleMediaUpload/SingleMediaUpload.tsx b/apps/web/src/components/SingleMediaUpload/SingleMediaUpload.tsx new file mode 100644 index 000000000..c42df0162 --- /dev/null +++ b/apps/web/src/components/SingleMediaUpload/SingleMediaUpload.tsx @@ -0,0 +1,169 @@ +import { Box, Flex, Stack, Text } from '@zoralabs/zord' +import { FormikProps } from 'formik' +import { normalizeIPFSUrl, uploadFile } from 'ipfs-service' +import React, { ReactElement, useEffect, useState } from 'react' + +import { Spinner } from 'src/components/Spinner' +import { defaultInputLabelStyle } from 'src/modules/create-proposal/components/TransactionForm/CustomTransaction/CustomTransaction.css' + +import { + defaultUploadStyle, + singleMediaUploadWrapper, + uploadErrorBox, +} from './SingleMediaUpload.css' + +interface SingleImageUploadProps { + formik: FormikProps + id: string + inputLabel: string | ReactElement + value: string + onUploadStart?: (value: File) => void +} + +const SingleMediaUpload: React.FC = ({ + id, + formik, + inputLabel, + onUploadStart, + value, +}) => { + const acceptableMIME = [ + 'image/jpeg', + 'image/png', + 'image/svg+xml', + 'image/webp', + 'image/gif', + 'video/mp4', + 'video/quicktime', + 'audio/mpeg', + 'audio/wav', + ] + + const [isMounted, setIsMounted] = useState(false) + const [uploadMediaError, setUploadMediaError] = React.useState() + const [isUploading, setIsUploading] = React.useState(false) + const [fileName, setFileName] = React.useState() + const [progress, setProgress] = React.useState(0) + + useEffect(() => { + setIsMounted(true) + }, []) + + const truncate = (value: string) => { + return value.length > 40 ? `${value.substring(0, 40)}...` : value + } + + const handleFileUpload = React.useCallback( + async (_input: FileList | null) => { + if (!_input) return + const input = _input[0] + + setUploadMediaError(false) + + if (input?.type?.length && !acceptableMIME.includes(input.type)) { + setUploadMediaError({ + message: `Sorry, ${input.type} is an unsupported file type`, + }) + return + } + + try { + setIsUploading(true) + setFileName(input.name) + + if (onUploadStart) onUploadStart(input) + + const { cid } = await uploadFile(_input[0], { + cache: true, + onProgress: setProgress, + }) + + formik.setFieldValue(id, normalizeIPFSUrl(cid)) + setIsUploading(false) + setUploadMediaError(null) + } catch (err: any) { + setIsUploading(false) + setUploadMediaError({ + ...err, + message: `Sorry, there was an error with our file uploading service. ${err?.message}`, + }) + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ) + + return ( + + + + + {!isUploading && isMounted && !value && ( + + + None selected + + + Select + + + )} + + {isUploading && fileName && ( + + + {truncate(fileName)} + + + + {`${Math.round(progress)}% uploaded`} + + + + + )} + + {!isUploading && value && ( + + + {truncate(value)} + + + Replace + + + )} + + { + handleFileUpload(event.currentTarget.files) + }} + /> + + + {uploadMediaError && ( + + +
  • {uploadMediaError.message}
  • +
    +
    + )} +
    +
    + ) +} + +export default SingleMediaUpload diff --git a/apps/web/src/constants/addresses.ts b/apps/web/src/constants/addresses.ts index 5a867abb7..03f98168f 100644 --- a/apps/web/src/constants/addresses.ts +++ b/apps/web/src/constants/addresses.ts @@ -17,4 +17,9 @@ export const PUBLIC_NOUNS_ADDRESS = { 5: '0x0BC3807Ec262cB779b38D65b38158acC3bfedE10', }[process.env.NEXT_PUBLIC_CHAIN_ID || 1] as AddressType +export const PUBLIC_ZORA_NFT_CREATOR = { + 1: '0xF74B146ce44CC162b601deC3BE331784DB111DC1', + 5: '0xb9583D05Ba9ba8f7F14CCEe3Da10D2bc0A72f519', +}[process.env.NEXT_PUBLIC_CHAIN_ID || 1] as AddressType + export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' diff --git a/apps/web/src/data/contract/abis/ZoraNFTCreator.ts b/apps/web/src/data/contract/abis/ZoraNFTCreator.ts new file mode 100644 index 000000000..78972c7cd --- /dev/null +++ b/apps/web/src/data/contract/abis/ZoraNFTCreator.ts @@ -0,0 +1,600 @@ +export const zoraNFTCreatorAbi = [ + { + inputs: [ + { + internalType: 'address', + name: '_implementation', + type: 'address', + }, + { + internalType: 'contract EditionMetadataRenderer', + name: '_editionMetadataRenderer', + type: 'address', + }, + { + internalType: 'contract DropMetadataRenderer', + name: '_dropMetadataRenderer', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'previousAdmin', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'newAdmin', + type: 'address', + }, + ], + name: 'AdminChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'beacon', + type: 'address', + }, + ], + name: 'BeaconUpgraded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'creator', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'editionContractAddress', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'editionSize', + type: 'uint256', + }, + ], + name: 'CreatedDrop', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'previousOwner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'implementation', + type: 'address', + }, + ], + name: 'Upgraded', + type: 'event', + }, + { + inputs: [], + name: 'contractVersion', + outputs: [ + { + internalType: 'uint32', + name: '', + type: 'uint32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: 'name', + type: 'string', + }, + { + internalType: 'string', + name: 'symbol', + type: 'string', + }, + { + internalType: 'address', + name: 'defaultAdmin', + type: 'address', + }, + { + internalType: 'uint64', + name: 'editionSize', + type: 'uint64', + }, + { + internalType: 'uint16', + name: 'royaltyBPS', + type: 'uint16', + }, + { + internalType: 'address payable', + name: 'fundsRecipient', + type: 'address', + }, + { + internalType: 'bytes[]', + name: 'setupCalls', + type: 'bytes[]', + }, + { + internalType: 'contract IMetadataRenderer', + name: 'metadataRenderer', + type: 'address', + }, + { + internalType: 'bytes', + name: 'metadataInitializer', + type: 'bytes', + }, + ], + name: 'createAndConfigureDrop', + outputs: [ + { + internalType: 'address payable', + name: 'newDropAddress', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: 'name', + type: 'string', + }, + { + internalType: 'string', + name: 'symbol', + type: 'string', + }, + { + internalType: 'address', + name: 'defaultAdmin', + type: 'address', + }, + { + internalType: 'uint64', + name: 'editionSize', + type: 'uint64', + }, + { + internalType: 'uint16', + name: 'royaltyBPS', + type: 'uint16', + }, + { + internalType: 'address payable', + name: 'fundsRecipient', + type: 'address', + }, + { + components: [ + { + internalType: 'uint104', + name: 'publicSalePrice', + type: 'uint104', + }, + { + internalType: 'uint32', + name: 'maxSalePurchasePerAddress', + type: 'uint32', + }, + { + internalType: 'uint64', + name: 'publicSaleStart', + type: 'uint64', + }, + { + internalType: 'uint64', + name: 'publicSaleEnd', + type: 'uint64', + }, + { + internalType: 'uint64', + name: 'presaleStart', + type: 'uint64', + }, + { + internalType: 'uint64', + name: 'presaleEnd', + type: 'uint64', + }, + { + internalType: 'bytes32', + name: 'presaleMerkleRoot', + type: 'bytes32', + }, + ], + internalType: 'struct IERC721Drop.SalesConfiguration', + name: 'saleConfig', + type: 'tuple', + }, + { + internalType: 'string', + name: 'metadataURIBase', + type: 'string', + }, + { + internalType: 'string', + name: 'metadataContractURI', + type: 'string', + }, + ], + name: 'createDrop', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: 'name', + type: 'string', + }, + { + internalType: 'string', + name: 'symbol', + type: 'string', + }, + { + internalType: 'uint64', + name: 'editionSize', + type: 'uint64', + }, + { + internalType: 'uint16', + name: 'royaltyBPS', + type: 'uint16', + }, + { + internalType: 'address payable', + name: 'fundsRecipient', + type: 'address', + }, + { + internalType: 'address', + name: 'defaultAdmin', + type: 'address', + }, + { + components: [ + { + internalType: 'uint104', + name: 'publicSalePrice', + type: 'uint104', + }, + { + internalType: 'uint32', + name: 'maxSalePurchasePerAddress', + type: 'uint32', + }, + { + internalType: 'uint64', + name: 'publicSaleStart', + type: 'uint64', + }, + { + internalType: 'uint64', + name: 'publicSaleEnd', + type: 'uint64', + }, + { + internalType: 'uint64', + name: 'presaleStart', + type: 'uint64', + }, + { + internalType: 'uint64', + name: 'presaleEnd', + type: 'uint64', + }, + { + internalType: 'bytes32', + name: 'presaleMerkleRoot', + type: 'bytes32', + }, + ], + internalType: 'struct IERC721Drop.SalesConfiguration', + name: 'saleConfig', + type: 'tuple', + }, + { + internalType: 'string', + name: 'description', + type: 'string', + }, + { + internalType: 'string', + name: 'animationURI', + type: 'string', + }, + { + internalType: 'string', + name: 'imageURI', + type: 'string', + }, + ], + name: 'createEdition', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'dropMetadataRenderer', + outputs: [ + { + internalType: 'contract DropMetadataRenderer', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'editionMetadataRenderer', + outputs: [ + { + internalType: 'contract EditionMetadataRenderer', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'implementation', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'initialize', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'proxiableUUID', + outputs: [ + { + internalType: 'bytes32', + name: '', + type: 'bytes32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: 'name', + type: 'string', + }, + { + internalType: 'string', + name: 'symbol', + type: 'string', + }, + { + internalType: 'address', + name: 'defaultAdmin', + type: 'address', + }, + { + internalType: 'uint64', + name: 'editionSize', + type: 'uint64', + }, + { + internalType: 'uint16', + name: 'royaltyBPS', + type: 'uint16', + }, + { + internalType: 'address payable', + name: 'fundsRecipient', + type: 'address', + }, + { + components: [ + { + internalType: 'uint104', + name: 'publicSalePrice', + type: 'uint104', + }, + { + internalType: 'uint32', + name: 'maxSalePurchasePerAddress', + type: 'uint32', + }, + { + internalType: 'uint64', + name: 'publicSaleStart', + type: 'uint64', + }, + { + internalType: 'uint64', + name: 'publicSaleEnd', + type: 'uint64', + }, + { + internalType: 'uint64', + name: 'presaleStart', + type: 'uint64', + }, + { + internalType: 'uint64', + name: 'presaleEnd', + type: 'uint64', + }, + { + internalType: 'bytes32', + name: 'presaleMerkleRoot', + type: 'bytes32', + }, + ], + internalType: 'struct IERC721Drop.SalesConfiguration', + name: 'saleConfig', + type: 'tuple', + }, + { + internalType: 'contract IMetadataRenderer', + name: 'metadataRenderer', + type: 'address', + }, + { + internalType: 'bytes', + name: 'metadataInitializer', + type: 'bytes', + }, + ], + name: 'setupDropsContract', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'newImplementation', + type: 'address', + }, + ], + name: 'upgradeTo', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'newImplementation', + type: 'address', + }, + { + internalType: 'bytes', + name: 'data', + type: 'bytes', + }, + ], + name: 'upgradeToAndCall', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, +] as const diff --git a/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/Droposal.css.ts b/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/Droposal.css.ts new file mode 100644 index 000000000..5da39bf15 --- /dev/null +++ b/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/Droposal.css.ts @@ -0,0 +1,14 @@ +import { style } from '@vanilla-extract/css' +import { atoms } from '@zoralabs/zord' + +export const defaultInputLabelStyle = style([ + atoms({ + display: 'inline-flex', + fontSize: 16, + mb: 'x4', + }), + { + whiteSpace: 'nowrap', + fontWeight: '700', + }, +]) diff --git a/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/Droposal.tsx b/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/Droposal.tsx new file mode 100644 index 000000000..a4329c592 --- /dev/null +++ b/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/Droposal.tsx @@ -0,0 +1,94 @@ +import { Stack } from '@zoralabs/zord' +import { BigNumber, ethers } from 'ethers' +import { FormikHelpers } from 'formik' +import { useContract } from 'wagmi' + +import { PUBLIC_ZORA_NFT_CREATOR } from 'src/constants/addresses' +import { zoraNFTCreatorAbi } from 'src/data/contract/abis/ZoraNFTCreator' +import { TransactionType } from 'src/modules/create-proposal/constants' +import { useProposalStore } from 'src/modules/create-proposal/stores' +import { useDaoStore } from 'src/modules/dao' +import { AddressType } from 'src/typings' + +import { DroposalForm } from './DroposalForm' +import { DroposalFormValues } from './DroposalForm.schema' + +export const Droposal: React.FC = () => { + const addTransaction = useProposalStore((state) => state.addTransaction) + const { treasury } = useDaoStore((state) => state.addresses) + const zoraNFTCreatorContract = useContract({ + abi: zoraNFTCreatorAbi, + address: PUBLIC_ZORA_NFT_CREATOR, + }) + + const handleDroposalTransaction = ( + values: DroposalFormValues, + actions: FormikHelpers + ) => { + const { + name, + symbol, + maxSupply: editionSize, + royaltyPercentage, + fundsRecipient, + pricePerMint: publicSalePrice, + maxPerAddress: maxSalePurchasePerAddress, + publicSaleStart, + publicSaleEnd, + description, + } = values + + const royaltyBPS = royaltyPercentage * 100 + const defaultAdmin = treasury + const salesConfig = [ + ethers.utils.parseEther((publicSalePrice || 0).toString()), + maxSalePurchasePerAddress, + BigNumber.from(Math.floor(new Date(publicSaleStart).getTime() / 1000)), + BigNumber.from(Math.floor(new Date(publicSaleEnd).getTime() / 1000)), + 0, // presaleStart + 0, // presaleEnd + ethers.constants.HashZero, // presaleMerkleRoot + ] + const animationUri = '' + const imageUri = '' + + const createEdition = { + target: PUBLIC_ZORA_NFT_CREATOR as AddressType, + functionSignature: 'createEdition()', + calldata: + zoraNFTCreatorContract?.interface.encodeFunctionData( + 'createEdition(string,string,uint64,uint16,address,address,(uint104,uint32,uint64,uint64,uint64,uint64,bytes32),string,string,string)', + [ + name, + symbol, + editionSize, + royaltyBPS, + fundsRecipient, + defaultAdmin, + salesConfig, + description, + animationUri, + imageUri, + ] + ) || '', + value: '', + } + + addTransaction({ + type: TransactionType.DROPOSAL, + summary: 'Create a droposal', + transactions: [createEdition], + }) + + actions.resetForm() + } + + return ( + + + + ) +} diff --git a/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/DroposalForm.schema.ts b/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/DroposalForm.schema.ts new file mode 100644 index 000000000..48e0ba143 --- /dev/null +++ b/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/DroposalForm.schema.ts @@ -0,0 +1,65 @@ +import { debounce } from 'lodash' +import * as yup from 'yup' + +import { isValidAddress } from 'src/utils/ens' +import { getProvider } from 'src/utils/provider' + +export interface DroposalFormValues { + name: string + symbol: string + description: string + media: string + cover: string + pricePerMint?: number + maxPerAddress?: number + maxSupply?: number + royaltyPercentage: number + fundsRecipient: string + publicSaleStart: string + publicSaleEnd: string +} + +const validateAddress = async ( + value: string | undefined, + res: (value: boolean | PromiseLike) => void +) => { + try { + res(!!value && (await isValidAddress(value, getProvider()))) + } catch (err) { + res(false) + } +} + +export const deboucedValidateAddress = debounce(validateAddress, 500) + +const droposalFormSchema = yup.object({ + name: yup.string().required('*'), + symbol: yup.string(), + description: yup.string().required('*'), + media: yup.string().required('*'), + cover: yup.string(), + pricePerMint: yup.number().required('*'), + maxPerAddress: yup.number().integer('Must be whole number'), + maxSupply: yup.number().integer('Must be whole number'), + royaltyPercentage: yup.number().required('*'), + fundsRecipient: yup + .string() + .required('*') + .test( + 'isValidAddress', + 'invalid address', + (value) => new Promise((res) => deboucedValidateAddress(value, res)) + ), + publicSaleStart: yup.string().required('*'), + publicSaleEnd: yup + .string() + .required('*') + .test('isDateInFuture', 'Must be in future', (value: string | undefined) => { + if (!value) return false + const date = new Date(value) + const now = new Date() + return date > now + }), +}) + +export default droposalFormSchema diff --git a/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/DroposalForm.tsx b/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/DroposalForm.tsx new file mode 100644 index 000000000..7fbf1c033 --- /dev/null +++ b/apps/web/src/modules/create-proposal/components/TransactionForm/Droposal/DroposalForm.tsx @@ -0,0 +1,306 @@ +import { Box, Button, Flex, Text } from '@zoralabs/zord' +import { Form, Formik, FormikHelpers } from 'formik' +import { useCallback, useState } from 'react' + +import SmartInput from 'src/components/Fields/SmartInput' +import TextArea from 'src/components/Fields/TextArea' +import { DATE, NUMBER, TEXT } from 'src/components/Fields/types' +import SingleMediaUpload from 'src/components/SingleMediaUpload/SingleMediaUpload' +import { DropdownSelect } from 'src/modules/create-proposal' + +import { defaultInputLabelStyle } from './Droposal.css' +import droposalFormSchema, { DroposalFormValues } from './DroposalForm.schema' + +export interface AirdropFormProps { + onSubmit?: ( + values: DroposalFormValues, + actions: FormikHelpers + ) => void + disabled?: boolean +} + +const editionSizeOptions = [ + { label: 'Fixed', value: 'fixed' }, + { label: 'Open edition', value: 'open' }, +] + +export const DroposalForm: React.FC = ({ onSubmit, disabled }) => { + const [showCover, setShowCover] = useState(false) + const [editionSize, setEditionType] = useState('fixed') + + const initialValues: DroposalFormValues = { + name: '', + symbol: '', + description: '', + media: '', + cover: '', + fundsRecipient: '', + publicSaleStart: '', + publicSaleEnd: '', + royaltyPercentage: 5, + } + + const handleSubmit = useCallback( + (values: DroposalFormValues, actions: FormikHelpers) => { + onSubmit?.(values, actions) + }, + [onSubmit] + ) + + const handleMediaUploadStart = (media: File) => { + if (!media.type.startsWith('image')) setShowCover(true) + } + + return ( + + + {(formik) => { + const handleEditionTypeChanged = (value: string) => { + value === 'open' + ? formik.setFieldValue('editionSize', 0) + : formik.setFieldValue('editionSize', undefined) + setEditionType(value) + } + + return ( + + + + + + +