Use existing base token URI for collection creation (#36)

* Use existing base token URI for collection creation

* Cover image preview when using existing base token URI option

* Fix cover image preview border for non-square images

* check baseTokenURI and coverImageUrl validity

* Fix typo

* Cover image validity condition update

* Remove unnecessary conditionals

* Display correct collection creation results when using an existing base token URI

* Fix: base token URI not being displayed correctly for new uploads

* Post-review update

* Remove extra props

Co-authored-by: findolor <anakisci@gmail.com>
This commit is contained in:
Serkan Reis 2022-08-16 10:04:33 +03:00 committed by GitHub
parent c7098cc40c
commit 1624f0c332
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 174 additions and 102 deletions

View File

@ -1,4 +1,5 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
@ -11,9 +12,12 @@ import { useEffect, useState } from 'react'
import { toast } from 'react-hot-toast'
import { TextInput } from '../../forms/FormInput'
import type { UploadMethod } from './UploadDetails'
interface CollectionDetailsProps {
onChange: (data: CollectionDetailsDataProps) => void
uploadMethod: UploadMethod
coverImageUrl: string
}
export interface CollectionDetailsDataProps {
@ -23,7 +27,7 @@ export interface CollectionDetailsDataProps {
externalLink?: string
}
export const CollectionDetails = ({ onChange }: CollectionDetailsProps) => {
export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: CollectionDetailsProps) => {
const [coverImage, setCoverImage] = useState<File | null>(null)
const nameState = useInputState({
@ -84,23 +88,44 @@ export const CollectionDetails = ({ onChange }: CollectionDetailsProps) => {
<FormGroup subtitle="Information about your collection" title="Collection Details">
<TextInput {...nameState} isRequired />
<TextInput {...descriptionState} isRequired />
<FormControl isRequired title="Cover Image">
<input
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:hover:bg-white/5 before:transition',
)}
id="cover-image"
onChange={selectCoverImage}
type="file"
/>
{coverImage !== null && (
<div className="w-[200px] h-[200px] rounded border-2">
<img alt="cover-preview" src={URL.createObjectURL(coverImage)} />
<FormControl isRequired={uploadMethod === 'new'} title="Cover Image">
{uploadMethod === 'new' && (
<input
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:hover:bg-white/5 before:transition',
)}
id="cover-image"
onChange={selectCoverImage}
type="file"
/>
)}
{coverImage !== null && uploadMethod === 'new' && (
<div className="max-w-[200px] max-h-[200px] rounded border-2">
<img alt="no-preview-available" src={URL.createObjectURL(coverImage)} />
</div>
)}
{uploadMethod === 'existing' && coverImageUrl?.includes('ipfs://') && (
<div className="max-w-[200px] max-h-[200px] rounded border-2">
<img
alt="no-preview-available"
src={`https://ipfs.io/ipfs/${coverImageUrl.substring(coverImageUrl.lastIndexOf('ipfs://') + 7)}`}
/>
</div>
)}
{uploadMethod === 'existing' && coverImageUrl && !coverImageUrl?.includes('ipfs://') && (
<div className="max-w-[200px] max-h-[200px] rounded border-2">
<img alt="no-preview-available" src={coverImageUrl} />
</div>
)}
{uploadMethod === 'existing' && !coverImageUrl && (
<span className="italic font-light ">Waiting for cover image URL to be specified.</span>
)}
</FormControl>
<TextInput {...externalLinkState} />
</FormGroup>
</div>

View File

@ -5,10 +5,12 @@ import { InputDateTime } from 'components/InputDateTime'
import React, { useEffect, useState } from 'react'
import { NumberInput } from '../../forms/FormInput'
import type { UploadMethod } from './UploadDetails'
interface MintingDetailsProps {
onChange: (data: MintingDetailsDataProps) => void
numberOfTokens: number | undefined
uploadMethod: UploadMethod
}
export interface MintingDetailsDataProps {
@ -18,7 +20,7 @@ export interface MintingDetailsDataProps {
startTime: string
}
export const MintingDetails = ({ onChange, numberOfTokens }: MintingDetailsProps) => {
export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: MintingDetailsProps) => {
const [timestamp, setTimestamp] = useState<Date | undefined>()
const numberOfTokensState = useNumberInputState({
@ -60,7 +62,12 @@ export const MintingDetails = ({ onChange, numberOfTokens }: MintingDetailsProps
return (
<div>
<FormGroup subtitle="Information about your minting settings" title="Minting Details">
<NumberInput {...numberOfTokensState} disabled isRequired value={numberOfTokens} />
<NumberInput
{...numberOfTokensState}
disabled={uploadMethod === 'new'}
isRequired
value={uploadMethod === 'new' ? numberOfTokens : numberOfTokensState.value}
/>
<NumberInput {...unitPriceState} isRequired />
<NumberInput {...perAddressLimitState} isRequired />
<FormControl htmlId="timestamp" isRequired subtitle="Start time for the minting" title="Start Time">

View File

@ -6,14 +6,13 @@ import { Conditional } from 'components/Conditional'
import { TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks'
import { MetadataModal } from 'components/MetadataModal'
import { setBaseTokenUri, setImage, useCollectionStore } from 'contexts/collection'
import type { ChangeEvent } from 'react'
import { useEffect, useState } from 'react'
import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload'
import { naturalCompare } from 'utils/sort'
type UploadMethod = 'new' | 'existing'
export type UploadMethod = 'new' | 'existing'
interface UploadDetailsProps {
onChange: (value: UploadDetailsDataProps) => void
@ -26,10 +25,12 @@ export interface UploadDetailsDataProps {
nftStorageApiKey?: string
pinataApiKey?: string
pinataSecretKey?: string
uploadMethod: UploadMethod
baseTokenURI?: string
imageUrl?: string
}
export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const baseTokenURI = useCollectionStore().base_token_uri
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
@ -60,13 +61,21 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
defaultValue: '9d6f42dc01eaab15f52eac8f36cc4f0ee4184944cb3cdbcda229d06ecf877ee7',
})
const handleChangeBaseTokenUri = (event: { target: { value: React.SetStateAction<string> } }) => {
setBaseTokenUri(event.target.value.toString())
}
const baseTokenUriState = useInputState({
id: 'baseTokenUri',
name: 'baseTokenUri',
title: 'Base Token URI',
placeholder: 'ipfs://',
defaultValue: '',
})
const handleChangeImage = (event: { target: { value: React.SetStateAction<string> } }) => {
setImage(event.target.value.toString())
}
const coverImageUrlState = useInputState({
id: 'coverImageUrl',
name: 'coverImageUrl',
title: 'Cover Image URL',
placeholder: 'ipfs://',
defaultValue: '',
})
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
const files: File[] = []
@ -135,6 +144,9 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
nftStorageApiKey: nftStorageApiKeyState.value,
pinataApiKey: pinataApiKeyState.value,
pinataSecretKey: pinataSecretKeyState.value,
uploadMethod,
baseTokenURI: baseTokenUriState.value,
imageUrl: coverImageUrlState.value,
}
onChange(data)
} catch (error: any) {
@ -147,11 +159,16 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
nftStorageApiKeyState.value,
pinataApiKeyState.value,
pinataSecretKeyState.value,
uploadMethod,
baseTokenUriState.value,
coverImageUrlState.value,
])
useEffect(() => {
setAssetFilesArray([])
setMetadataFilesArray([])
baseTokenUriState.onChange('')
coverImageUrlState.onChange('')
}, [uploadMethod])
return (
@ -192,14 +209,6 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
</div>
</div>
{baseTokenURI && (
<Alert className="mt-5" type="info">
<a href={baseTokenURI} rel="noreferrer" target="_blank">
Base Token URI: {baseTokenURI}
</a>
</Alert>
)}
<div className="p-3 py-5 pb-8">
<Conditional test={uploadMethod === 'existing'}>
<div className="ml-3 flex-column">
@ -216,33 +225,10 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
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" htmlFor="coverImage">
Collection Cover Image
</label>
<input
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,"
id="coverImage"
onChange={handleChangeImage}
placeholder="ipfs://bafybeigi3bwpvyvsmnbj46ra4hyffcxdeaj6ntfk5jpic5mx27x6ih2qvq/images/1.png"
/>
<TextInput {...baseTokenUriState} className="w-1/2" />
</div>
<div>
<label
className="block mt-3 mr-1 mb-1 ml-5 font-bold text-white dark:text-gray-300"
htmlFor="baseTokenURI"
>
Base Token URI
</label>
<input
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,"
id="baseTokenURI"
onChange={handleChangeBaseTokenUri}
placeholder="ipfs://..."
/>
<TextInput {...coverImageUrlState} className="mt-2 w-1/2" />
</div>
</div>
</Conditional>

View File

@ -1,4 +1,5 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
@ -27,13 +28,13 @@ import { NextSeo } from 'next-seo'
import { useEffect, useRef, useState } from 'react'
import useCollapse from 'react-collapsed'
import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload'
import { upload } from 'services/upload'
import { compareFileArrays } from 'utils/compareFileArrays'
import { MINTER_CODE_ID, SG721_CODE_ID, WHITELIST_CODE_ID } from 'utils/constants'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import type { UploadMethod } from '../../components/collections/creation/UploadDetails'
import { getAssetType } from '../../utils/getAssetType'
const CollectionCreationPage: NextPage = () => {
@ -55,11 +56,13 @@ const CollectionCreationPage: NextPage = () => {
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 createCollection = async () => {
try {
setBaseTokenUri(null)
setCoverImageUrl(null)
setMinterContractAddress(null)
setSg721ContractAddress(null)
setTransactionHash(null)
@ -68,28 +71,34 @@ const CollectionCreationPage: NextPage = () => {
checkMintingDetails()
checkWhitelistDetails()
checkRoyaltyDetails()
if (uploadDetails?.uploadMethod === 'new') {
setUploading(true)
setUploading(true)
const baseUri = await uploadFiles()
setBaseTokenUri(baseUri)
//upload coverImageUri and append the file name
const coverImageUri = await upload(
collectionDetails?.imageFile as File[],
uploadDetails?.uploadService as UploadServiceType,
'cover',
uploadDetails?.nftStorageApiKey as string,
uploadDetails?.pinataApiKey as string,
uploadDetails?.pinataSecretKey as string,
)
setUploading(false)
let whitelist: string | undefined
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist()
await instantiate(baseUri, coverImageUri, whitelist)
const baseUri = await uploadFiles()
//upload coverImageUri and append the file name
const coverImageUri = await upload(
collectionDetails?.imageFile as File[],
uploadDetails.uploadService,
'cover',
uploadDetails.nftStorageApiKey as string,
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) {
@ -132,9 +141,8 @@ const CollectionCreationPage: NextPage = () => {
share: (Number(royaltyDetails.share) / 100).toString(),
}
}
const msg = {
base_token_uri: `ipfs://${baseUri}/`,
base_token_uri: `${uploadDetails?.uploadMethod === 'new' ? `ipfs://${baseUri}/` : `${baseUri}`}`,
num_tokens: mintingDetails?.numTokens,
sg721_code_id: SG721_CODE_ID,
sg721_instantiate_msg: {
@ -144,7 +152,11 @@ const CollectionCreationPage: NextPage = () => {
collection_info: {
creator: wallet.address,
description: collectionDetails?.description,
image: `ipfs://${coverImageUri}/${collectionDetails?.imageFile[0].name as string}`,
image: `${
uploadDetails?.uploadMethod === 'new'
? `ipfs://${coverImageUri}/${collectionDetails?.imageFile[0].name as string}`
: `${coverImageUri}`
}`,
external_link: collectionDetails?.externalLink === '' ? null : collectionDetails?.externalLink,
royalty_info: royaltyInfo,
},
@ -223,19 +235,34 @@ const CollectionCreationPage: NextPage = () => {
if (!uploadDetails) {
throw new Error('Please select assets and metadata')
}
if (uploadDetails.assetFiles.length === 0) {
if (uploadDetails.uploadMethod === 'new' && uploadDetails.assetFiles.length === 0) {
throw new Error('Please select the assets')
}
if (uploadDetails.metadataFiles.length === 0) {
if (uploadDetails.uploadMethod === 'new' && uploadDetails.metadataFiles.length === 0) {
throw new Error('Please select the metadata files')
}
compareFileArrays(uploadDetails.assetFiles, uploadDetails.metadataFiles)
if (uploadDetails.uploadService === 'nft-storage') {
if (uploadDetails.nftStorageApiKey === '') {
throw new Error('Please enter a valid NFT Storage API key')
if (uploadDetails.uploadMethod === 'new') compareFileArrays(uploadDetails.assetFiles, uploadDetails.metadataFiles)
if (uploadDetails.uploadMethod === 'new') {
if (uploadDetails.uploadService === 'nft-storage') {
if (uploadDetails.nftStorageApiKey === '') {
throw new Error('Please enter a valid NFT Storage API key')
}
} else if (uploadDetails.pinataApiKey === '' || uploadDetails.pinataSecretKey === '') {
throw new Error('Please enter Pinata API and secret keys')
}
} else if (uploadDetails.pinataApiKey === '' || uploadDetails.pinataSecretKey === '') {
throw new Error('Please enter Pinata API and secret keys')
}
if (uploadDetails.uploadMethod === 'existing' && !uploadDetails.baseTokenURI?.includes('ipfs://')) {
throw new Error('Please specify a valid base token URI')
}
if (
uploadDetails.uploadMethod === 'existing' &&
uploadDetails.imageUrl?.substring(uploadDetails.imageUrl.lastIndexOf('.') + 1) !== 'jpg' &&
uploadDetails.imageUrl?.substring(uploadDetails.imageUrl.lastIndexOf('.') + 1) !== 'png' &&
uploadDetails.imageUrl?.substring(uploadDetails.imageUrl.lastIndexOf('.') + 1) !== 'jpeg' &&
uploadDetails.imageUrl?.substring(uploadDetails.imageUrl.lastIndexOf('.') + 1) !== 'gif' &&
uploadDetails.imageUrl?.substring(uploadDetails.imageUrl.lastIndexOf('.') + 1) !== 'svg'
) {
throw new Error('Please specify a valid cover image URL')
}
}
@ -243,7 +270,8 @@ const CollectionCreationPage: NextPage = () => {
if (!collectionDetails) throw new Error('Please fill out the collection details')
if (collectionDetails.name === '') throw new Error('Collection name is required')
if (collectionDetails.description === '') throw new Error('Collection description is required')
if (collectionDetails.imageFile.length === 0) throw new Error('Collection cover image is required')
if (uploadDetails?.uploadMethod === 'new' && collectionDetails.imageFile.length === 0)
throw new Error('Collection cover image is required')
}
const checkMintingDetails = () => {
@ -288,6 +316,11 @@ const CollectionCreationPage: NextPage = () => {
if (minterContractAddress !== null) scrollRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [minterContractAddress])
useEffect(() => {
setBaseTokenUri(uploadDetails?.baseTokenURI as string)
setCoverImageUrl(uploadDetails?.imageUrl as string)
}, [uploadDetails?.baseTokenURI, uploadDetails?.imageUrl])
return (
<div>
<NextSeo title="Create Collection" />
@ -312,13 +345,26 @@ const CollectionCreationPage: NextPage = () => {
<Alert className="mt-5" type="info">
<div>
Base Token URI:{' '}
<Anchor
className="text-stargaze hover:underline"
external
href={`https://ipfs.stargaze.zone/ipfs/${baseTokenUri as string}/`}
>
ipfs://{baseTokenUri as string}/
</Anchor>
{uploadDetails?.uploadMethod === 'new' && (
<Anchor
className="text-stargaze hover:underline"
external
href={`https://ipfs.stargaze.zone/ipfs/${baseTokenUri as string}/`}
>
ipfs://{baseTokenUri as string}/
</Anchor>
)}
{uploadDetails?.uploadMethod === 'existing' && (
<Anchor
className="text-stargaze hover:underline"
external
href={`https://ipfs.stargaze.zone/ipfs/${baseTokenUri?.substring(
baseTokenUri.lastIndexOf('ipfs://') + 7,
)}/`}
>
ipfs://{baseTokenUri?.substring(baseTokenUri.lastIndexOf('ipfs://') + 7)}/
</Anchor>
)}
<br />
Minter Contract Address:{' '}
<Anchor
@ -354,8 +400,16 @@ const CollectionCreationPage: NextPage = () => {
<UploadDetails onChange={setUploadDetails} />
<div className="flex justify-between py-3 px-8 rounded border-2 border-white/20 grid-col-2">
<CollectionDetails onChange={setCollectionDetails} />
<MintingDetails numberOfTokens={uploadDetails?.assetFiles.length} onChange={setMintingDetails} />
<CollectionDetails
coverImageUrl={coverImageUrl as string}
onChange={setCollectionDetails}
uploadMethod={uploadDetails?.uploadMethod as UploadMethod}
/>
<MintingDetails
numberOfTokens={uploadDetails?.assetFiles.length}
onChange={setMintingDetails}
uploadMethod={uploadDetails?.uploadMethod as UploadMethod}
/>
</div>
<div className="flex justify-between my-6">