Add collection actions page (#3)

* Add collection actions page

* Add collections index page and update sidebar
This commit is contained in:
Arda Nakışçı 2022-07-25 11:29:52 +03:00 committed by GitHub
parent aa42f8763a
commit 708da8a58a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 496 additions and 326 deletions

View File

@ -3,13 +3,14 @@ import { Anchor } from 'components/Anchor'
import { useWallet } from 'contexts/wallet'
import { useRouter } from 'next/router'
// import BrandText from 'public/brand/brand-text.svg'
import { footerLinks, links, socialsLinks } from 'utils/links'
import { footerLinks, socialsLinks } from 'utils/links'
import { SidebarLayout } from './SidebarLayout'
import { WalletLoader } from './WalletLoader'
const routes = [
{ text: 'Create Collection', href: `/collection/` },
{ text: 'Create Collection', href: `/collections/` },
{ text: 'Collections', href: `/collections` },
{ text: 'Contract Dashboards', href: `/contracts/` },
]
@ -53,7 +54,7 @@ export const Sidebar = () => {
<div className="flex-grow" />
{/* Stargaze network status */}
<div className="text-sm">Network: {wallet.network}</div>
<div className="text-sm capitalize">Network: {wallet.network}</div>
{/* footer reference links */}
<ul className="text-sm list-disc list-inside">
@ -67,13 +68,7 @@ export const Sidebar = () => {
</ul>
{/* footer attribution */}
<div className="text-xs text-white/50">
StargazeTools {process.env.APP_VERSION} <br />
Made by{' '}
<Anchor className="text-plumbus hover:underline" href={links.deuslabs}>
deus labs
</Anchor>
</div>
<div className="text-xs text-white/50">Stargaze Studio {process.env.APP_VERSION}</div>
{/* footer social links */}
<div className="flex gap-x-6 items-center text-white/75">

View File

@ -0,0 +1,8 @@
import { useState } from 'react'
import type { ActionListItem } from './actions'
export const useActionsComboboxState = () => {
const [value, setValue] = useState<ActionListItem | null>(null)
return { value, onChange: (item: ActionListItem) => setValue(item) }
}

View File

@ -0,0 +1,93 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { ActionListItem } from './actions'
import { ACTION_LIST } from './actions'
export interface ActionsComboboxProps {
value: ActionListItem | null
onChange: (item: ActionListItem) => void
}
export const ActionsCombobox = ({ value, onChange }: ActionsComboboxProps) => {
const [search, setSearch] = useState('')
const filtered =
search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="action"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Collection actions"
title="Action"
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: ActionListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select action"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Action found.
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -0,0 +1,169 @@
import type { MinterInstance } from 'contracts/minter'
import { useMinterContract } from 'contracts/minter'
import type { SG721Instance } from 'contracts/sg721'
import { useSG721Contract } from 'contracts/sg721'
export type ActionType = typeof ACTION_TYPES[number]
export const ACTION_TYPES = [
'mint_to',
'mint_for',
'set_whitelist',
'update_start_time',
'update_per_address_limit',
'withdraw',
'transfer',
'shuffle',
] as const
export interface ActionListItem {
id: ActionType
name: string
description?: string
}
export const ACTION_LIST: ActionListItem[] = [
{
id: 'mint_to',
name: 'Mint To',
description: `Mint a token to a user`,
},
{
id: 'mint_for',
name: 'Mint For',
description: `Mint a token for a user with given token ID`,
},
{
id: 'set_whitelist',
name: 'Set Whitelist',
description: `Set whitelist contract address`,
},
{
id: 'update_start_time',
name: 'Update Start Time',
description: `Update start time for minting`,
},
{
id: 'update_per_address_limit',
name: 'Update Tokens Per Address Limit',
description: `Update token per address limit`,
},
{
id: 'withdraw',
name: 'Withdraw Tokens',
description: `Withdraw tokens from the contract`,
},
{
id: 'transfer',
name: 'Transfer Tokens',
description: `Transfer tokens from one address to another`,
},
{
id: 'shuffle',
name: 'Shuffle Tokens',
description: 'Shuffle the token IDs',
},
]
export interface DispatchExecuteProps {
type: ActionType
[k: string]: unknown
}
type Select<T extends ActionType> = T
/** @see {@link MinterInstance} */
export type DispatchExecuteArgs = {
minterContract: string
sg721Contract: string
minterMessages?: MinterInstance
sg721Messages?: SG721Instance
txSigner: string
} & (
| { type: undefined }
| { type: Select<'mint_to'>; recipient: string }
| { type: Select<'mint_for'>; recipient: string; tokenId: number }
| { type: Select<'set_whitelist'>; whitelist: string }
| { type: Select<'update_start_time'>; startTime: string }
| { type: Select<'update_per_address_limit'>; limit: number }
| { type: Select<'shuffle'> }
| { type: Select<'withdraw'> }
| { type: Select<'transfer'>; recipient: string; tokenId: number }
)
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { minterMessages, sg721Messages, txSigner } = args
if (!minterMessages || !sg721Messages) {
throw new Error('Cannot execute actions')
}
switch (args.type) {
case 'mint_to': {
return minterMessages.mintTo(txSigner, args.recipient)
}
case 'mint_for': {
return minterMessages.mintFor(txSigner, args.recipient, args.tokenId)
}
case 'set_whitelist': {
return minterMessages.setWhitelist(txSigner, args.whitelist)
}
case 'update_start_time': {
return minterMessages.updateStartTime(txSigner, args.startTime)
}
case 'update_per_address_limit': {
return minterMessages.updatePerAddressLimit(txSigner, args.limit)
}
case 'shuffle': {
return minterMessages.shuffle(txSigner)
}
case 'withdraw': {
return minterMessages.withdraw(txSigner)
}
case 'transfer': {
return sg721Messages.transferNft(args.recipient, args.tokenId.toString())
}
default: {
throw new Error('Unknown action')
}
}
}
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: minterMessages } = useMinterContract()
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: sg721Messages } = useSG721Contract()
const { minterContract, sg721Contract } = args
switch (args.type) {
case 'mint_to': {
return minterMessages()?.mintTo(minterContract, args.recipient)
}
case 'mint_for': {
return minterMessages()?.mintFor(minterContract, args.recipient, args.tokenId)
}
case 'set_whitelist': {
return minterMessages()?.setWhitelist(minterContract, args.whitelist)
}
case 'update_start_time': {
return minterMessages()?.updateStartTime(minterContract, args.startTime)
}
case 'update_per_address_limit': {
return minterMessages()?.updatePerAddressLimit(minterContract, args.limit)
}
case 'shuffle': {
return minterMessages()?.shuffle(minterContract)
}
case 'withdraw': {
return minterMessages()?.withdraw(minterContract)
}
case 'transfer': {
return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString())
}
default: {
return {}
}
}
}
export const isEitherType = <T extends ActionType>(type: unknown, arr: T[]): type is T => {
return arr.some((val) => type === val)
}

View File

@ -1 +0,0 @@
export { default } from './upload'

View File

@ -1,315 +0,0 @@
import clsx from 'clsx'
import Anchor from 'components/Anchor'
import AnchorButton from 'components/AnchorButton'
import Button from 'components/Button'
import { useCollectionStore } from 'contexts/collection'
import { setBaseTokenUri, setImage } from 'contexts/collection'
import { useWallet } from 'contexts/wallet'
import { NextPage } from 'next'
import { NextSeo } from 'next-seo'
import { Blob, File, NFTStorage } from 'nft.storage'
import { useEffect, useRef, useState } from 'react'
import toast from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import { naturalCompare } from 'utils/sort'
const UploadPage: NextPage = () => {
const wallet = useWallet()
const baseTokenURI = useCollectionStore().base_token_uri
const [baseImageURI, setBaseImageURI] = useState('')
const [uploadMethod, setUploadMethod] = useState('New')
const [imageFiles, setImageFiles] = useState<File[]>([])
const [metadataFiles, setMetadataFiles] = useState<File[]>([])
const [updatedMetadataFiles, setUpdatedMetadataFiles] = useState<File[]>([])
let imageFilesArray: File[] = []
let metadataFilesArray: File[] = []
let updatedMetadataFilesArray: File[] = []
const imageFilesRef = useRef<HTMLInputElement>(null)
const metadataFilesRef = useRef<HTMLInputElement>(null)
const NFT_STORAGE_TOKEN =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDJBODk5OGI4ZkE2YTM1NzMyYmMxQTRDQzNhOUU2M0Y2NUM3ZjA1RWIiLCJpc3MiOiJuZnQtc3RvcmFnZSIsImlhdCI6MTY1NTE5MTcwNDQ2MiwibmFtZSI6IlRlc3QifQ.IbdV_26bkPHSdd81sxox5AoG-5a4CCEY4aCrdbCXwAE'
const client = new NFTStorage({ token: NFT_STORAGE_TOKEN })
const handleChangeBaseTokenUri = (event: {
target: { value: React.SetStateAction<string> }
}) => {
setBaseTokenUri(event.target.value.toString())
}
const handleChangeImage = (event: {
target: { value: React.SetStateAction<string> }
}) => {
setImage(event.target.value.toString())
}
const selectImages = async () => {
imageFilesArray = []
let reader: FileReader
if (!imageFilesRef.current?.files) return toast.error('No files selected.')
for (let i = 0; i < imageFilesRef.current.files.length; i++) {
reader = new FileReader()
reader.onload = function (e) {
if (!e.target?.result) return toast.error('Error parsing file.')
if (!imageFilesRef.current?.files)
return toast.error('No files selected.')
let imageFile = new File(
[e.target.result],
imageFilesRef.current.files[i].name,
{ type: 'image/jpg' }
)
imageFilesArray.push(imageFile)
if (i === imageFilesRef.current.files.length - 1) {
imageFilesArray.sort((a, b) => naturalCompare(a.name, b.name))
console.log(imageFilesArray)
selectMetadata()
}
}
if (!imageFilesRef.current.files) return toast.error('No file selected.')
reader.readAsArrayBuffer(imageFilesRef.current.files[i])
//reader.onloadend = function (e) { ...
}
}
const selectMetadata = async () => {
metadataFilesArray = []
let reader: FileReader
if (!metadataFilesRef.current?.files)
return toast.error('No files selected.')
for (let i = 0; i < metadataFilesRef.current.files.length; i++) {
reader = new FileReader()
reader.onload = function (e) {
if (!e.target?.result) return toast.error('Error parsing file.')
if (!metadataFilesRef.current?.files)
return toast.error('No files selected.')
let metadataFile = new File(
[e.target.result],
metadataFilesRef.current.files[i].name,
{ type: 'image/jpg' }
)
metadataFilesArray.push(metadataFile)
if (i === metadataFilesRef.current.files.length - 1) {
metadataFilesArray.sort((a, b) => naturalCompare(a.name, b.name))
console.log(metadataFilesArray)
updateMetadata()
}
}
if (!metadataFilesRef.current?.files)
return toast.error('No file selected.')
reader.readAsText(metadataFilesRef.current.files[i], 'utf8')
//reader.onloadend = function (e) { ...
}
}
const updateMetadata = async () => {
const imageURI = await client.storeDirectory(imageFilesArray)
console.log(imageURI)
updatedMetadataFilesArray = []
let reader: FileReader
for (let i = 0; i < metadataFilesArray.length; i++) {
reader = new FileReader()
reader.onload = function (e) {
let metadataJSON = JSON.parse(e.target?.result as string)
metadataJSON.image = `ipfs://${imageURI}/${imageFilesArray[i].name}`
let metadataFileBlob = new Blob([JSON.stringify(metadataJSON)], {
type: 'application/json',
})
let updatedMetadataFile = new File(
[metadataFileBlob],
metadataFilesArray[i].name,
{ type: 'application/json' }
)
updatedMetadataFilesArray.push(updatedMetadataFile)
console.log(updatedMetadataFile.name + ' => ' + metadataJSON.image)
if (i === metadataFilesArray.length - 1) {
upload()
}
}
reader.readAsText(metadataFilesArray[i], 'utf8')
//reader.onloadend = function (e) { ...
}
}
const upload = async () => {
const baseTokenURI = await client.storeDirectory(updatedMetadataFilesArray)
console.log(baseTokenURI)
}
return (
<div>
<NextSeo title="Create Collection" />
<div className="space-y-8 mt-5 text-center">
<h1 className="font-heading text-4xl font-bold">
Upload Assets & Metadata
</h1>
<p>
Make sure you check our{' '}
<Anchor
href={links['Docs']}
className="font-bold text-plumbus hover:underline"
>
documentation
</Anchor>{' '}
on how to create your collection
</p>
</div>
<hr className="border-white/20" />
<div className="justify-items-start mt-5 mb-3 ml-3 flex-column">
<div className="mt-3 ml-4 form-check form-check-inline">
<input
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
type="radio"
name="inlineRadioOptions2"
id="inlineRadio2"
value="Existing"
onClick={() => {
setUploadMethod('Existing')
}}
onChange={() => { }}
checked={uploadMethod === 'Existing'}
/>
<label
className="inline-block text-white cursor-pointer form-check-label"
htmlFor="inlineRadio2"
>
Use an existing URI
</label>
</div>
<div className="mt-3 ml-4 form-check form-check-inline">
<input
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
type="radio"
name="inlineRadioOptions"
id="inlineRadio3"
value="New"
onClick={() => {
setUploadMethod('New')
}}
onChange={() => { }}
checked={uploadMethod === 'New'}
/>
<label
className="inline-block text-white cursor-pointer form-check-label"
htmlFor="inlineRadio3"
>
Upload assets & metadata
</label>
</div>
</div>
<hr className="border-white/20" />
{uploadMethod == 'Existing' && (
<div className="ml-3 flex-column">
<p className="my-3 ml-5">
Though Stargaze&apos;s sg721 contract allows for off-chain metadata
storage, it is recommended to use a decentralized storage solution,
such as IPFS. <br /> You may head over to{' '}
<Anchor
href="https://nft.storage"
className="font-bold text-plumbus hover:underline"
>
NFT Storage
</Anchor>{' '}
and upload your assets & metadata manually to get a base URI for
your collection.
</p>
<div>
<label className="block mr-1 mb-1 ml-5 font-bold text-white dark:text-gray-300">
Collection Cover Image
</label>
<input
onChange={handleChangeImage}
placeholder="ipfs://bafybeigi3bwpvyvsmnbj46ra4hyffcxdeaj6ntfk5jpic5mx27x6ih2qvq/images/1.png"
className="py-2 px-1 mx-5 mt-2 mb-2 w-1/2 bg-white/10 rounded border-2 border-white/20 focus:ring
focus:ring-plumbus-20
form-input, placeholder:text-white/50,"
/>
</div>
<div>
<label className="block mt-3 mr-1 mb-1 ml-5 font-bold text-white dark:text-gray-300">
Base Token URI
</label>
<input
onChange={handleChangeBaseTokenUri}
placeholder="ipfs://..."
className="py-2 px-1 mx-5 mt-2 mb-2 w-1/2 bg-white/10 rounded border-2 border-white/20 focus:ring
focus:ring-plumbus-20
form-input, placeholder:text-white/50,"
/>
</div>
</div>
)}
{uploadMethod == 'New' && (
<div>
<label className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300">
Image File Selection
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-1/2 h-32',
'rounded border-2 border-white/20 border-dashed'
)}
>
<input
id="imageFiles"
accept="image/*"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition'
)}
onChange={() => { }}
ref={imageFilesRef}
type="file"
multiple
/>
</div>
<label className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300">
Metadata Selection
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-1/2 h-32',
'rounded border-2 border-white/20 border-dashed'
)}
>
<input
id="metadataFiles"
accept=""
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition'
)}
onChange={() => { }}
ref={metadataFilesRef}
type="file"
multiple
/>
</div>
<div className="mt-5 ml-8">
<Button
onClick={selectImages}
variant="solid"
isWide
className="w-[120px]"
>
Upload
</Button>
</div>
</div>
)}
</div>
)
}
export default withMetadata(UploadPage, { center: false })

View File

@ -0,0 +1,172 @@
import { Button } from 'components/Button'
import type { DispatchExecuteArgs } from 'components/collections/actions/actions'
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/collections/actions/actions'
import { ActionsCombobox } from 'components/collections/actions/Combobox'
import { useActionsComboboxState } from 'components/collections/actions/Combobox.hooks'
import { Conditional } from 'components/Conditional'
import { ContractPageHeader } from 'components/ContractPageHeader'
import { FormControl } from 'components/FormControl'
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 { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
import type { NextPage } from 'next'
import { NextSeo } from 'next-seo'
import type { FormEvent } from 'react'
import { useMemo, useState } from 'react'
import { toast } from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa'
import { useMutation } from 'react-query'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
const CollectionActionsPage: NextPage = () => {
const { minter: minterContract, sg721: sg721Contract } = useContracts()
const wallet = useWallet()
const [lastTx, setLastTx] = useState('')
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const comboboxState = useActionsComboboxState()
const type = comboboxState.value?.id
const sg721ContractState = useInputState({
id: 'sg721-contract-address',
name: 'sg721-contract-address',
title: 'Sg721 Address',
subtitle: 'Address of the Sg721 contract',
})
const minterContractState = useInputState({
id: 'minter-contract-address',
name: 'minter-contract-address',
title: 'Minter Address',
subtitle: 'Address of the Minter contract',
})
const limitState = useNumberInputState({
id: 'per-address-limi',
name: 'perAddressLimit',
title: 'Per Address Limit',
subtitle: 'Enter the per address limit',
})
const tokenIdState = useNumberInputState({
id: 'token-id',
name: 'tokenId',
title: 'Token ID',
subtitle: 'Enter the token ID',
})
const recipientState = useInputState({
id: 'recipient-address',
name: 'recipient',
title: 'Recipient Address',
subtitle: 'Address of the recipient',
})
const whitelistState = useInputState({
id: 'whitelist-address',
name: 'whitelistAddress',
title: 'Whitelist Address',
subtitle: 'Address of the whitelist contract',
})
const showWhitelistField = type === 'set_whitelist'
const showDateField = type === 'update_start_time'
const showLimitField = type === 'update_per_address_limit'
const showTokenIdField = isEitherType(type, ['transfer', 'mint_for'])
const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for'])
const minterMessages = useMemo(
() => minterContract?.use(minterContractState.value),
[minterContract, minterContractState.value],
)
const sg721Messages = useMemo(
() => sg721Contract?.use(sg721ContractState.value),
[sg721Contract, sg721ContractState.value],
)
const payload: DispatchExecuteArgs = {
whitelist: whitelistState.value,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
limit: limitState.value,
minterContract: minterContractState.value,
sg721Contract: sg721ContractState.value,
tokenId: tokenIdState.value,
minterMessages,
sg721Messages,
recipient: recipientState.value,
txSigner: wallet.address,
type,
}
const { isLoading, mutate } = useMutation(
async (event: FormEvent) => {
event.preventDefault()
if (!type) {
throw new Error('Please select an action!')
}
if (minterContractState.value === '' && sg721ContractState.value === '') {
throw new Error('Please enter minter and sg721 contract addresses!')
}
const txHash = await toast.promise(dispatchExecute(payload), {
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
loading: 'Executing message...',
success: (tx) => `Transaction ${tx} success!`,
})
if (txHash) {
setLastTx(txHash)
}
},
{
onError: (error) => {
toast.error(String(error))
},
},
)
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="Collection Actions" />
<ContractPageHeader
description="Here you can execute various actions on a collection."
link={links.Documentation}
title="Collection Actions"
/>
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
<div className="space-y-8">
<AddressInput {...sg721ContractState} />
<AddressInput {...minterContractState} />
<ActionsCombobox {...comboboxState} />
{showRecipientField && <AddressInput {...recipientState} />}
{showWhitelistField && <AddressInput {...whitelistState} />}
{showLimitField && <NumberInput {...limitState} />}
{showTokenIdField && <NumberInput {...tokenIdState} />}
<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} />
</FormControl>
</Conditional>
</div>
<div className="space-y-8">
<div className="relative">
<Button className="absolute top-0 right-0" isLoading={isLoading} rightIcon={<FaArrowRight />} type="submit">
Execute
</Button>
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
<TransactionHash hash={lastTx} />
</FormControl>
</div>
<FormControl subtitle="View current message to be sent" title="Payload Preview">
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
</FormControl>
</div>
</form>
</section>
)
}
export default withMetadata(CollectionActionsPage, { center: false })

View File

@ -0,0 +1,49 @@
import { HomeCard } from 'components/HomeCard'
import type { NextPage } from 'next'
// import Brand from 'public/brand/brand.svg'
import { withMetadata } from 'utils/layout'
const HomePage: NextPage = () => {
return (
<section className="px-8 pt-4 pb-16 mx-auto space-y-8 max-w-4xl">
<div className="flex justify-center items-center py-8 max-w-xl">
{/* <Brand className="w-full text-plumbus" /> */}
</div>
<h1 className="font-heading text-4xl font-bold">Collections</h1>
<p className="text-xl">
Here you can create a collection, execute actions and query the results.
<br />
</p>
<br />
<br />
<div className="grid gap-8 md:grid-cols-2">
<HomeCard
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
link="/collections/create"
title="Create a Collection"
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</HomeCard>
<HomeCard
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
link="/collections/actions"
title="Collection Actions"
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</HomeCard>
<HomeCard
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
link="/collections/queries"
title="Collection Queries"
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</HomeCard>
</div>
</section>
)
}
export default withMetadata(HomePage, { center: false })