Merge development to main (#43)

* UI changes & improvements (#41)

* Remove show advanced options button, add symbol input

* Add checks for minting time constraints

* Royalty share input placeholder update

* Update input subtitles & error messages

* Token price subtitles now include minimum token price

* Ensure the files to be uploaded are in numerical order starting from 1

* Add collection creation confirmation modal

* Make some changes

Co-authored-by: findolor <anakisci@gmail.com>

* Airdrop feature added to collection actions (#42)

Co-authored-by: name-user1 <eray@deuslabs.fi>

Co-authored-by: Serkan Reis <serkanreis@gmail.com>
Co-authored-by: name-user1 <101495985+name-user1@users.noreply.github.com>
Co-authored-by: name-user1 <eray@deuslabs.fi>
This commit is contained in:
Arda Nakışçı 2022-09-01 09:27:23 +03:00 committed by GitHub
parent 00960f429e
commit 9dc19a99ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 234 additions and 77 deletions

View File

@ -0,0 +1,41 @@
import { Button } from './Button'
export interface ConfirmationModalProps {
confirm: () => void
}
export const ConfirmationModal = (props: ConfirmationModalProps) => {
return (
<div>
<input className="modal-toggle" id="my-modal-2" type="checkbox" />
<label className="cursor-pointer modal" htmlFor="my-modal-2">
<label
className="absolute top-[40%] bottom-5 left-1/3 max-w-xl max-h-40 border-2 no-scrollbar modal-box"
htmlFor="temp"
>
{/* <Alert type="warning"></Alert> */}
<div className="text-xl font-bold">
Are you sure to create a collection with the specified assets, metadata and parameters?
</div>
<div className="flex justify-end w-full">
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600">
<label
className="w-full h-full text-white bg-gray-600 hover:bg-gray-600 border-0 btn modal-button"
htmlFor="my-modal-2"
>
Go Back
</label>
</Button>
<Button className="px-0 mt-4 mb-4 max-h-12" onClick={props.confirm}>
<label
className="w-full h-full text-white bg-plumbus-light hover:bg-plumbus-light border-0 btn modal-button"
htmlFor="my-modal-2"
>
Confirm
</label>
</Button>
</div>
</label>
</label>
</div>
)
}

View File

@ -18,6 +18,7 @@ export const ACTION_TYPES = [
'burn',
'batch_burn',
'shuffle',
'airdrop',
] as const
export interface ActionListItem {
@ -87,6 +88,11 @@ export const ACTION_LIST: ActionListItem[] = [
name: 'Shuffle Tokens',
description: 'Shuffle the token IDs',
},
{
id: 'airdrop',
name: 'Airdrop Tokens',
description: 'Airdrop tokens to given addresses',
},
]
export interface DispatchExecuteProps {
@ -117,6 +123,7 @@ export type DispatchExecuteArgs = {
| { type: Select<'batch_transfer'>; recipient: string; tokenIds: string }
| { type: Select<'burn'>; tokenId: number }
| { type: Select<'batch_burn'>; tokenIds: string }
| { type: Select<'airdrop'>; recipients: string[] }
)
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
@ -161,6 +168,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'batch_burn': {
return sg721Messages.batchBurn(args.tokenIds)
}
case 'airdrop': {
return minterMessages.airdrop(txSigner, args.recipients)
}
default: {
throw new Error('Unknown action')
}
@ -210,6 +220,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
case 'batch_burn': {
return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds)
}
case 'airdrop': {
return minterMessages(minterContract)?.airdrop(args.recipients)
}
default: {
return {}
}

View File

@ -23,6 +23,7 @@ interface CollectionDetailsProps {
export interface CollectionDetailsDataProps {
name: string
description: string
symbol: string
imageFile: File[]
externalLink?: string
}
@ -44,10 +45,17 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
placeholder: 'My Awesome Collection Description',
})
const symbolState = useInputState({
id: 'symbol',
name: 'symbol',
title: 'Symbol',
placeholder: 'SYMBOL',
})
const externalLinkState = useInputState({
id: 'external-link',
name: 'externalLink',
title: 'External Link',
title: 'External Link (optional)',
placeholder: 'https://my-collection...',
})
@ -56,6 +64,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
const data: CollectionDetailsDataProps = {
name: nameState.value,
description: descriptionState.value,
symbol: symbolState.value,
imageFile: coverImage ? [coverImage] : [],
externalLink: externalLinkState.value,
}
@ -88,6 +97,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
<FormGroup subtitle="Information about your collection" title="Collection Details">
<TextInput {...nameState} isRequired />
<TextInput {...descriptionState} isRequired />
<TextInput {...symbolState} isRequired />
<FormControl isRequired={uploadMethod === 'new'} title="Cover Image">
{uploadMethod === 'new' && (

View File

@ -35,8 +35,8 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
id: 'unitPrice',
name: 'unitPrice',
title: 'Unit Price',
subtitle: '',
placeholder: '100',
subtitle: 'Price of each token (min. 50 STARS)',
placeholder: '50',
})
const perAddressLimitState = useNumberInputState({

View File

@ -1,6 +1,6 @@
import { Conditional } from 'components/Conditional'
import { FormGroup } from 'components/FormGroup'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { useInputState } from 'components/forms/FormInput.hooks'
import React, { useEffect, useState } from 'react'
import { NumberInput, TextInput } from '../../forms/FormInput'
@ -28,19 +28,19 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
})
const royaltyShareState = useNumberInputState({
const royaltyShareState = useInputState({
id: 'royalty-share',
name: 'royaltyShare',
title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid',
placeholder: '8',
placeholder: '8%',
})
useEffect(() => {
const data: RoyaltyDetailsDataProps = {
royaltyType: royaltyState,
paymentAddress: royaltyPaymentAddressState.value,
share: royaltyShareState.value,
share: Number(royaltyShareState.value),
}
onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -84,8 +84,8 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
</div>
<Conditional test={royaltyState === 'new'}>
<FormGroup subtitle="Information about royalty" title="Royalty Details">
<TextInput {...royaltyPaymentAddressState} />
<NumberInput {...royaltyShareState} />
<TextInput {...royaltyPaymentAddressState} isRequired />
<NumberInput {...royaltyShareState} isRequired />
</FormGroup>
</Conditional>
</div>

View File

@ -78,11 +78,23 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
})
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
const files: File[] = []
let reader: FileReader
if (event.target.files === null) return
setAssetFilesArray([])
setMetadataFilesArray([])
if (event.target.files === null) return
//sort the files
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
//check if the sorted file names are in numerical order
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
for (let i = 0; i < sortedFileNames.length; i++) {
if (parseInt(sortedFileNames[i]) !== i + 1) {
toast.error('The file names should be in numerical order starting from 1.')
//clear the input
event.target.value = ''
return
}
}
const files: File[] = []
let reader: FileReader
for (let i = 0; i < event.target.files.length; i++) {
reader = new FileReader()
reader.onload = (e) => {
@ -102,10 +114,26 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
}
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
setMetadataFilesArray([])
if (event.target.files === null) return toast.error('No files selected.')
if (event.target.files.length !== assetFilesArray.length) {
event.target.value = ''
return toast.error('The number of metadata files should be equal to the number of asset files.')
}
//sort the files
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
//check if the sorted file names are in numerical order
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
for (let i = 0; i < sortedFileNames.length; i++) {
if (parseInt(sortedFileNames[i]) !== i + 1) {
toast.error('The file names should be in numerical order starting from 1.')
//clear the input
event.target.value = ''
return
}
}
const files: File[] = []
let reader: FileReader
if (event.target.files === null) return toast.error('No files selected.')
setMetadataFilesArray([])
for (let i = 0; i < event.target.files.length; i++) {
reader = new FileReader()
reader.onload = (e) => {

View File

@ -43,15 +43,15 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
id: 'unit-price',
name: 'unitPrice',
title: 'Unit Price',
subtitle: 'Price of each tokens in collection',
placeholder: '500',
subtitle: 'Token price for whitelisted addresses \n (min. 25 STARS)',
placeholder: '25',
})
const memberLimitState = useNumberInputState({
id: 'member-limit',
name: 'memberLimit',
title: 'Member Limit',
subtitle: 'Limit of the whitelisted members',
subtitle: 'Maximum number of whitelisted addresses',
placeholder: '1000',
})
@ -59,7 +59,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
id: 'per-address-limit',
name: 'perAddressLimit',
title: 'Per Address Limit',
subtitle: 'Limit of tokens per address',
subtitle: 'Maximum number of tokens per whitelisted address',
placeholder: '5',
})
@ -145,11 +145,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
</div>
<Conditional test={whitelistState === 'existing'}>
<AddressInput
{...whitelistAddressState}
className="pb-5"
onChange={(e) => whitelistAddressState.onChange(e.target.value)}
/>
<AddressInput {...whitelistAddressState} className="pb-5" isRequired />
</Conditional>
<Conditional test={whitelistState === 'new'}>
@ -158,10 +154,20 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
<NumberInput isRequired {...uniPriceState} />
<NumberInput isRequired {...memberLimitState} />
<NumberInput isRequired {...perAddressLimitState} />
<FormControl htmlId="start-date" isRequired subtitle="Start time for the minting" title="Start Time">
<FormControl
htmlId="start-date"
isRequired
subtitle="Start time for minting tokens to whitelisted addresses"
title="Start Time"
>
<InputDateTime minDate={new Date()} onChange={(date) => setStartDate(date)} value={startDate} />
</FormControl>
<FormControl htmlId="end-date" isRequired subtitle="End time for the minting" title="End Time">
<FormControl
htmlId="end-date"
isRequired
subtitle="End time for minting tokens to whitelisted addresses"
title="End Time"
>
<InputDateTime minDate={new Date()} onChange={(date) => setEndDate(date)} value={endDate} />
</FormControl>
</FormGroup>

View File

@ -37,6 +37,7 @@ export interface MinterInstance {
batchMint: (senderAddress: string, recipient: string, batchNumber: number) => Promise<string>
shuffle: (senderAddress: string) => Promise<string>
withdraw: (senderAddress: string) => Promise<string>
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
}
export interface MinterMessages {
@ -46,9 +47,10 @@ export interface MinterMessages {
updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage
mintTo: (recipient: string) => MintToMessage
mintFor: (recipient: string, tokenId: number) => MintForMessage
batchMint: (recipient: string, batchNumber: number) => BatchMintMessage
batchMint: (recipient: string, batchNumber: number) => CustomMessage
shuffle: () => ShuffleMessage
withdraw: () => WithdrawMessage
airdrop: (recipients: string[]) => CustomMessage
}
export interface MintMessage {
@ -114,7 +116,7 @@ export interface MintForMessage {
funds: Coin[]
}
export interface BatchMintMessage {
export interface CustomMessage {
sender: string
contract: string
msg: Record<string, unknown>[]
@ -301,6 +303,29 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
return res.transactionHash
}
const airdrop = async (senderAddress: string, recipients: string[]): Promise<string> => {
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
for (let i = 0; i < recipients.length; i++) {
const msg = {
mint_to: { recipient: recipients[i] },
}
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: senderAddress,
contract: contractAddress,
msg: toUtf8(JSON.stringify(msg)),
}),
}
executeContractMsgs.push(executeContractMsg)
}
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'airdrop')
return res.transactionHash
}
const shuffle = async (senderAddress: string): Promise<string> => {
const res = await client.execute(
senderAddress,
@ -343,6 +368,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
mintTo,
mintFor,
batchMint,
airdrop,
shuffle,
withdraw,
}
@ -441,7 +467,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
}
}
const batchMint = (recipient: string, batchNumber: number): BatchMintMessage => {
const batchMint = (recipient: string, batchNumber: number): CustomMessage => {
const msg: Record<string, unknown>[] = []
for (let i = 0; i < batchNumber; i++) {
msg.push({ mint_to: { recipient } })
@ -454,6 +480,19 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
}
}
const airdrop = (recipients: string[]): CustomMessage => {
const msg: Record<string, unknown>[] = []
for (let i = 0; i < recipients.length; i++) {
msg.push({ mint_to: { recipient: recipients[i] } })
}
return {
sender: txSigner,
contract: contractAddress,
msg,
funds: [],
}
}
const shuffle = (): ShuffleMessage => {
return {
sender: txSigner,
@ -484,6 +523,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
mintTo,
mintFor,
batchMint,
airdrop,
shuffle,
withdraw,
}

View File

@ -22,6 +22,7 @@
"@fontsource/roboto": "^4",
"@headlessui/react": "^1",
"@keplr-wallet/cosmos": "^0.9.16",
"@pinata/sdk": "^1.1.26",
"@popperjs/core": "^2",
"@svgr/webpack": "^6",
"@tailwindcss/forms": "^0",
@ -34,7 +35,6 @@
"next": "^12",
"next-seo": "^4",
"nft.storage": "^6.3.0",
"@pinata/sdk": "^1.1.26",
"react": "^18",
"react-datetime-picker": "^3",
"react-dom": "^18",
@ -44,7 +44,6 @@
"react-popper": "^2",
"react-query": "^3",
"react-tracked": "^1",
"react-collapsed": "^3",
"scheduler": "^0",
"zustand": "^3"
},

View File

@ -6,11 +6,13 @@ import { useActionsComboboxState } from 'components/collections/actions/Combobox
import { Conditional } from 'components/Conditional'
import { ContractPageHeader } from 'components/ContractPageHeader'
import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview'
import { TransactionHash } from 'components/TransactionHash'
import { WhitelistUpload } from 'components/WhitelistUpload'
import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
import type { NextPage } from 'next'
@ -31,6 +33,7 @@ const CollectionActionsPage: NextPage = () => {
const [lastTx, setLastTx] = useState('')
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [airdropArray, setAirdropArray] = useState<string[]>([])
const comboboxState = useActionsComboboxState()
const type = comboboxState.value?.id
@ -99,6 +102,7 @@ const CollectionActionsPage: NextPage = () => {
const showNumberOfTokensField = type === 'batch_mint'
const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer'])
const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer'])
const showAirdropFileField = type === 'airdrop'
const minterMessages = useMemo(
() => minterContract?.use(minterContractState.value),
@ -120,6 +124,7 @@ const CollectionActionsPage: NextPage = () => {
minterMessages,
sg721Messages,
recipient: recipientState.value,
recipients: airdropArray,
txSigner: wallet.address,
type,
}
@ -148,6 +153,10 @@ const CollectionActionsPage: NextPage = () => {
},
)
const airdropFileOnChange = (data: string[]) => {
setAirdropArray(data)
}
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="Collection Actions" />
@ -168,6 +177,11 @@ const CollectionActionsPage: NextPage = () => {
{showTokenIdField && <NumberInput {...tokenIdState} />}
{showTokenIdListField && <TextInput {...tokenIdListState} />}
{showNumberOfTokensField && <NumberInput {...batchNumberState} />}
{showAirdropFileField && (
<FormGroup subtitle="TXT file that contains the airdrop addresses" title="Airdrop File">
<WhitelistUpload onChange={airdropFileOnChange} />
</FormGroup>
)}
<Conditional test={showDateField}>
<FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />

View File

@ -26,7 +26,6 @@ import { useWallet } from 'contexts/wallet'
import type { NextPage } from 'next'
import { NextSeo } from 'next-seo'
import { useEffect, useRef, useState } from 'react'
import useCollapse from 'react-collapsed'
import { toast } from 'react-hot-toast'
import { upload } from 'services/upload'
import { compareFileArrays } from 'utils/compareFileArrays'
@ -35,15 +34,12 @@ import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import type { UploadMethod } from '../../components/collections/creation/UploadDetails'
import { ConfirmationModal } from '../../components/ConfirmationModal'
import { getAssetType } from '../../utils/getAssetType'
const CollectionCreationPage: NextPage = () => {
const wallet = useWallet()
const { minter: minterContract, whitelist: whitelistContract } = useContracts()
const { getCollapseProps, getToggleProps, isExpanded } = useCollapse()
const toggleProps = getToggleProps()
const collapseProps = getCollapseProps()
const scrollRef = useRef<HTMLDivElement>(null)
const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null)
@ -53,12 +49,29 @@ const CollectionCreationPage: NextPage = () => {
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
const [uploading, setUploading] = useState(false)
const [readyToCreate, setReadyToCreate] = useState(false)
const [minterContractAddress, setMinterContractAddress] = useState<string | null>(null)
const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null)
const [baseTokenUri, setBaseTokenUri] = useState<string | null>(null)
const [coverImageUrl, setCoverImageUrl] = useState<string | null>(null)
const [transactionHash, setTransactionHash] = useState<string | null>(null)
const performChecks = () => {
try {
// setReadyToCreate(false)
// checkUploadDetails()
// checkCollectionDetails()
// checkMintingDetails()
// checkWhitelistDetails()
// checkRoyaltyDetails()
setReadyToCreate(true)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
toast.error(error.message)
setUploading(false)
}
}
const createCollection = async () => {
try {
setBaseTokenUri(null)
@ -66,11 +79,6 @@ const CollectionCreationPage: NextPage = () => {
setMinterContractAddress(null)
setSg721ContractAddress(null)
setTransactionHash(null)
checkUploadDetails()
checkCollectionDetails()
checkMintingDetails()
checkWhitelistDetails()
checkRoyaltyDetails()
if (uploadDetails?.uploadMethod === 'new') {
setUploading(true)
@ -84,22 +92,27 @@ const CollectionCreationPage: NextPage = () => {
uploadDetails.pinataApiKey as string,
uploadDetails.pinataSecretKey as string,
)
setUploading(false)
setBaseTokenUri(baseUri)
setCoverImageUrl(coverImageUri)
let whitelist: string | undefined
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist()
await instantiate(baseUri, coverImageUri, whitelist)
} else {
setBaseTokenUri(uploadDetails?.baseTokenURI as string)
setCoverImageUrl(uploadDetails?.imageUrl as string)
let whitelist: string | undefined
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist()
await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
toast.error(error.message)
@ -141,13 +154,14 @@ const CollectionCreationPage: NextPage = () => {
share: (Number(royaltyDetails.share) / 100).toString(),
}
}
const msg = {
base_token_uri: `${uploadDetails?.uploadMethod === 'new' ? `ipfs://${baseUri}/` : `${baseUri}`}`,
num_tokens: mintingDetails?.numTokens,
sg721_code_id: SG721_CODE_ID,
sg721_instantiate_msg: {
name: collectionDetails?.name,
symbol: 'SYMBOL',
symbol: collectionDetails?.symbol,
minter: wallet.address,
collection_info: {
creator: wallet.address,
@ -187,20 +201,25 @@ const CollectionCreationPage: NextPage = () => {
.then((assetUri: string) => {
const fileArray: File[] = []
let reader: FileReader
for (let i = 0; i < uploadDetails.metadataFiles.length; i++) {
reader = new FileReader()
reader.onload = (e) => {
const data: any = JSON.parse(e.target?.result as string)
if (
getAssetType(uploadDetails.assetFiles[i].name) === 'audio' ||
getAssetType(uploadDetails.assetFiles[i].name) === 'video'
) {
data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}`
}
data.image = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}`
const metadataFileBlob = new Blob([JSON.stringify(data)], {
type: 'application/json',
})
const updatedMetadataFile = new File(
[metadataFileBlob],
uploadDetails.metadataFiles[i].name.substring(0, uploadDetails.metadataFiles[i].name.lastIndexOf('.')),
@ -208,6 +227,7 @@ const CollectionCreationPage: NextPage = () => {
type: 'application/json',
},
)
fileArray.push(updatedMetadataFile)
}
reader.onloadend = () => {
@ -286,6 +306,7 @@ const CollectionCreationPage: NextPage = () => {
)
throw new Error('Invalid limit for tokens per address')
if (mintingDetails.startTime === '') throw new Error('Start time is required')
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
}
const checkWhitelistDetails = () => {
@ -301,14 +322,18 @@ const CollectionCreationPage: NextPage = () => {
if (whitelistDetails.endTime === '') throw new Error('End time is required')
if (whitelistDetails.perAddressLimit === 0) throw new Error('Per address limit is required')
if (whitelistDetails.memberLimit === 0) throw new Error('Member limit is required')
if (Number(whitelistDetails.startTime) > Number(whitelistDetails.endTime))
throw new Error('Whitelist start time cannot be later than whitelist end time')
if (Number(whitelistDetails.endTime) > Number(mintingDetails?.startTime))
throw new Error('Whitelist end time cannot be later than public start time')
}
}
const checkRoyaltyDetails = () => {
if (!royaltyDetails) throw new Error('Please fill out the royalty details')
if (royaltyDetails.royaltyType === 'new') {
if (royaltyDetails.share === 0) throw new Error('Royalty share is required')
if (royaltyDetails.share > 100 || royaltyDetails.share < 0) throw new Error('Invalid royalty share')
if (royaltyDetails.share === 0) throw new Error('Royalty share percentage is required')
if (royaltyDetails.share > 100 || royaltyDetails.share < 0) throw new Error('Invalid royalty share percentage')
if (royaltyDetails.paymentAddress === '') throw new Error('Royalty payment address is required')
}
}
@ -411,21 +436,22 @@ const CollectionCreationPage: NextPage = () => {
uploadMethod={uploadDetails?.uploadMethod as UploadMethod}
/>
</div>
<div className="flex justify-between my-6">
<Button {...toggleProps} isWide type="button" variant="outline">
{isExpanded ? 'Hide' : 'Show'} Advanced Details
</Button>
<Button isWide onClick={createCollection} variant="solid">
Create Collection
</Button>
</div>
<section {...collapseProps} className="mb-10">
<div className="my-6">
<WhitelistDetails onChange={setWhitelistDetails} />
<div className="my-6" />
<RoyaltyDetails onChange={setRoyaltyDetails} />
</section>
</div>
{readyToCreate && <ConfirmationModal confirm={createCollection} />}
<div className="flex justify-end w-full">
<Button className="px-0 mb-6 max-h-12" onClick={performChecks} variant="solid">
<label
className="relative justify-end w-full h-full text-white bg-plumbus-light hover:bg-plumbus-light border-0 btn modal-button"
htmlFor="my-modal-2"
>
Create Collection
</label>
</Button>
</div>
</div>
</div>
)

View File

@ -6541,11 +6541,6 @@ pbkdf2@^3.0.16, pbkdf2@^3.0.9, pbkdf2@^3.1.1:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
@ -6725,13 +6720,6 @@ rabin-wasm@^0.1.4:
node-fetch "^2.6.1"
readable-stream "^3.6.0"
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
dependencies:
performance-now "^2.1.0"
randombytes@^2.0.1:
version "2.1.0"
resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz"
@ -6759,14 +6747,6 @@ react-clock@^3.0.0:
merge-class-names "^1.1.1"
prop-types "^15.6.0"
react-collapsed@^3:
version "3.3.2"
resolved "https://registry.yarnpkg.com/react-collapsed/-/react-collapsed-3.3.2.tgz#b04dd13db5370ef553feac6556c911bb41dc63e2"
integrity sha512-P3YmP0k3Z7LaELQP2CbXjWSjiaNEIBvvxFUAJToJwztMFrTdHe9v7vQMz3FtMTyxLDJc9eGdNA8qMxCe9Aj3tg==
dependencies:
raf "^3.4.1"
tiny-warning "^1.0.3"
react-date-picker@^8.4.0:
version "8.4.0"
resolved "https://registry.npmjs.org/react-date-picker/-/react-date-picker-8.4.0.tgz"
@ -7585,7 +7565,7 @@ tiny-secp256k1@^1.1.3:
elliptic "^6.4.0"
nan "^2.13.2"
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
tiny-warning@^1.0.0:
version "1.0.3"
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==