// eslint-disable-next-line eslint-comments/disable-enable-pair /* eslint-disable @typescript-eslint/no-loop-func */ import clsx from 'clsx' import { Alert } from 'components/Alert' import { Anchor } from 'components/Anchor' import { AssetsPreview } from 'components/AssetsPreview' import { Conditional } from 'components/Conditional' import { TextInput } from 'components/forms/FormInput' import { useInputState } from 'components/forms/FormInput.hooks' import { MetadataModal } from 'components/MetadataModal' 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' export type UploadMethod = 'new' | 'existing' interface UploadDetailsProps { onChange: (value: UploadDetailsDataProps) => void } export interface UploadDetailsDataProps { assetFiles: File[] metadataFiles: File[] uploadService: UploadServiceType nftStorageApiKey?: string pinataApiKey?: string pinataSecretKey?: string uploadMethod: UploadMethod baseTokenURI?: string imageUrl?: string } export const UploadDetails = ({ onChange }: UploadDetailsProps) => { const [assetFilesArray, setAssetFilesArray] = useState([]) const [metadataFilesArray, setMetadataFilesArray] = useState([]) const [uploadMethod, setUploadMethod] = useState('new') const [uploadService, setUploadService] = useState('nft-storage') const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0) const [refreshMetadata, setRefreshMetadata] = useState(false) const nftStorageApiKeyState = useInputState({ id: 'nft-storage-api-key', name: 'nftStorageApiKey', title: 'NFT.Storage API Key', placeholder: 'Enter NFT.Storage API Key', defaultValue: '', }) const pinataApiKeyState = useInputState({ id: 'pinata-api-key', name: 'pinataApiKey', title: 'Pinata API Key', placeholder: 'Enter Pinata API Key', defaultValue: '', }) const pinataSecretKeyState = useInputState({ id: 'pinata-secret-key', name: 'pinataSecretKey', title: 'Pinata Secret Key', placeholder: 'Enter Pinata Secret Key', defaultValue: '', }) const baseTokenUriState = useInputState({ id: 'baseTokenUri', name: 'baseTokenUri', title: 'Base Token URI', placeholder: 'ipfs://', defaultValue: '', }) const coverImageUrlState = useInputState({ id: 'coverImageUrl', name: 'coverImageUrl', title: 'Cover Image URL', placeholder: 'ipfs://', defaultValue: '', }) const selectAssets = (event: ChangeEvent) => { 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 (isNaN(Number(sortedFileNames[i])) || 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 } } let loadedFileCount = 0 const files: File[] = [] let reader: FileReader for (let i = 0; i < event.target.files.length; i++) { reader = new FileReader() reader.onload = (e) => { if (!e.target?.result) return toast.error('Error parsing file.') if (!event.target.files) return toast.error('No files selected.') const assetFile = new File([e.target.result], event.target.files[i].name, { type: 'image/jpg' }) files.push(assetFile) } reader.readAsArrayBuffer(event.target.files[i]) reader.onloadend = () => { if (!event.target.files) return toast.error('No file selected.') loadedFileCount++ if (loadedFileCount === event.target.files.length) { setAssetFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name))) } } } } const selectMetadata = (event: ChangeEvent) => { 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 (isNaN(Number(sortedFileNames[i])) || 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 } } let loadedFileCount = 0 const files: File[] = [] let reader: FileReader for (let i = 0; i < event.target.files.length; i++) { reader = new FileReader() reader.onload = (e) => { if (!e.target?.result) return toast.error('Error parsing file.') if (!event.target.files) return toast.error('No files selected.') const metadataFile = new File([e.target.result], event.target.files[i].name, { type: 'application/json' }) files.push(metadataFile) } reader.readAsText(event.target.files[i], 'utf8') reader.onloadend = () => { if (!event.target.files) return toast.error('No file selected.') loadedFileCount++ if (loadedFileCount === event.target.files.length) { setMetadataFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name))) } } } } const updateMetadataFileIndex = (index: number) => { setMetadataFileArrayIndex(index) setRefreshMetadata((prev) => !prev) } const updateMetadataFileArray = async (updatedMetadataFile: File) => { metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile console.log('Updated Metadata File:') console.log(JSON.parse(await metadataFilesArray[metadataFileArrayIndex]?.text())) } useEffect(() => { try { const data: UploadDetailsDataProps = { assetFiles: assetFilesArray, metadataFiles: metadataFilesArray, uploadService, nftStorageApiKey: nftStorageApiKeyState.value, pinataApiKey: pinataApiKeyState.value, pinataSecretKey: pinataSecretKeyState.value, uploadMethod, baseTokenURI: baseTokenUriState.value, imageUrl: coverImageUrlState.value, } onChange(data) } catch (error: any) { toast.error(error.message) } }, [ assetFilesArray, metadataFilesArray, uploadService, nftStorageApiKeyState.value, pinataApiKeyState.value, pinataSecretKeyState.value, uploadMethod, baseTokenUriState.value, coverImageUrlState.value, ]) useEffect(() => { setAssetFilesArray([]) setMetadataFilesArray([]) baseTokenUriState.onChange('') coverImageUrlState.onChange('') }, [uploadMethod]) return (
{ setUploadMethod('new') }} type="radio" value="New" />
{ setUploadMethod('existing') }} type="radio" value="Existing" />

Though Stargaze's sg721 contract allows for off-chain metadata storage, it is recommended to use a decentralized storage solution, such as IPFS.
You may head over to{' '} NFT.Storage {' '} or{' '} Pinata {' '} and upload your assets & metadata manually to get a base URI for your collection.

{ setUploadService('nft-storage') }} type="radio" value="nft-storage" />
{ setUploadService('pinata') }} type="radio" value="pinata" />
0 && metadataFilesArray.length > 0 && assetFilesArray.length !== metadataFilesArray.length } > The number of assets and metadata files should match.
{assetFilesArray.length > 0 && (
)}
0}>
) }