Add collection actions page (#3)
* Add collection actions page * Add collections index page and update sidebar
This commit is contained in:
parent
aa42f8763a
commit
708da8a58a
@ -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">
|
||||
|
||||
8
components/collections/actions/Combobox.hooks.ts
Normal file
8
components/collections/actions/Combobox.hooks.ts
Normal 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) }
|
||||
}
|
||||
93
components/collections/actions/Combobox.tsx
Normal file
93
components/collections/actions/Combobox.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
169
components/collections/actions/actions.ts
Normal file
169
components/collections/actions/actions.ts
Normal 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)
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export { default } from './upload'
|
||||
@ -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'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 })
|
||||
172
pages/collections/actions.tsx
Normal file
172
pages/collections/actions.tsx
Normal 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 })
|
||||
49
pages/collections/index.tsx
Normal file
49
pages/collections/index.tsx
Normal 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 })
|
||||
Loading…
Reference in New Issue
Block a user