Isolate Base & Vending Minter creation UI

This commit is contained in:
Serkan Reis 2022-12-13 15:52:43 +03:00
parent 637294c9b6
commit 5c6c87eb9e
7 changed files with 521 additions and 45 deletions

View File

@ -46,7 +46,7 @@ export const Sidebar = () => {
!router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1).includes(href) && isChild,
},
{
'text-plumbus': router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1).includes(href) && isChild,
'text-stargaze': router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1).includes(href) && isChild,
}, // active route styling
// { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling
)}

View File

@ -0,0 +1,199 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { toUtf8 } from '@cosmjs/encoding'
import axios from 'axios'
import { useInputState } from 'components/forms/FormInput.hooks'
import { useWallet } from 'contexts/wallet'
import React, { useCallback, useEffect, useState } from 'react'
import { toast } from 'react-hot-toast'
import { API_URL } from 'utils/constants'
import { useDebounce } from '../../../utils/debounce'
import { TextInput } from '../../forms/FormInput'
import type { MinterType } from '../actions/Combobox'
export type MinterAcquisitionMethod = 'existing' | 'new'
export interface MinterInfo {
name: string
minter: string
}
interface MinterDetailsProps {
onChange: (data: MinterDetailsDataProps) => void
minterType: MinterType
}
export interface MinterDetailsDataProps {
minterAcquisitionMethod: MinterAcquisitionMethod
existingMinter: string | undefined
}
export const MinterDetails = ({ onChange, minterType }: MinterDetailsProps) => {
const wallet = useWallet()
const [myBaseMinterContracts, setMyBaseMinterContracts] = useState<MinterInfo[]>([])
const [minterAcquisitionMethod, setMinterAcquisitionMethod] = useState<MinterAcquisitionMethod>('existing')
const existingMinterState = useInputState({
id: 'existingMinter',
name: 'existingMinter',
title: 'Existing Base Minter Contract Address',
subtitle: '',
placeholder: 'stars1...',
})
const fetchMinterContracts = async (): Promise<MinterInfo[]> => {
const contracts: MinterInfo[] = await axios
.get(`${API_URL}/api/v1beta/collections/${wallet.address}`)
.then((response) => {
const collectionData = response.data
const minterContracts = collectionData.map((collection: any) => {
return { name: collection.name, minter: collection.minter }
})
return minterContracts
})
.catch(console.error)
console.log(contracts)
return contracts
}
async function getMinterContractType(minterContractAddress: string) {
if (wallet.client && minterContractAddress.length > 0) {
const client = wallet.client
const data = await client.queryContractRaw(
minterContractAddress,
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()),
)
const contractType: string = JSON.parse(new TextDecoder().decode(data as Uint8Array)).contract
return contractType
}
}
const filterBaseMinterContracts = async () => {
setMyBaseMinterContracts([])
await fetchMinterContracts()
.then((minterContracts) =>
minterContracts.map(async (minterContract: any) => {
await getMinterContractType(minterContract.minter)
.then((contractType) => {
if (contractType?.includes('sg-minter')) {
setMyBaseMinterContracts((prevState) => [...prevState, minterContract])
}
})
.catch((err) => {
console.log(err)
console.log('Unable to retrieve contract type')
})
}),
)
.catch((err) => {
console.log(err)
console.log('Unable to fetch base minter contracts')
})
}
const debouncedMyBaseMinterContracts = useDebounce(myBaseMinterContracts, 500)
const renderMinterContracts = useCallback(() => {
return myBaseMinterContracts.map((minterContract, index) => {
return (
<option key={index} className="mt-2 text-lg bg-[#1A1A1A]">
{`${minterContract.name} - ${minterContract.minter}`}
</option>
)
})
}, [debouncedMyBaseMinterContracts])
const debouncedWalletAddress = useDebounce(wallet.address, 500)
const displayToast = async () => {
await toast.promise(filterBaseMinterContracts(), {
loading: 'Fetching Base Minter contracts...',
success: 'Base Minter contracts retrieved.',
error: 'Unable to retrieve Base Minter contracts.',
})
}
useEffect(() => {
if (debouncedWalletAddress && minterAcquisitionMethod === 'existing') {
void displayToast()
}
}, [debouncedWalletAddress, minterAcquisitionMethod])
useEffect(() => {
const data: MinterDetailsDataProps = {
minterAcquisitionMethod,
existingMinter: existingMinterState.value,
}
onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [existingMinterState.value, minterAcquisitionMethod])
return (
<div className="mx-10 mb-4 rounded border-2 border-white/20">
<div className="flex justify-center mb-2">
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
<input
checked={minterAcquisitionMethod === 'new'}
className="peer sr-only"
id="inlineRadio5"
name="inlineRadioOptions5"
onClick={() => {
setMinterAcquisitionMethod('new')
}}
type="radio"
value="New"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio5"
>
Create a New Base Minter Contract
</label>
</div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
<input
checked={minterAcquisitionMethod === 'existing'}
className="peer sr-only"
id="inlineRadio6"
name="inlineRadioOptions6"
onClick={() => {
setMinterAcquisitionMethod('existing')
}}
type="radio"
value="Existing"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio6"
>
Use an Existing Base Minter Contract
</label>
</div>
</div>
{minterAcquisitionMethod === 'existing' && (
<div>
<div className="grid grid-cols-2 grid-flow-col my-4 mx-10">
<select
className="mt-8 w-full max-w-lg text-sm bg-white/10 select select-bordered"
onChange={(e) => {
existingMinterState.onChange(e.target.value.slice(e.target.value.indexOf('-') + 2))
e.preventDefault()
}}
>
<option className="mt-2 text-lg bg-[#1A1A1A]" disabled selected>
Select a Base Minter Contract
</option>
{renderMinterContracts()}
</select>
<TextInput defaultValue={existingMinterState.value} {...existingMinterState} isRequired />
</div>
</div>
)}
</div>
)
}

View File

@ -233,7 +233,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
}, [uploadMethod])
return (
<div className="justify-items-start mt-5 mb-3 rounded border border-2 border-white/20 flex-column">
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
<div className="flex justify-center">
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
<input

View File

@ -14,7 +14,7 @@ const nextConfig = {
NEXT_PUBLIC_WEBSITE_URL:
process.env.NODE_ENV === 'development' ? LOCALHOST_URL : process.env.NEXT_PUBLIC_WEBSITE_URL,
},
reactStrictMode: true,
reactStrictMode: false,
trailingSlash: true,
webpack(config, { dev, webpack }) {
// svgr integration

View File

@ -39,7 +39,8 @@
"react-datetime-picker": "^3",
"react-dom": "^18",
"react-hook-form": "^7",
"react-hot-toast": "^2",
"react-hot-toast": "2.4.0",
"react-toastify": "9.1.1",
"react-icons": "^4",
"react-popper": "^2",
"react-query": "^3",

View File

@ -4,6 +4,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { coin } from '@cosmjs/proto-signing'
import clsx from 'clsx'
import { Alert } from 'components/Alert'
import { Anchor } from 'components/Anchor'
import { Button } from 'components/Button'
@ -15,6 +16,8 @@ import {
WhitelistDetails,
} from 'components/collections/creation'
import type { CollectionDetailsDataProps } from 'components/collections/creation/CollectionDetails'
import type { MinterDetailsDataProps } from 'components/collections/creation/MinterDetails'
import { MinterDetails } from 'components/collections/creation/MinterDetails'
import type { MintingDetailsDataProps } from 'components/collections/creation/MintingDetails'
import type { RoyaltyDetailsDataProps } from 'components/collections/creation/RoyaltyDetails'
import type { UploadDetailsDataProps } from 'components/collections/creation/UploadDetails'
@ -42,6 +45,7 @@ import {
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import type { MinterType } from '../../components/collections/actions/Combobox'
import type { UploadMethod } from '../../components/collections/creation/UploadDetails'
import { ConfirmationModal } from '../../components/ConfirmationModal'
import { getAssetType } from '../../utils/getAssetType'
@ -62,13 +66,17 @@ const CollectionCreationPage: NextPage = () => {
const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null)
const [collectionDetails, setCollectionDetails] = useState<CollectionDetailsDataProps | null>(null)
const [minterDetails, setMinterDetails] = useState<MinterDetailsDataProps | null>(null)
const [mintingDetails, setMintingDetails] = useState<MintingDetailsDataProps | null>(null)
const [whitelistDetails, setWhitelistDetails] = useState<WhitelistDetailsDataProps | null>(null)
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
const [minterType, setMinterType] = useState<MinterType>('vending')
const [uploading, setUploading] = useState(false)
const [creatingCollection, setCreatingCollection] = useState(false)
const [readyToCreate, setReadyToCreate] = useState(false)
const [readyToCreateVm, setReadyToCreateVm] = useState(false)
const [readyToCreateBm, setReadyToCreateBm] = useState(false)
const [readyToUploadAndMint, setReadyToUploadAndMint] = useState(false)
const [vendingMinterContractAddress, setVendingMinterContractAddress] = useState<string | null>(null)
const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null)
const [whitelistContractAddress, setWhitelistContractAddress] = useState<string | null | undefined>(null)
@ -76,20 +84,20 @@ const CollectionCreationPage: NextPage = () => {
const [coverImageUrl, setCoverImageUrl] = useState<string | null>(null)
const [transactionHash, setTransactionHash] = useState<string | null>(null)
const performChecks = () => {
const performVendingMinterChecks = () => {
try {
setReadyToCreate(false)
setReadyToCreateVm(false)
checkUploadDetails()
checkCollectionDetails()
checkMintingDetails()
checkRoyaltyDetails()
checkWhitelistDetails()
.then(() => {
setReadyToCreate(true)
setReadyToCreateVm(true)
})
.catch((err) => {
toast.error(`Error in Whitelist Configuration: ${err.message}`, { style: { maxWidth: 'none' } })
setReadyToCreate(false)
setReadyToCreateVm(false)
})
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
@ -97,7 +105,157 @@ const CollectionCreationPage: NextPage = () => {
}
}
const createCollection = async () => {
const performBaseMinterChecks = () => {
try {
setReadyToCreateBm(false)
checkUploadDetails()
checkCollectionDetails()
checkMintingDetails()
checkRoyaltyDetails()
checkWhitelistDetails()
.then(() => {
setReadyToCreateBm(true)
})
.catch((err) => {
toast.error(`Error in Whitelist Configuration: ${err.message}`, { style: { maxWidth: 'none' } })
setReadyToCreateBm(false)
})
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
setUploading(false)
}
}
const performUploadAndMintChecks = () => {
try {
setReadyToUploadAndMint(false)
checkUploadDetails()
checkCollectionDetails()
checkMintingDetails()
checkRoyaltyDetails()
checkWhitelistDetails()
.then(() => {
setReadyToUploadAndMint(true)
})
.catch((err) => {
toast.error(`Error in Whitelist Configuration: ${err.message}`, { style: { maxWidth: 'none' } })
setReadyToUploadAndMint(false)
})
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
setUploading(false)
}
}
const createVendingMinterCollection = async () => {
try {
setCreatingCollection(true)
setBaseTokenUri(null)
setCoverImageUrl(null)
setVendingMinterContractAddress(null)
setSg721ContractAddress(null)
setWhitelistContractAddress(null)
setTransactionHash(null)
if (uploadDetails?.uploadMethod === 'new') {
setUploading(true)
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()
setWhitelistContractAddress(whitelist as string)
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()
setWhitelistContractAddress(whitelist as string)
await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist)
}
setCreatingCollection(false)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
setCreatingCollection(false)
setUploading(false)
}
}
const createBaseMinterCollection = async () => {
try {
setCreatingCollection(true)
setBaseTokenUri(null)
setCoverImageUrl(null)
setVendingMinterContractAddress(null)
setSg721ContractAddress(null)
setWhitelistContractAddress(null)
setTransactionHash(null)
if (uploadDetails?.uploadMethod === 'new') {
setUploading(true)
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()
setWhitelistContractAddress(whitelist as string)
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()
setWhitelistContractAddress(whitelist as string)
await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist)
}
setCreatingCollection(false)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
setCreatingCollection(false)
setUploading(false)
}
}
const uploadAndMint = async () => {
try {
setCreatingCollection(true)
setBaseTokenUri(null)
@ -513,36 +671,142 @@ const CollectionCreationPage: NextPage = () => {
</Alert>
</Conditional>
</div>
<div>
<div
className={clsx(
'mx-10 mt-5',
'grid before:absolute relative grid-cols-2 grid-flow-col items-stretch rounded',
'before:inset-x-0 before:bottom-0 before:border-white/25',
)}
>
<div
className={clsx(
'isolate space-y-1 border-2',
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
minterType === 'vending' ? 'border-stargaze' : 'border-transparent',
minterType !== 'vending' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
)}
>
<button
className="p-4 w-full h-full text-left bg-transparent"
onClick={() => setMinterType('vending')}
type="button"
>
<h4 className="font-bold">Vending Minter</h4>
<span className="text-sm text-white/80 line-clamp-2">
Vending Minter contract facilitates primary market vending machine style minting
</span>
</button>
</div>
<div
className={clsx(
'isolate space-y-1 border-2',
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
minterType === 'base' ? 'border-stargaze' : 'border-transparent',
minterType !== 'base' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
)}
>
<button
className="p-4 w-full h-full text-left bg-transparent"
onClick={() => setMinterType('base')}
type="button"
>
<h4 className="font-bold">Base Minter</h4>
<span className="text-sm text-white/80 line-clamp-2">Base Minter contract enables 1/1 minting</span>
</button>
</div>
</div>
</div>
{minterType === 'base' && (
<div>
<MinterDetails minterType={minterType} onChange={setMinterDetails} />
</div>
)}
<div className="mx-10">
<UploadDetails onChange={setUploadDetails} />
<Conditional
test={minterType === 'vending' || (minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'new')}
>
<div className="flex justify-between py-3 px-8 rounded border-2 border-white/20 grid-col-2">
<Conditional
test={
minterType === 'vending' || (minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'new')
}
>
<CollectionDetails
coverImageUrl={coverImageUrl as string}
onChange={setCollectionDetails}
uploadMethod={uploadDetails?.uploadMethod as UploadMethod}
/>
</Conditional>
<Conditional test={minterType === 'vending'}>
<MintingDetails
numberOfTokens={uploadDetails?.assetFiles.length}
onChange={setMintingDetails}
uploadMethod={uploadDetails?.uploadMethod as UploadMethod}
/>
</Conditional>
</div>
</Conditional>
<Conditional
test={minterType === 'vending' || (minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'new')}
>
<div className="my-6">
<Conditional test={minterType === 'vending'}>
<WhitelistDetails onChange={setWhitelistDetails} />
<div className="my-6" />
</Conditional>
<RoyaltyDetails onChange={setRoyaltyDetails} />
</div>
{readyToCreate && <ConfirmationModal confirm={createCollection} />}
</Conditional>
<Conditional test={readyToCreateVm && minterType === 'vending'}>
<ConfirmationModal confirm={createVendingMinterCollection} />
</Conditional>
<Conditional
test={readyToCreateBm && minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'new'}
>
<ConfirmationModal confirm={createBaseMinterCollection} />
</Conditional>
<Conditional
test={readyToUploadAndMint && minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'existing'}
>
<ConfirmationModal confirm={uploadAndMint} />
</Conditional>
<div className="flex justify-end w-full">
<Conditional test={minterType === 'vending'}>
<Button
className="relative justify-center p-2 mb-6 max-h-12 text-white bg-plumbus hover:bg-plumbus-light border-0"
isLoading={creatingCollection}
onClick={performChecks}
onClick={performVendingMinterChecks}
variant="solid"
>
Create Collection
</Button>
</Conditional>
<Conditional test={minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'new'}>
<Button
className="relative justify-center p-2 mb-6 max-h-12 text-white bg-plumbus hover:bg-plumbus-light border-0"
isLoading={creatingCollection}
onClick={performBaseMinterChecks}
variant="solid"
>
Create BM Collection
</Button>
</Conditional>
<Conditional test={minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'existing'}>
<Button
className="relative justify-center p-2 mb-6 max-h-12 text-white bg-plumbus hover:bg-plumbus-light border-0"
isLoading={creatingCollection}
onClick={performUploadAndMintChecks}
variant="solid"
>
Upload & Mint Token
</Button>
</Conditional>
</div>
</div>
</div>

View File

@ -3704,6 +3704,11 @@ clsx@^1:
resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
clsx@^1.1.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz"
@ -4922,10 +4927,10 @@ globby@^11.0.4:
merge2 "^1.4.1"
slash "^3.0.0"
goober@^2.1.1:
version "2.1.9"
resolved "https://registry.npmjs.org/goober/-/goober-2.1.9.tgz"
integrity sha512-PAtnJbrWtHbfpJUIveG5PJIB6Mc9Kd0gimu9wZwPyA+wQUSeOeA4x4Ug16lyaaUUKZ/G6QEH1xunKOuXP1F4Vw==
goober@^2.1.10:
version "2.1.11"
resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.11.tgz#bbd71f90d2df725397340f808dbe7acc3118e610"
integrity sha512-5SS2lmxbhqH0u9ABEWq7WPU69a4i2pYcHeCxqaNq6Cw3mnrF0ghWNM4tEGid4dKy8XNIAUbuThuozDHHKJVh3A==
hamt-sharding@^2.0.0:
version "2.0.1"
@ -6806,12 +6811,12 @@ react-hook-form@^7:
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.30.0.tgz"
integrity sha512-DzjiM6o2vtDGNMB9I4yCqW8J21P314SboNG1O0obROkbg7KVS0I7bMtwSdKyapnCPjHgnxc3L7E5PEdISeEUcQ==
react-hot-toast@^2:
version "2.2.0"
resolved "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.2.0.tgz"
integrity sha512-248rXw13uhf/6TNDVzagX+y7R8J183rp7MwUMNkcrBRyHj/jWOggfXTGlM8zAOuh701WyVW+eUaWG2LeSufX9g==
react-hot-toast@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.0.tgz#b91e7a4c1b6e3068fc599d3d83b4fb48668ae51d"
integrity sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA==
dependencies:
goober "^2.1.1"
goober "^2.1.10"
react-icons@^4:
version "4.3.1"
@ -6862,6 +6867,13 @@ react-time-picker@^4.5.0:
react-fit "^1.4.0"
update-input-width "^1.2.2"
react-toastify@9.1.1:
version "9.1.1"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.1.tgz#9280caea4a13dc1739c350d90660a630807bf10b"
integrity sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==
dependencies:
clsx "^1.1.1"
react-tracked@^1:
version "1.7.9"
resolved "https://registry.npmjs.org/react-tracked/-/react-tracked-1.7.9.tgz"