diff --git a/components/collections/creation/CollectionDetails.tsx b/components/collections/creation/CollectionDetails.tsx index 3de023d..0d6a06e 100644 --- a/components/collections/creation/CollectionDetails.tsx +++ b/components/collections/creation/CollectionDetails.tsx @@ -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(null) const nameState = useInputState({ @@ -84,23 +88,44 @@ export const CollectionDetails = ({ onChange }: CollectionDetailsProps) => { - - - {coverImage !== null && ( -
- cover-preview + + + {uploadMethod === 'new' && ( + + )} + + {coverImage !== null && uploadMethod === 'new' && ( +
+ no-preview-available
)} + {uploadMethod === 'existing' && coverImageUrl?.includes('ipfs://') && ( +
+ no-preview-available +
+ )} + {uploadMethod === 'existing' && coverImageUrl && !coverImageUrl?.includes('ipfs://') && ( +
+ no-preview-available +
+ )} + {uploadMethod === 'existing' && !coverImageUrl && ( + Waiting for cover image URL to be specified. + )}
+
diff --git a/components/collections/creation/MintingDetails.tsx b/components/collections/creation/MintingDetails.tsx index 328e093..32d6319 100644 --- a/components/collections/creation/MintingDetails.tsx +++ b/components/collections/creation/MintingDetails.tsx @@ -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() const numberOfTokensState = useNumberInputState({ @@ -60,7 +62,12 @@ export const MintingDetails = ({ onChange, numberOfTokens }: MintingDetailsProps return (
- + diff --git a/components/collections/creation/UploadDetails.tsx b/components/collections/creation/UploadDetails.tsx index 233d5f3..65ee304 100644 --- a/components/collections/creation/UploadDetails.tsx +++ b/components/collections/creation/UploadDetails.tsx @@ -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([]) const [metadataFilesArray, setMetadataFilesArray] = useState([]) const [uploadMethod, setUploadMethod] = useState('new') @@ -60,13 +61,21 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { defaultValue: '9d6f42dc01eaab15f52eac8f36cc4f0ee4184944cb3cdbcda229d06ecf877ee7', }) - const handleChangeBaseTokenUri = (event: { target: { value: React.SetStateAction } }) => { - 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 } }) => { - setImage(event.target.value.toString()) - } + const coverImageUrlState = useInputState({ + id: 'coverImageUrl', + name: 'coverImageUrl', + title: 'Cover Image URL', + placeholder: 'ipfs://', + defaultValue: '', + }) const selectAssets = (event: ChangeEvent) => { 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) => {
- {baseTokenURI && ( - - - Base Token URI: {baseTokenURI} - - - )} -
@@ -216,33 +225,10 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { and upload your assets & metadata manually to get a base URI for your collection.

- - +
- - +
diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index b6a283a..ac771e3 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -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(null) const [sg721ContractAddress, setSg721ContractAddress] = useState(null) const [baseTokenUri, setBaseTokenUri] = useState(null) + const [coverImageUrl, setCoverImageUrl] = useState(null) const [transactionHash, setTransactionHash] = useState(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 (
@@ -312,13 +345,26 @@ const CollectionCreationPage: NextPage = () => {
Base Token URI:{' '} - - ipfs://{baseTokenUri as string}/ - + {uploadDetails?.uploadMethod === 'new' && ( + + ipfs://{baseTokenUri as string}/ + + )} + {uploadDetails?.uploadMethod === 'existing' && ( + + ipfs://{baseTokenUri?.substring(baseTokenUri.lastIndexOf('ipfs://') + 7)}/ + + )}
Minter Contract Address:{' '} {
- - + +