Merge pull request #17 from public-awesome/develop

deploy to main
This commit is contained in:
Jorge Hernandez 2022-09-23 08:33:24 -06:00 committed by GitHub
commit 9c664a4eb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 746 additions and 241 deletions

View File

@ -8,6 +8,7 @@ NEXT_PUBLIC_SG721_CODE_ID=1
NEXT_PUBLIC_API_URL=https://
NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze
NEXT_PUBLIC_NETWORK=testnet
NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev
NEXT_PUBLIC_WEBSITE_URL=https://
NEXT_PUBLIC_S3_BUCKET= # TODO

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @orkunkl @findolor @kaymakf
* @findolor @MightOfOaks @name-user1

View File

@ -25,7 +25,7 @@ export const Button = (props: ButtonProps) => {
'bg-plumbus hover:bg-plumbus-light rounded ': variant === 'solid',
'bg-plumbus hover:bg-plumbus-light rounded border border-plumbus-dark': variant === 'outline',
'opacity-50 cursor-not-allowed pointer-events-none': isDisabled,
'animate-pulse cursor-wait pointer-events-none': isLoading,
'pl-2 animate-pulse cursor-wait pointer-events-none': isLoading,
},
className,
)}

View File

@ -28,7 +28,7 @@ export const Layout = ({ children, metadata = {} }: LayoutProps) => {
</div> */}
{/* actual layout container */}
<div className="hidden sm:flex">
<div className="hidden bg-black sm:flex">
<Sidebar />
<div className="overflow-auto relative flex-grow h-screen no-scrollbar">
<main

View File

@ -9,9 +9,14 @@ import { SidebarLayout } from './SidebarLayout'
import { WalletLoader } from './WalletLoader'
const routes = [
{ text: 'Create Collection', href: `/collections/create/` },
{ text: 'Collections', href: `/collections/` },
{ text: 'Contract Dashboards', href: `/contracts/` },
{ text: 'Collections', href: `/collections/`, isChild: false },
{ text: 'Create a Collection', href: `/collections/create/`, isChild: true },
{ text: 'My Collections', href: `/collections/myCollections/`, isChild: true },
{ text: 'Collection Actions', href: `/collections/actions/`, isChild: true },
{ text: 'Contract Dashboards', href: `/contracts/`, isChild: false },
{ text: 'Minter Contract', href: `/contracts/minter/`, isChild: true },
{ text: 'SG721 Contract', href: `/contracts/sg721/`, isChild: true },
{ text: 'Whitelist Contract', href: `/contracts/whitelist/`, isChild: true },
]
export const Sidebar = () => {
@ -22,20 +27,24 @@ export const Sidebar = () => {
<SidebarLayout>
{/* Stargaze brand as home button */}
<Anchor href="/" onContextMenu={(e) => [e.preventDefault(), router.push('/brand')]}>
<img alt="Brand Text" className="w-full" src="/stargaze-text.png" />
<img alt="Brand Text" className="w-full" src="/stargaze_logo_800.svg" />
</Anchor>
{/* wallet button */}
<WalletLoader />
{/* main navigation routes */}
{routes.map(({ text, href }) => (
{routes.map(({ text, href, isChild }) => (
<Anchor
key={href}
className={clsx(
'py-2 px-4 -mx-4 uppercase', // styling
'px-4 -mx-5 font-extrabold uppercase rounded-lg', // styling
'hover:bg-white/5 transition-colors', // hover styling
{ 'font-bold text-plumbus': router.asPath === href }, // active route styling
{ 'py-0 ml-2 text-sm font-bold': isChild },
{
'text-gray hover:text-white':
router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1) !== href && isChild,
},
{ 'text-plumbus': router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1) === href && isChild }, // active route styling
// { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling
)}
href={href}

View File

@ -0,0 +1,188 @@
import { Button } from 'components/Button'
import type { DispatchExecuteArgs } from 'components/collections/actions/actions'
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/collections/actions/actions'
import { ActionsCombobox } from 'components/collections/actions/Combobox'
import { useActionsComboboxState } from 'components/collections/actions/Combobox.hooks'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview'
import { TransactionHash } from 'components/TransactionHash'
import { WhitelistUpload } from 'components/WhitelistUpload'
import { useWallet } from 'contexts/wallet'
import type { MinterInstance } from 'contracts/minter'
import type { SG721Instance } from 'contracts/sg721'
import type { FormEvent } from 'react'
import { useState } from 'react'
import { toast } from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa'
import { useMutation } from 'react-query'
import { TextInput } from '../../forms/FormInput'
interface CollectionActionsProps {
minterContractAddress: string
sg721ContractAddress: string
sg721Messages: SG721Instance | undefined
minterMessages: MinterInstance | undefined
}
export const CollectionActions = ({
sg721ContractAddress,
sg721Messages,
minterContractAddress,
minterMessages,
}: CollectionActionsProps) => {
const wallet = useWallet()
const [lastTx, setLastTx] = useState('')
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [airdropArray, setAirdropArray] = useState<string[]>([])
const actionComboboxState = useActionsComboboxState()
const type = actionComboboxState.value?.id
const limitState = useNumberInputState({
id: 'per-address-limi',
name: 'perAddressLimit',
title: 'Per Address Limit',
subtitle: 'Enter the per address limit',
})
const tokenIdState = useNumberInputState({
id: 'token-id',
name: 'tokenId',
title: 'Token ID',
subtitle: 'Enter the token ID',
})
const batchNumberState = useNumberInputState({
id: 'batch-number',
name: 'batchNumber',
title: 'Number of Tokens',
subtitle: 'Enter the number of tokens to mint',
})
const tokenIdListState = useInputState({
id: 'token-id-list',
name: 'tokenIdList',
title: 'List of token IDs',
subtitle:
'Specify individual token IDs separated by commas (e.g., 2, 4, 8) or a range of IDs separated by a colon (e.g., 8:13)',
})
const recipientState = useInputState({
id: 'recipient-address',
name: 'recipient',
title: 'Recipient Address',
subtitle: 'Address of the recipient',
})
const whitelistState = useInputState({
id: 'whitelist-address',
name: 'whitelistAddress',
title: 'Whitelist Address',
subtitle: 'Address of the whitelist contract',
})
const showWhitelistField = type === 'set_whitelist'
const showDateField = type === 'update_start_time'
const showLimitField = type === 'update_per_address_limit'
const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn'])
const showNumberOfTokensField = type === 'batch_mint'
const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer'])
const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer'])
const showAirdropFileField = type === 'airdrop'
const payload: DispatchExecuteArgs = {
whitelist: whitelistState.value,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
limit: limitState.value,
minterContract: minterContractAddress,
sg721Contract: sg721ContractAddress,
tokenId: tokenIdState.value,
tokenIds: tokenIdListState.value,
batchNumber: batchNumberState.value,
minterMessages,
sg721Messages,
recipient: recipientState.value,
recipients: airdropArray,
txSigner: wallet.address,
type,
}
const { isLoading, mutate } = useMutation(
async (event: FormEvent) => {
event.preventDefault()
if (!type) {
throw new Error('Please select an action!')
}
if (minterContractAddress === '' && sg721ContractAddress === '') {
throw new Error('Please enter minter and sg721 contract addresses!')
}
const txHash = await toast.promise(dispatchExecute(payload), {
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
loading: 'Executing message...',
success: (tx) => `Transaction ${tx} success!`,
})
if (txHash) {
setLastTx(txHash)
}
},
{
onError: (error) => {
toast.error(String(error))
},
},
)
const airdropFileOnChange = (data: string[]) => {
setAirdropArray(data)
}
return (
<form>
<div className="grid grid-cols-2 mt-4">
<div className="mr-2">
<ActionsCombobox {...actionComboboxState} />
{showRecipientField && <AddressInput {...recipientState} />}
{showWhitelistField && <AddressInput {...whitelistState} />}
{showLimitField && <NumberInput {...limitState} />}
{showTokenIdField && <NumberInput {...tokenIdState} />}
{showTokenIdListField && <TextInput {...tokenIdListState} />}
{showNumberOfTokensField && <NumberInput {...batchNumberState} />}
{showAirdropFileField && (
<FormGroup subtitle="TXT file that contains the airdrop addresses" title="Airdrop File">
<WhitelistUpload onChange={airdropFileOnChange} />
</FormGroup>
)}
<Conditional test={showDateField}>
<FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</FormControl>
</Conditional>
</div>
<div className="-mt-6">
<div className="relative mb-2">
<Button
className="absolute top-0 right-0"
isLoading={isLoading}
onClick={mutate}
rightIcon={<FaArrowRight />}
>
Execute
</Button>
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
<TransactionHash hash={lastTx} />
</FormControl>
</div>
<FormControl subtitle="View current message to be sent" title="Payload Preview">
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
</FormControl>
</div>
</div>
</form>
)
}

View File

@ -26,7 +26,7 @@ export const ActionsCombobox = ({ value, onChange }: ActionsComboboxProps) => {
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Collection actions"
title="Action"
title=""
value={value}
>
<div className="relative">

View File

@ -52,7 +52,7 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
<div className="ml-4 font-bold form-check form-check-inline">
<input
checked={royaltyState === 'none'}
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
className="peer sr-only"
id="royaltyRadio1"
name="royaltyRadioOptions1"
onClick={() => {
@ -61,14 +61,17 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
type="radio"
value="None"
/>
<label className="inline-block text-white cursor-pointer form-check-label" htmlFor="royaltyRadio1">
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="royaltyRadio1"
>
No royalty
</label>
</div>
<div className="ml-4 font-bold form-check form-check-inline">
<input
checked={royaltyState === 'new'}
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
className="peer sr-only"
id="royaltyRadio2"
name="royaltyRadioOptions2"
onClick={() => {
@ -77,7 +80,10 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
type="radio"
value="Existing"
/>
<label className="inline-block text-white cursor-pointer form-check-label" htmlFor="royaltyRadio2">
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="royaltyRadio2"
>
Configure royalty details
</label>
</div>

View File

@ -41,24 +41,23 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key',
name: 'nftStorageApiKey',
title: 'NFT Storage API Key',
placeholder: '...',
defaultValue:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDJBODk5OGI4ZkE2YTM1NzMyYmMxQTRDQzNhOUU2M0Y2NUM3ZjA1RWIiLCJpc3MiOiJuZnQtc3RvcmFnZSIsImlhdCI6MTY1NTE5MTcwNDQ2MiwibmFtZSI6IlRlc3QifQ.IbdV_26bkPHSdd81sxox5AoG-5a4CCEY4aCrdbCXwAE',
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: '...',
defaultValue: 'c8c2ea440c09ee8fa639',
placeholder: 'Enter Pinata API Key',
defaultValue: '',
})
const pinataSecretKeyState = useInputState({
id: 'pinata-secret-key',
name: 'pinataSecretKey',
title: 'Pinata Secret Key',
placeholder: '...',
defaultValue: '9d6f42dc01eaab15f52eac8f36cc4f0ee4184944cb3cdbcda229d06ecf877ee7',
placeholder: 'Enter Pinata Secret Key',
defaultValue: '',
})
const baseTokenUriState = useInputState({
@ -86,7 +85,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
//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 (parseInt(sortedFileNames[i]) !== i + 1) {
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 = ''
@ -125,7 +124,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
//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 (parseInt(sortedFileNames[i]) !== i + 1) {
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 = ''
@ -202,27 +201,10 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
return (
<div className="justify-items-start mt-5 mb-3 rounded border 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
checked={uploadMethod === 'existing'}
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
id="inlineRadio1"
name="inlineRadioOptions1"
onClick={() => {
setUploadMethod('existing')
}}
type="radio"
value="Existing"
/>
<label className="inline-block text-white cursor-pointer form-check-label" htmlFor="inlineRadio1">
Use an existing base URI
</label>
</div>
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
<input
checked={uploadMethod === 'new'}
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
className="peer sr-only"
id="inlineRadio2"
name="inlineRadioOptions2"
onClick={() => {
@ -231,10 +213,32 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
type="radio"
value="New"
/>
<label className="inline-block text-white cursor-pointer form-check-label" htmlFor="inlineRadio2">
<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="inlineRadio2"
>
Upload assets & metadata
</label>
</div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
<input
checked={uploadMethod === 'existing'}
className="peer sr-only"
id="inlineRadio1"
name="inlineRadioOptions1"
onClick={() => {
setUploadMethod('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="inlineRadio1"
>
Use an existing base URI
</label>
</div>
</div>
<div className="p-3 py-5 pb-8">
@ -244,7 +248,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
Though Stargaze&apos;s sg721 contract allows for off-chain metadata storage, it is recommended to use a
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
NFT Storage
NFT.Storage
</Anchor>{' '}
or{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
@ -267,7 +271,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
<div className="form-check form-check-inline">
<input
checked={uploadService === 'nft-storage'}
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
className="peer sr-only"
id="inlineRadio3"
name="inlineRadioOptions3"
onClick={() => {
@ -276,15 +280,18 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
type="radio"
value="nft-storage"
/>
<label className="inline-block text-white cursor-pointer form-check-label" htmlFor="inlineRadio3">
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio3"
>
Upload using NFT.Storage
</label>
</div>
<div className="ml-4 form-check form-check-inline">
<div className="ml-2 form-check form-check-inline">
<input
checked={uploadService === 'pinata'}
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
className="peer sr-only"
id="inlineRadio4"
name="inlineRadioOptions4"
onClick={() => {
@ -293,7 +300,10 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
type="radio"
value="pinata"
/>
<label className="inline-block text-white cursor-pointer form-check-label" htmlFor="inlineRadio4">
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio4"
>
Upload using Pinata
</label>
</div>

View File

@ -97,7 +97,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
<div className="ml-4 font-bold form-check form-check-inline">
<input
checked={whitelistState === 'none'}
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
className="peer sr-only"
id="whitelistRadio1"
name="whitelistRadioOptions1"
onClick={() => {
@ -106,14 +106,17 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
type="radio"
value="None"
/>
<label className="inline-block text-white cursor-pointer form-check-label" htmlFor="whitelistRadio1">
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="whitelistRadio1"
>
No whitelist
</label>
</div>
<div className="ml-4 font-bold form-check form-check-inline">
<input
checked={whitelistState === 'existing'}
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
className="peer sr-only"
id="whitelistRadio2"
name="whitelistRadioOptions2"
onClick={() => {
@ -122,14 +125,17 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
type="radio"
value="Existing"
/>
<label className="inline-block text-white cursor-pointer form-check-label" htmlFor="whitelistRadio2">
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="whitelistRadio2"
>
Existing whitelist
</label>
</div>
<div className="ml-4 font-bold form-check form-check-inline">
<input
checked={whitelistState === 'new'}
className="float-none mr-2 mb-1 w-4 h-4 align-middle bg-white checked:bg-stargaze bg-center bg-no-repeat bg-contain rounded-full border border-gray-300 checked:border-white focus:outline-none transition duration-200 appearance-none cursor-pointer form-check-input"
className="peer sr-only"
id="whitelistRadio3"
name="whitelistRadioOptions3"
onClick={() => {
@ -138,7 +144,10 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
type="radio"
value="New"
/>
<label className="inline-block text-white cursor-pointer form-check-label" htmlFor="whitelistRadio3">
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="whitelistRadio3"
>
New whitelist
</label>
</div>

View File

@ -25,7 +25,7 @@ export const QueryCombobox = ({ value, onChange }: QueryComboboxProps) => {
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Collection queries"
title="Query"
title=""
value={value}
>
<div className="relative">

View File

@ -0,0 +1,85 @@
import { QueryCombobox } from 'components/collections/queries/Combobox'
import { useQueryComboboxState } from 'components/collections/queries/Combobox.hooks'
import { dispatchQuery } from 'components/collections/queries/query'
import { FormControl } from 'components/FormControl'
import { AddressInput, TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks'
import { JsonPreview } from 'components/JsonPreview'
import type { MinterInstance } from 'contracts/minter'
import type { SG721Instance } from 'contracts/sg721'
import { toast } from 'react-hot-toast'
import { useQuery } from 'react-query'
interface CollectionQueriesProps {
minterContractAddress: string
sg721ContractAddress: string
sg721Messages: SG721Instance | undefined
minterMessages: MinterInstance | undefined
}
export const CollectionQueries = ({
sg721ContractAddress,
sg721Messages,
minterContractAddress,
minterMessages,
}: CollectionQueriesProps) => {
const comboboxState = useQueryComboboxState()
const type = comboboxState.value?.id
const tokenIdState = useInputState({
id: 'token-id',
name: 'tokenId',
title: 'Token ID',
subtitle: 'Enter the token ID',
placeholder: '1',
})
const tokenId = tokenIdState.value
const addressState = useInputState({
id: 'address',
name: 'address',
title: 'User Address',
subtitle: 'Address of the user',
})
const address = addressState.value
const showTokenIdField = type === 'token_info'
const showAddressField = type === 'tokens_minted_to_user'
const { data: response } = useQuery(
[sg721Messages, minterMessages, type, tokenId, address] as const,
async ({ queryKey }) => {
const [_sg721Messages, _minterMessages, _type, _tokenId, _address] = queryKey
const result = await dispatchQuery({
tokenId: _tokenId,
minterMessages: _minterMessages,
sg721Messages: _sg721Messages,
address: _address,
type: _type,
})
return result
},
{
placeholderData: null,
onError: (error: any) => {
toast.error(error.message)
},
enabled: Boolean(sg721ContractAddress && minterContractAddress && type),
retry: false,
},
)
return (
<div className="grid grid-cols-2 mt-4">
<div className="mr-2 space-y-8">
<QueryCombobox {...comboboxState} />
{showAddressField && <AddressInput {...addressState} />}
{showTokenIdField && <TextInput {...tokenIdState} />}
</div>
<div className="space-y-8">
<FormControl title="Query Response">
<JsonPreview content={response || {}} isCopyable />
</FormControl>
</div>
</div>
)
}

18
env. Normal file
View File

@ -0,0 +1,18 @@
APP_VERSION=0.1.0
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
NEXT_PUBLIC_WHITELIST_CODE_ID=3
NEXT_PUBLIC_MINTER_CODE_ID=2
NEXT_PUBLIC_SG721_CODE_ID=1
NEXT_PUBLIC_API_URL=https://
NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze
NEXT_PUBLIC_NETWORK=testnet
NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev
NEXT_PUBLIC_WEBSITE_URL=https://
NEXT_PUBLIC_S3_BUCKET= # TODO
NEXT_PUBLIC_S3_ENDPOINT= # TODO
NEXT_PUBLIC_S3_KEY= # TODO
NEXT_PUBLIC_S3_REGION= # TODO
NEXT_PUBLIC_S3_SECRET= # TODO

1
env.d.ts vendored
View File

@ -22,6 +22,7 @@ declare namespace NodeJS {
readonly NEXT_PUBLIC_API_URL: string
readonly NEXT_PUBLIC_BLOCK_EXPLORER_URL: string
readonly NEXT_PUBLIC_NETWORK: string
readonly NEXT_PUBLIC_STARGAZE_WEBSITE_URL: string
readonly NEXT_PUBLIC_WEBSITE_URL: string
}
}

View File

@ -1,42 +1,22 @@
import { Button } from 'components/Button'
import type { DispatchExecuteArgs } from 'components/collections/actions/actions'
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/collections/actions/actions'
import { ActionsCombobox } from 'components/collections/actions/Combobox'
import { useActionsComboboxState } from 'components/collections/actions/Combobox.hooks'
import { Conditional } from 'components/Conditional'
import { CollectionActions } from 'components/collections/actions/Action'
import { CollectionQueries } from 'components/collections/queries/Queries'
import { ContractPageHeader } from 'components/ContractPageHeader'
import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview'
import { TransactionHash } from 'components/TransactionHash'
import { WhitelistUpload } from 'components/WhitelistUpload'
import { AddressInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks'
import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { NextSeo } from 'next-seo'
import type { FormEvent } from 'react'
import { useMemo, useState } from 'react'
import { toast } from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa'
import { useMutation } from 'react-query'
import { useEffect, useMemo, useState } from 'react'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import { TextInput } from '../../components/forms/FormInput'
const CollectionActionsPage: NextPage = () => {
const { minter: minterContract, sg721: sg721Contract } = useContracts()
const wallet = useWallet()
const [lastTx, setLastTx] = useState('')
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [airdropArray, setAirdropArray] = useState<string[]>([])
const comboboxState = useActionsComboboxState()
const type = comboboxState.value?.id
const [action, setAction] = useState<boolean>(false)
const sg721ContractState = useInputState({
id: 'sg721-contract-address',
@ -52,58 +32,6 @@ const CollectionActionsPage: NextPage = () => {
subtitle: 'Address of the Minter contract',
})
const limitState = useNumberInputState({
id: 'per-address-limi',
name: 'perAddressLimit',
title: 'Per Address Limit',
subtitle: 'Enter the per address limit',
})
const tokenIdState = useNumberInputState({
id: 'token-id',
name: 'tokenId',
title: 'Token ID',
subtitle: 'Enter the token ID',
})
const batchNumberState = useNumberInputState({
id: 'batch-number',
name: 'batchNumber',
title: 'Number of Tokens',
subtitle: 'Enter the number of tokens to mint',
})
const tokenIdListState = useInputState({
id: 'token-id-list',
name: 'tokenIdList',
title: 'List of token IDs',
subtitle:
'Specify individual token IDs separated by commas (e.g., 2, 4, 8) or a range of IDs separated by a colon (e.g., 8:13)',
})
const recipientState = useInputState({
id: 'recipient-address',
name: 'recipient',
title: 'Recipient Address',
subtitle: 'Address of the recipient',
})
const whitelistState = useInputState({
id: 'whitelist-address',
name: 'whitelistAddress',
title: 'Whitelist Address',
subtitle: 'Address of the whitelist contract',
})
const showWhitelistField = type === 'set_whitelist'
const showDateField = type === 'update_start_time'
const showLimitField = type === 'update_per_address_limit'
const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn'])
const showNumberOfTokensField = type === 'batch_mint'
const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer'])
const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer'])
const showAirdropFileField = type === 'airdrop'
const minterMessages = useMemo(
() => minterContract?.use(minterContractState.value),
[minterContract, minterContractState.value],
@ -112,50 +40,31 @@ const CollectionActionsPage: NextPage = () => {
() => sg721Contract?.use(sg721ContractState.value),
[sg721Contract, sg721ContractState.value],
)
const payload: DispatchExecuteArgs = {
whitelist: whitelistState.value,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
limit: limitState.value,
minterContract: minterContractState.value,
sg721Contract: sg721ContractState.value,
tokenId: tokenIdState.value,
tokenIds: tokenIdListState.value,
batchNumber: batchNumberState.value,
minterMessages,
sg721Messages,
recipient: recipientState.value,
recipients: airdropArray,
txSigner: wallet.address,
type,
}
const { isLoading, mutate } = useMutation(
async (event: FormEvent) => {
event.preventDefault()
if (!type) {
throw new Error('Please select an action!')
}
if (minterContractState.value === '' && sg721ContractState.value === '') {
throw new Error('Please enter minter and sg721 contract addresses!')
}
const txHash = await toast.promise(dispatchExecute(payload), {
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
loading: 'Executing message...',
success: (tx) => `Transaction ${tx} success!`,
})
if (txHash) {
setLastTx(txHash)
}
},
{
onError: (error) => {
toast.error(String(error))
},
},
)
const airdropFileOnChange = (data: string[]) => {
setAirdropArray(data)
}
const sg721ContractAddress = sg721ContractState.value
const minterContractAddress = minterContractState.value
const router = useRouter()
useEffect(() => {
if (minterContractAddress.length > 0 && sg721ContractAddress.length === 0) {
void router.replace({ query: { minterContractAddress } })
}
if (sg721ContractAddress.length > 0 && minterContractAddress.length === 0) {
void router.replace({ query: { sg721ContractAddress } })
}
if (sg721ContractAddress.length > 0 && minterContractAddress.length > 0) {
void router.replace({ query: { sg721ContractAddress, minterContractAddress } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sg721ContractAddress, minterContractAddress])
useEffect(() => {
const initialMinter = new URL(document.URL).searchParams.get('minterContractAddress')
const initialSg721 = new URL(document.URL).searchParams.get('sg721ContractAddress')
if (initialMinter && initialMinter.length > 0) minterContractState.onChange(initialMinter)
if (initialSg721 && initialSg721.length > 0) sg721ContractState.onChange(initialSg721)
}, [])
return (
<section className="py-6 px-12 space-y-4">
@ -166,40 +75,71 @@ const CollectionActionsPage: NextPage = () => {
title="Collection Actions"
/>
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
<div className="space-y-8">
<AddressInput {...sg721ContractState} />
<form className="p-4">
<div className="grid grid-cols-2">
<AddressInput {...sg721ContractState} className="mr-2" />
<AddressInput {...minterContractState} />
<ActionsCombobox {...comboboxState} />
{showRecipientField && <AddressInput {...recipientState} />}
{showWhitelistField && <AddressInput {...whitelistState} />}
{showLimitField && <NumberInput {...limitState} />}
{showTokenIdField && <NumberInput {...tokenIdState} />}
{showTokenIdListField && <TextInput {...tokenIdListState} />}
{showNumberOfTokensField && <NumberInput {...batchNumberState} />}
{showAirdropFileField && (
<FormGroup subtitle="TXT file that contains the airdrop addresses" title="Airdrop File">
<WhitelistUpload onChange={airdropFileOnChange} />
</FormGroup>
)}
<Conditional test={showDateField}>
<FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</FormControl>
</Conditional>
</div>
<div className="space-y-8">
<div className="relative">
<Button className="absolute top-0 right-0" isLoading={isLoading} rightIcon={<FaArrowRight />} type="submit">
Execute
</Button>
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
<TransactionHash hash={lastTx} />
</FormControl>
<div className="mt-4">
<div className="mr-2">
<div className="flex justify-items-start font-bold">
<div className="form-check form-check-inline">
<input
checked={!action}
className="peer sr-only"
id="inlineRadio4"
name="inlineRadioOptions4"
onClick={() => {
setAction(false)
}}
type="radio"
value="false"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio4"
>
Queries
</label>
</div>
<div className="ml-2 form-check form-check-inline">
<input
checked={action}
className="peer sr-only"
id="inlineRadio3"
name="inlineRadioOptions3"
onClick={() => {
setAction(true)
}}
type="radio"
value="true"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio3"
>
Actions
</label>
</div>
</div>
<div>
{(action && (
<CollectionActions
minterContractAddress={minterContractState.value}
minterMessages={minterMessages}
sg721ContractAddress={sg721ContractState.value}
sg721Messages={sg721Messages}
/>
)) || (
<CollectionQueries
minterContractAddress={minterContractState.value}
minterMessages={minterMessages}
sg721ContractAddress={sg721ContractState.value}
sg721Messages={sg721Messages}
/>
)}
</div>
</div>
<FormControl subtitle="View current message to be sent" title="Payload Preview">
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
</FormControl>
</div>
</form>
</section>

View File

@ -29,7 +29,7 @@ import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { upload } from 'services/upload'
import { compareFileArrays } from 'utils/compareFileArrays'
import { MINTER_CODE_ID, SG721_CODE_ID, WHITELIST_CODE_ID } from 'utils/constants'
import { MINTER_CODE_ID, SG721_CODE_ID, STARGAZE_URL, WHITELIST_CODE_ID } from 'utils/constants'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
@ -37,6 +37,14 @@ import type { UploadMethod } from '../../components/collections/creation/UploadD
import { ConfirmationModal } from '../../components/ConfirmationModal'
import { getAssetType } from '../../utils/getAssetType'
export interface CollectionData {
name: string
address: string
minter: string
imageURL: string
time: number
}
const CollectionCreationPage: NextPage = () => {
const wallet = useWallet()
const { minter: minterContract, whitelist: whitelistContract } = useContracts()
@ -49,21 +57,23 @@ const CollectionCreationPage: NextPage = () => {
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
const [uploading, setUploading] = useState(false)
const [creatingCollection, setCreatingCollection] = useState(false)
const [readyToCreate, setReadyToCreate] = useState(false)
const [minterContractAddress, setMinterContractAddress] = useState<string | null>(null)
const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null)
const [whitelistContractAddress, setWhitelistContractAddress] = useState<string | null | undefined>(null)
const [baseTokenUri, setBaseTokenUri] = useState<string | null>(null)
const [coverImageUrl, setCoverImageUrl] = useState<string | null>(null)
const [transactionHash, setTransactionHash] = useState<string | null>(null)
const performChecks = () => {
try {
// setReadyToCreate(false)
// checkUploadDetails()
// checkCollectionDetails()
// checkMintingDetails()
// checkWhitelistDetails()
// checkRoyaltyDetails()
setReadyToCreate(false)
checkUploadDetails()
checkCollectionDetails()
checkMintingDetails()
checkWhitelistDetails()
checkRoyaltyDetails()
setReadyToCreate(true)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
@ -74,10 +84,12 @@ const CollectionCreationPage: NextPage = () => {
const createCollection = async () => {
try {
setCreatingCollection(true)
setBaseTokenUri(null)
setCoverImageUrl(null)
setMinterContractAddress(null)
setSg721ContractAddress(null)
setWhitelistContractAddress(null)
setTransactionHash(null)
if (uploadDetails?.uploadMethod === 'new') {
setUploading(true)
@ -101,6 +113,7 @@ const CollectionCreationPage: NextPage = () => {
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 {
@ -110,12 +123,15 @@ const CollectionCreationPage: NextPage = () => {
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)
setCreatingCollection(false)
setUploading(false)
}
}
@ -185,6 +201,41 @@ const CollectionCreationPage: NextPage = () => {
setTransactionHash(data.transactionHash)
setMinterContractAddress(data.contractAddress)
setSg721ContractAddress(data.logs[0].events[3].attributes[2].value)
console.log(`ipfs://${coverImageUri}/${collectionDetails?.imageFile[0].name as string}`)
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const allCollections: Record<string, CollectionData[]>[] = localStorage['collections']
? JSON.parse(localStorage['collections'])
: []
console.log('allCollections', allCollections)
const newCollectionData: CollectionData = {
name: collectionDetails?.name as string,
address: data.logs[0].events[3].attributes[2].value,
minter: data.contractAddress,
imageURL: `https://ipfs.stargaze.zone/ipfs/${coverImageUri}/${collectionDetails?.imageFile[0].name as string}`,
time: new Date().getTime(),
}
//Get the CollectionData array for the current wallet address
const myCollections = allCollections.find((c) => Object.keys(c)[0] === wallet.address)
console.log('myCollections', myCollections)
if (myCollections === undefined) {
//If there is no CollectionData array for the current wallet address, create one in allCollections
allCollections.push({ [wallet.address]: [newCollectionData] })
//allCollections[allCollections.indexOf(myCollections)] = {[wallet.address]: myCollectionList}
} else {
//If there is a CollectionData array for the current wallet address, push the new collection data to it in allCollections
allCollections[allCollections.indexOf(myCollections)][wallet.address].push(newCollectionData)
}
//List of all collections for the current wallet address
const myCollectionList = myCollections ? Object.values(myCollections)[0] : []
console.log('myCollectionList', myCollectionList)
//Update the localStorage
localStorage['collections'] = JSON.stringify(allCollections)
console.log(localStorage['collections'])
}
const uploadFiles = async (): Promise<string> => {
@ -265,7 +316,7 @@ const CollectionCreationPage: NextPage = () => {
if (uploadDetails.uploadMethod === 'new') {
if (uploadDetails.uploadService === 'nft-storage') {
if (uploadDetails.nftStorageApiKey === '') {
throw new Error('Please enter a valid NFT Storage API key')
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')
@ -409,6 +460,17 @@ const CollectionCreationPage: NextPage = () => {
{sg721ContractAddress}
</Anchor>
<br />
<Conditional test={whitelistContractAddress !== null && whitelistContractAddress !== undefined}>
Whitelist Contract Address:{' '}
<Anchor
className="text-stargaze hover:underline"
external
href={`/contracts/whitelist/query/?contractAddress=${whitelistContractAddress as string}`}
>
{whitelistContractAddress}
</Anchor>
<br />
</Conditional>
Transaction Hash: {' '}
<Anchor
className="text-stargaze hover:underline"
@ -417,6 +479,15 @@ const CollectionCreationPage: NextPage = () => {
>
{transactionHash}
</Anchor>
<Button className="mt-2">
<Anchor
className="text-white"
external
href={`${STARGAZE_URL}/launchpad/${minterContractAddress as string}`}
>
View on Launchpad
</Anchor>
</Button>
</div>
</Alert>
</Conditional>
@ -443,9 +514,9 @@ const CollectionCreationPage: NextPage = () => {
</div>
{readyToCreate && <ConfirmationModal confirm={createCollection} />}
<div className="flex justify-end w-full">
<Button className="px-0 mb-6 max-h-12" onClick={performChecks} variant="solid">
<Button className="px-0 mb-6 max-h-12" isLoading={creatingCollection} onClick={performChecks} variant="solid">
<label
className="relative justify-end w-full h-full text-white bg-plumbus-light hover:bg-plumbus-light border-0 btn modal-button"
className="relative justify-end w-full h-full text-white bg-plumbus hover:bg-plumbus-light border-0 btn modal-button"
htmlFor="my-modal-2"
>
Create Collection

View File

@ -27,6 +27,13 @@ const HomePage: NextPage = () => {
>
Upload your assets, enter collection metadata and deploy your collection.
</HomeCard>
<HomeCard
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
link="/collections/myCollections"
title="My Collections"
>
View a list of your collections.
</HomeCard>
<HomeCard
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
link="/collections/actions"
@ -34,13 +41,6 @@ const HomePage: NextPage = () => {
>
Execute messages on a collection.
</HomeCard>
<HomeCard
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
link="/collections/queries"
title="Collection Queries"
>
Query data from a collection.
</HomeCard>
</div>
</section>
)

View File

@ -0,0 +1,118 @@
import { Alert } from 'components/Alert'
import { Anchor } from 'components/Anchor'
import { Button } from 'components/Button'
import { Conditional } from 'components/Conditional'
import { ContractPageHeader } from 'components/ContractPageHeader'
import { useWallet } from 'contexts/wallet'
import type { NextPage } from 'next'
import { NextSeo } from 'next-seo'
import { useCallback, useState } from 'react'
import { FaRocket, FaSlidersH } from 'react-icons/fa'
import { STARGAZE_URL } from 'utils/constants'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import type { CollectionData } from './create'
const CollectionList: NextPage = () => {
const wallet = useWallet()
const [clearFlag, setClearFlag] = useState(false)
let allCollections: Record<string, CollectionData[]>[] = []
let myCollections: Record<string, CollectionData[]> | undefined
let myCollectionList: CollectionData[] = []
if (typeof localStorage !== 'undefined') {
allCollections = localStorage['collections'] ? JSON.parse(localStorage['collections']) : []
myCollections = allCollections.find((c) => Object.keys(c)[0] === wallet.address)
myCollectionList = myCollections ? Object.values(myCollections)[0] : []
console.log(localStorage['collections'])
}
const renderTable = useCallback(() => {
return (
<div className="overflow-x-auto w-full">
{myCollectionList.length > 0 && (
<table className="table w-full">
<thead>
<tr>
<th className="pl-20 text-lg font-bold text-left bg-black">Collection Name</th>
<th className="text-lg font-bold bg-black">Contract Address</th>
<th className="text-lg font-bold bg-black">Creation Time</th>
<th className="bg-black" />
</tr>
</thead>
<tbody>
{myCollectionList.map((collection, index) => {
return (
<tr key={index}>
<td className="bg-black">
<div className="flex items-center space-x-3">
<div className="avatar">
<div className="w-12 h-12 mask mask-squircle">
<img alt="Cover" src={collection.imageURL} />
</div>
</div>
<div>
<div className="ml-2 font-bold">{collection.name}</div>
<div className="text-sm opacity-50" />
</div>
</div>
</td>
<td className="bg-black">
{collection.address}
{/* <br /> */}
{/* <span className="badge badge-ghost badge-sm"></span> */}
</td>
<td className="bg-black">{new Date(collection.time).toDateString()}</td>
<th className="bg-black">
<div className="flex items-center space-x-8">
<Anchor
className="text-xl text-plumbus"
href={`/collections/actions?sg721ContractAddress=${collection.address}&minterContractAddress=${collection.minter}`}
>
<FaSlidersH />
</Anchor>
<Anchor
className="text-xl text-plumbus"
external
href={`${STARGAZE_URL}/launchpad/${collection.minter}`}
>
<FaRocket />
</Anchor>
</div>
</th>
</tr>
)
})}
</tbody>
</table>
)}
</div>
)
}, [clearFlag, wallet.address])
const clearMyCollections = () => {
console.log('Cleared!')
if (typeof localStorage !== 'undefined') {
localStorage['collections'] = JSON.stringify(allCollections.filter((c) => Object.keys(c)[0] !== wallet.address))
myCollectionList = []
setClearFlag(!clearFlag)
}
}
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="My Collections" />
<ContractPageHeader description="A list of your collections." link={links.Documentation} title="My Collections" />
<hr />
<div>{renderTable()}</div>
<br />
{myCollectionList.length > 0 && <Button onClick={clearMyCollections}>Clear Collection List</Button>}
<Conditional test={myCollectionList.length === 0}>
<Alert type="info">You haven&apos;t created any collections so far.</Alert>
</Conditional>
</section>
)
}
export default withMetadata(CollectionList, { center: false })

View File

@ -16,9 +16,10 @@ import { useWallet } from 'contexts/wallet'
import type { DispatchExecuteArgs } from 'contracts/minter/messages/execute'
import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/minter/messages/execute'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { NextSeo } from 'next-seo'
import type { FormEvent } from 'react'
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa'
import { useMutation } from 'react-query'
@ -62,6 +63,7 @@ const MinterExecutePage: NextPage = () => {
title: 'Minter Address',
subtitle: 'Address of the Minter contract',
})
const contractAddress = contractState.value
const recipientState = useInputState({
id: 'recipient-address',
@ -122,6 +124,19 @@ const MinterExecutePage: NextPage = () => {
},
)
const router = useRouter()
useEffect(() => {
if (contractAddress.length > 0) {
void router.replace({ query: { contractAddress } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [contractAddress])
useEffect(() => {
const initial = new URL(document.URL).searchParams.get('contractAddress')
if (initial && initial.length > 0) contractState.onChange(initial)
}, [])
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="Execute Minter Contract" />

View File

@ -15,9 +15,10 @@ import { useWallet } from 'contexts/wallet'
import type { DispatchExecuteArgs } from 'contracts/sg721/messages/execute'
import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/sg721/messages/execute'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { NextSeo } from 'next-seo'
import type { FormEvent } from 'react'
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa'
import { useMutation } from 'react-query'
@ -49,6 +50,8 @@ const Sg721ExecutePage: NextPage = () => {
subtitle: 'Address of the Sg721 contract',
})
const contractAddress = contractState.value
const messageState = useInputState({
id: 'message',
name: 'message',
@ -121,6 +124,19 @@ const Sg721ExecutePage: NextPage = () => {
},
)
const router = useRouter()
useEffect(() => {
if (contractAddress.length > 0) {
void router.replace({ query: { contractAddress } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [contractAddress])
useEffect(() => {
const initial = new URL(document.URL).searchParams.get('contractAddress')
if (initial && initial.length > 0) contractState.onChange(initial)
}, [])
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="Execute Sg721 Contract" />

View File

@ -40,7 +40,7 @@ const WhitelistQueryPage: NextPage = () => {
})
const address = addressState.value
const [type, setType] = useState<QueryType>('has_started')
const [type, setType] = useState<QueryType>('config')
const addressVisible = type === 'has_member'
@ -98,6 +98,7 @@ const WhitelistQueryPage: NextPage = () => {
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
defaultValue="config"
id="contract-query-type"
name="query-type"
onChange={(e) => setType(e.target.value as QueryType)}

View File

@ -6,7 +6,7 @@ const HomePage: NextPage = () => {
return (
<section className="px-8 pt-4 pb-16 mx-auto space-y-8 max-w-4xl">
<div className="flex justify-center items-center py-8 max-w-xl">
<img alt="Brand Text" className="ml-[50%] w-full" src="/stargaze-text.png" />
<img alt="Brand Text" className="ml-[50%] w-full" src="/stargaze_logo_800.svg" />
</div>
<h1 className="font-heading text-4xl font-bold">Welcome!</h1>
<p className="text-xl">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 991 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="801px" height="161px" viewBox="0 0 801 161" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>stargaze_logo</title>
<defs>
<linearGradient x1="0.0995425432%" y1="47.2856461%" x2="104.967972%" y2="50.3175741%" id="linearGradient-1">
<stop stop-color="#FFF59E" offset="0%"></stop>
<stop stop-color="#81EFD9" offset="63.5417%"></stop>
<stop stop-color="#6B9BE3" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="stargaze_logo" transform="translate(0.357912, -0.000003)" fill="url(#linearGradient-1)">
<path d="M41.3060853,35.0506617 L77.4908947,35.0506617 L77.4908947,0.0952535567 L41.9975438,0.0952535567 C6.73463446,-0.86903264 0.972725983,4.91668454 0.0178962024,43.9703426 L0.0178962024,53.8539743 C-0.410124003,88.809852 6.73463446,98.6934837 32.7784769,97.9705038 L39.2317768,97.9705038 L39.2317768,125.934669 L0.742217452,125.934669 L0.742217452,160.889876 L39.0013353,160.889876 C66.658268,160.889876 68.5026045,160.408337 75.4165191,151.970665 C80.0260191,144.255973 80.9481873,139.193772 81.6396458,115.568827 L81.6396458,105.684525 C81.6396458,71.6937384 74.7250606,62.5330866 48.6813523,63.0152968 L41.3060853,63.0152968 L41.3060853,35.0506617 Z M146.173316,35.0501252 L164.842025,35.0501252 L164.842025,0.0947170224 L86.2498165,0.0947170224 L86.2498165,35.0501252 L104.918526,35.0501252 L104.918526,160.889876 L146.173316,160.889876 L146.173316,35.0501252 Z M215.051572,160.889876 L173.796112,160.889876 L173.796112,32.8804477 C174.25686,4.43400491 179.557819,0.0947170224 215.314474,0.0947170224 L232.139518,0.0947170224 C267.86331,0.0947170224 273.164268,4.19289983 273.625017,32.8804477 L273.625017,160.889876 L232.370227,160.889876 L232.370227,126.898419 L215.084435,126.898419 L215.084435,160.889876 L215.051572,160.889876 Z M232.337365,91.2195615 L232.337365,48.7911029 C232.337365,38.4251269 229.57153,33.8447339 223.579113,33.8447339 C217.586696,33.8447339 215.051572,38.6661649 215.051572,48.7911029 L215.051572,91.2195615 L232.337365,91.2195615 Z M388.369575,31.1927121 C387.217368,3.46945045 382.147119,-0.628732359 351.493575,0.0944487553 L288.573532,0.0944487553 L288.573532,160.889206 L329.828993,160.889206 L329.828993,97.9698331 L335.129951,97.9698331 C343.887532,98.2106029 347.344824,102.309054 347.114785,112.434126 L347.114785,133.88946 C347.114785,145.219723 348.036283,153.898164 350.341368,160.889206 L391.596158,160.889206 C389.752492,154.139605 388.369575,144.496743 388.369575,133.88946 L388.369575,109.058655 C388.830995,90.9787917 383.068617,83.2640997 365.782824,78.4426687 C383.068617,76.9960382 389.061034,66.8716368 388.369575,41.5588223 L388.369575,31.1927121 Z M347.344824,49.2733131 C347.344824,60.6035753 345.270449,63.0139555 335.5907,63.0139555 L329.828993,63.0139555 L329.828993,35.0498569 L335.5907,35.0498569 C344.348951,35.0498569 347.344824,38.6659637 347.344824,49.2733131 Z M501.568244,125.211689 C502.028993,157.273635 497.189454,161.613527 460.774203,160.889876 L443.718449,160.889876 C404.537364,161.372086 401.772201,158.479496 402.23295,117.496997 L402.00224,104.961545 L402.00224,56.7472353 C402.430126,4.43440731 405.887419,-0.14598566 443.685587,0.0951194231 L501.535381,0.0951194231 L501.535381,39.6308535 C496.695171,37.220138 485.171757,35.5326707 471.573296,35.0505276 L457.053338,35.0505276 C444.377045,35.0505276 443.224838,36.2558518 443.224838,49.7561939 L443.224838,106.889715 C442.994128,123.765059 444.146335,125.693229 453.596045,125.934669 L460.27992,125.934669 L460.27992,87.1217811 C460.27992,80.6129498 459.127713,75.0685389 457.053338,69.0419179 L501.535381,69.0419179 L501.535381,125.211689 L501.568244,125.211689 Z M515.59392,160.889876 L556.84871,160.889876 L556.84871,126.898419 L574.134502,126.898419 L574.134502,160.889876 L615.389963,160.889876 L615.389963,32.8804477 C614.929214,4.19289983 609.628256,0.0947170224 573.904463,0.0947170224 L557.07942,0.0947170224 C521.355627,0.0947170224 516.054669,4.43400491 515.59392,32.8804477 L515.59392,160.889876 Z M574.134502,48.7911029 L574.134502,91.2195615 L556.84871,91.2195615 L556.84871,48.7911029 C556.84871,38.6661649 559.384505,33.8447339 565.376922,33.8447339 C571.369339,33.8447339 574.134502,38.4251269 574.134502,48.7911029 Z M709.42631,0.0947170224 L675.081411,125.933999 L711.961435,125.933999 L707.34724,160.889876 L624.839002,160.889876 L659.642638,35.0501252 L625.300421,35.0501252 L630.13996,0.0947170224 L709.42631,0.0947170224 Z M761.973134,35.0497899 L800,35.0497899 L800,0.0943146217 L761.510374,0.0943146217 C725.555872,-0.387828477 721.639171,4.43366958 720.720357,51.6836932 L720.720357,103.032034 C720.485623,126.656979 722.329959,144.014533 725.555872,149.558943 C731.779669,159.924785 734.086766,160.648436 761.510374,160.889206 L800,160.889206 L800,125.933999 L761.973134,125.933999 L761.973134,97.9691624 L800,97.9691624 L800,63.0139555 L761.973134,63.0139555 L761.973134,35.0497899 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -2,7 +2,7 @@ import type { CIDString } from 'nft.storage'
import { NFTStorage } from 'nft.storage'
export const uploadToNftStorage = async (fileArray: File[], nftStorageApiKey: string): Promise<CIDString> => {
console.log('Uploading to NFT Storage...')
console.log('Uploading to NFT.Storage...')
const client = new NFTStorage({ token: nftStorageApiKey })
return client.storeDirectory(fileArray)
}

View File

@ -12,24 +12,24 @@ module.exports = {
theme: {
extend: {
colors: {
stargaze: { DEFAULT: '#FFA900' },
stargaze: { DEFAULT: '#DB2676' },
dark: { DEFAULT: '#06090B' },
gray: { DEFAULT: '#F3F6F8' },
gray: { DEFAULT: '#A9A9A9' },
'dark-gray': { DEFAULT: '#191D20' },
purple: { DEFAULT: '#7E5DFF' },
neutral: colors.neutral,
plumbus: {
DEFAULT: '#FFA900',
light: '#FFC922',
DEFAULT: '#DB2676',
light: '#AF1F5F',
matte: '#5D89E9',
dark: '#FFC900',
10: '#FFF0ED',
20: '#5D89E9',
30: '#F5A7A2',
40: '#FFA900',
50: '#FFA900',
60: '#FFA900',
40: '#DB2676',
50: '#DB2676',
60: '#DB2676',
70: '#AB5152',
80: '#944144',
90: '#7D3136',

View File

@ -4,6 +4,7 @@ export const WHITELIST_CODE_ID = parseInt(process.env.NEXT_PUBLIC_WHITELIST_CODE
export const PINATA_ENDPOINT_URL = process.env.NEXT_PUBLIC_PINATA_ENDPOINT_URL
export const NETWORK = process.env.NEXT_PUBLIC_NETWORK
export const STARGAZE_URL = process.env.NEXT_PUBLIC_STARGAZE_WEBSITE_URL
export const BLOCK_EXPLORER_URL = process.env.NEXT_PUBLIC_BLOCK_EXPLORER_URL

View File

@ -8,7 +8,7 @@ export const links = {
deuslabs: `https://deuslabs.fi`,
Discord: `https://discord.gg/stargaze`,
Docs: `https://docs.stargaze.zone/`,
GitHub: `https://github.com/public-awesome`,
GitHub: `https://github.com/public-awesome/stargaze-studio`,
Stargaze: `https://stargaze.zone/`,
Telegram: `https://t.me/joinchat/ZQ95YmIn3AI0ODFh`,
Twitter: `https://twitter.com/stargazezone`,