379 lines
13 KiB
TypeScript
379 lines
13 KiB
TypeScript
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
/* eslint-disable no-nested-ternary */
|
|
|
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
|
|
import { toUtf8 } from '@cosmjs/encoding'
|
|
import clsx from 'clsx'
|
|
import { Conditional } from 'components/Conditional'
|
|
import { FormControl } from 'components/FormControl'
|
|
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
|
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
|
import { InputDateTime } from 'components/InputDateTime'
|
|
import { useGlobalSettings } from 'contexts/globalSettings'
|
|
import { useWallet } from 'contexts/wallet'
|
|
import type { Trait } from 'contracts/badgeHub'
|
|
import type { ChangeEvent } from 'react'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { toast } from 'react-hot-toast'
|
|
import { BADGE_HUB_ADDRESS } from 'utils/constants'
|
|
|
|
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
|
import { MetadataAttributes } from '../../forms/MetadataAttributes'
|
|
import { Tooltip } from '../../Tooltip'
|
|
import type { MintRule, UploadMethod } from './ImageUploadDetails'
|
|
|
|
interface BadgeDetailsProps {
|
|
onChange: (data: BadgeDetailsDataProps) => void
|
|
uploadMethod: UploadMethod | undefined
|
|
mintRule: MintRule
|
|
metadataSize: number
|
|
}
|
|
|
|
export interface BadgeDetailsDataProps {
|
|
manager: string
|
|
name?: string
|
|
description?: string
|
|
attributes?: Trait[]
|
|
expiry?: number
|
|
transferrable: boolean
|
|
max_supply?: number
|
|
image_data?: string
|
|
external_url?: string
|
|
background_color?: string
|
|
animation_url?: string
|
|
youtube_url?: string
|
|
}
|
|
|
|
export const BadgeDetails = ({ metadataSize, onChange, uploadMethod }: BadgeDetailsProps) => {
|
|
const wallet = useWallet()
|
|
const { timezone } = useGlobalSettings()
|
|
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
|
const [transferrable, setTransferrable] = useState<boolean>(false)
|
|
const [metadataFile, setMetadataFile] = useState<File>()
|
|
const [metadataFeeRate, setMetadataFeeRate] = useState<number>(0)
|
|
|
|
const metadataFileRef = useRef<HTMLInputElement | null>(null)
|
|
|
|
const managerState = useInputState({
|
|
id: 'manager-address',
|
|
name: 'manager',
|
|
title: 'Manager',
|
|
subtitle: 'Badge Hub Manager',
|
|
defaultValue: wallet.address ? wallet.address : '',
|
|
})
|
|
|
|
const nameState = useInputState({
|
|
id: 'name',
|
|
name: 'name',
|
|
title: 'Name',
|
|
placeholder: 'My Awesome Collection',
|
|
})
|
|
|
|
const descriptionState = useInputState({
|
|
id: 'description',
|
|
name: 'description',
|
|
title: 'Description',
|
|
placeholder: 'My Awesome Collection Description',
|
|
})
|
|
|
|
const imageDataState = useInputState({
|
|
id: 'metadata-image-data',
|
|
name: 'metadata-image-data',
|
|
title: 'Image Data',
|
|
subtitle: 'Raw SVG image data',
|
|
})
|
|
|
|
const externalUrlState = useInputState({
|
|
id: 'metadata-external-url',
|
|
name: 'metadata-external-url',
|
|
title: 'External URL',
|
|
subtitle: 'External URL for the badge',
|
|
})
|
|
|
|
const attributesState = useMetadataAttributesState()
|
|
|
|
const maxSupplyState = useNumberInputState({
|
|
id: 'max-supply',
|
|
name: 'max-supply',
|
|
title: 'Max Supply',
|
|
subtitle: 'Maximum number of badges that can be minted',
|
|
})
|
|
|
|
const backgroundColorState = useInputState({
|
|
id: 'metadata-background-color',
|
|
name: 'metadata-background-color',
|
|
title: 'Background Color',
|
|
subtitle: 'Background color of the badge',
|
|
})
|
|
|
|
const animationUrlState = useInputState({
|
|
id: 'metadata-animation-url',
|
|
name: 'metadata-animation-url',
|
|
title: 'Animation URL',
|
|
subtitle: 'Animation URL for the badge',
|
|
})
|
|
|
|
const youtubeUrlState = useInputState({
|
|
id: 'metadata-youtube-url',
|
|
name: 'metadata-youtube-url',
|
|
title: 'YouTube URL',
|
|
subtitle: 'YouTube URL for the badge',
|
|
})
|
|
|
|
const parseMetadata = async () => {
|
|
try {
|
|
let parsedMetadata: any
|
|
if (metadataFile) {
|
|
attributesState.reset()
|
|
parsedMetadata = JSON.parse(await metadataFile.text())
|
|
|
|
if (!parsedMetadata.attributes || parsedMetadata.attributes.length === 0) {
|
|
attributesState.add({
|
|
trait_type: '',
|
|
value: '',
|
|
})
|
|
} else {
|
|
for (let i = 0; i < parsedMetadata.attributes.length; i++) {
|
|
attributesState.add({
|
|
trait_type: parsedMetadata.attributes[i].trait_type,
|
|
value: parsedMetadata.attributes[i].value,
|
|
})
|
|
}
|
|
}
|
|
nameState.onChange(parsedMetadata.name ? parsedMetadata.name : '')
|
|
descriptionState.onChange(parsedMetadata.description ? parsedMetadata.description : '')
|
|
externalUrlState.onChange(parsedMetadata.external_url ? parsedMetadata.external_url : '')
|
|
youtubeUrlState.onChange(parsedMetadata.youtube_url ? parsedMetadata.youtube_url : '')
|
|
animationUrlState.onChange(parsedMetadata.animation_url ? parsedMetadata.animation_url : '')
|
|
backgroundColorState.onChange(parsedMetadata.background_color ? parsedMetadata.background_color : '')
|
|
imageDataState.onChange(parsedMetadata.image_data ? parsedMetadata.image_data : '')
|
|
} else {
|
|
attributesState.reset()
|
|
nameState.onChange('')
|
|
descriptionState.onChange('')
|
|
externalUrlState.onChange('')
|
|
youtubeUrlState.onChange('')
|
|
animationUrlState.onChange('')
|
|
backgroundColorState.onChange('')
|
|
imageDataState.onChange('')
|
|
}
|
|
} catch (error) {
|
|
toast.error('Error parsing metadata file: Invalid JSON format.')
|
|
if (metadataFileRef.current) metadataFileRef.current.value = ''
|
|
setMetadataFile(undefined)
|
|
}
|
|
}
|
|
|
|
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
|
setMetadataFile(undefined)
|
|
if (event.target.files === null) return
|
|
|
|
let selectedFile: File
|
|
const reader = new FileReader()
|
|
reader.onload = (e) => {
|
|
if (!event.target.files) return toast.error('No file selected.')
|
|
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
selectedFile = new File([e.target.result], event.target.files[0].name, { type: 'application/json' })
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
|
else return toast.error('No file selected.')
|
|
reader.onloadend = () => {
|
|
if (!event.target.files) return toast.error('No file selected.')
|
|
setMetadataFile(selectedFile)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void parseMetadata()
|
|
if (!metadataFile)
|
|
attributesState.add({
|
|
trait_type: '',
|
|
value: '',
|
|
})
|
|
}, [metadataFile])
|
|
|
|
useEffect(() => {
|
|
animationUrlState.onChange('')
|
|
}, [uploadMethod])
|
|
|
|
useEffect(() => {
|
|
try {
|
|
const data: BadgeDetailsDataProps = {
|
|
manager: managerState.value,
|
|
name: nameState.value || undefined,
|
|
description: descriptionState.value || undefined,
|
|
attributes:
|
|
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
|
? attributesState.values
|
|
.map((attr) => ({
|
|
trait_type: attr.trait_type,
|
|
value: attr.value,
|
|
}))
|
|
.filter((attr) => attr.trait_type && attr.value)
|
|
: undefined,
|
|
expiry: timestamp ? timestamp.getTime() / 1000 : undefined,
|
|
max_supply: maxSupplyState.value || undefined,
|
|
transferrable,
|
|
image_data: imageDataState.value || undefined,
|
|
external_url: externalUrlState.value || undefined,
|
|
background_color: backgroundColorState.value || undefined,
|
|
animation_url: animationUrlState.value || undefined,
|
|
youtube_url: youtubeUrlState.value || undefined,
|
|
}
|
|
onChange(data)
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
toast.error(error.message, { style: { maxWidth: 'none' } })
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [
|
|
managerState.value,
|
|
nameState.value,
|
|
descriptionState.value,
|
|
timestamp,
|
|
maxSupplyState.value,
|
|
transferrable,
|
|
imageDataState.value,
|
|
externalUrlState.value,
|
|
attributesState.values,
|
|
backgroundColorState.value,
|
|
animationUrlState.value,
|
|
youtubeUrlState.value,
|
|
])
|
|
|
|
useEffect(() => {
|
|
const retrieveFeeRate = async () => {
|
|
try {
|
|
if (wallet.client) {
|
|
const feeRateRaw = await wallet.client.queryContractRaw(
|
|
BADGE_HUB_ADDRESS,
|
|
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
|
|
)
|
|
console.log('Fee Rate Raw: ', feeRateRaw)
|
|
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
|
|
setMetadataFeeRate(Number(feeRate.metadata))
|
|
}
|
|
} catch (error) {
|
|
toast.error('Error retrieving metadata fee rate.')
|
|
setMetadataFeeRate(0)
|
|
console.log('Error retrieving fee rate: ', error)
|
|
}
|
|
}
|
|
void retrieveFeeRate()
|
|
}, [wallet.client])
|
|
|
|
return (
|
|
<div>
|
|
<div className={clsx('grid grid-cols-2 ml-4 max-w-5xl')}>
|
|
<div className={clsx('mt-2')}>
|
|
<AddressInput {...managerState} isRequired />
|
|
<TextInput className="mt-2" {...nameState} />
|
|
<TextInput className="mt-2" {...descriptionState} />
|
|
<NumberInput className="mt-2" {...maxSupplyState} />
|
|
{uploadMethod === 'existing' ? <TextInput className="mt-2" {...animationUrlState} /> : null}
|
|
<TextInput className="mt-2" {...externalUrlState} />
|
|
|
|
<FormControl
|
|
className="mt-2"
|
|
htmlId="expiry-date"
|
|
subtitle={`Badge minting expiry date ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
|
title="Expiry Date"
|
|
>
|
|
<InputDateTime
|
|
minDate={
|
|
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
}
|
|
onChange={(date) =>
|
|
setTimestamp(
|
|
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
|
)
|
|
}
|
|
value={
|
|
timezone === 'Local'
|
|
? timestamp
|
|
: timestamp
|
|
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
: undefined
|
|
}
|
|
/>
|
|
</FormControl>
|
|
<div className="grid grid-cols-2">
|
|
<div className="mt-2 w-1/3 form-control">
|
|
<label className="justify-start cursor-pointer label">
|
|
<span className="mr-4 font-bold">Transferrable</span>
|
|
<input
|
|
checked={transferrable}
|
|
className={`toggle ${transferrable ? `bg-stargaze` : `bg-gray-600`}`}
|
|
onClick={() => setTransferrable(!transferrable)}
|
|
type="checkbox"
|
|
/>
|
|
</label>
|
|
</div>
|
|
<Conditional test={managerState.value !== ''}>
|
|
<Tooltip
|
|
backgroundColor="bg-stargaze"
|
|
className="bg-yellow-600"
|
|
label="This is only an estimate. Be sure to check the final amount before signing the transaction."
|
|
placement="bottom"
|
|
>
|
|
<div className="grid grid-cols-2 ml-12 w-full">
|
|
<div className="mt-4 font-bold">Fee Estimate:</div>
|
|
<span className="mt-4">{(metadataSize * Number(metadataFeeRate)) / 1000000} stars</span>
|
|
</div>
|
|
</Tooltip>
|
|
</Conditional>
|
|
</div>
|
|
</div>
|
|
<div className={clsx('ml-10')}>
|
|
<div>
|
|
<MetadataAttributes
|
|
attributes={attributesState.entries}
|
|
onAdd={attributesState.add}
|
|
onChange={attributesState.update}
|
|
onRemove={attributesState.remove}
|
|
title="Traits"
|
|
/>
|
|
</div>
|
|
<div className="w-full">
|
|
<Tooltip
|
|
backgroundColor="bg-blue-500"
|
|
label="A metadata file can be selected to automatically fill in the related fields."
|
|
placement="bottom"
|
|
>
|
|
<div>
|
|
<label
|
|
className="block mt-2 mr-1 mb-1 w-full font-bold text-white dark:text-gray-300"
|
|
htmlFor="assetFile"
|
|
>
|
|
Metadata File Selection (optional)
|
|
</label>
|
|
<div
|
|
className={clsx(
|
|
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
|
'rounded border-2 border-white/20 border-dashed',
|
|
)}
|
|
>
|
|
<input
|
|
accept="application/json"
|
|
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',
|
|
)}
|
|
id="metadataFile"
|
|
onChange={selectMetadata}
|
|
ref={metadataFileRef}
|
|
type="file"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|