From 7b58c9e8ac72748e6de47cd1cfea62fbb5fec904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20Nak=C4=B1=C5=9F=C3=A7=C4=B1?= Date: Tue, 26 Jul 2022 11:42:45 +0300 Subject: [PATCH] Add collection queries page (#5) --- components/collections/actions/Combobox.tsx | 2 +- .../collections/queries/Combobox.hooks.ts | 8 ++ components/collections/queries/Combobox.tsx | 92 ++++++++++++++ components/collections/queries/query.ts | 103 ++++++++++++++++ pages/collections/queries.tsx | 116 ++++++++++++++++++ 5 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 components/collections/queries/Combobox.hooks.ts create mode 100644 components/collections/queries/Combobox.tsx create mode 100644 components/collections/queries/query.ts create mode 100644 pages/collections/queries.tsx diff --git a/components/collections/actions/Combobox.tsx b/components/collections/actions/Combobox.tsx index 526cfa0..2f59969 100644 --- a/components/collections/actions/Combobox.tsx +++ b/components/collections/actions/Combobox.tsx @@ -61,7 +61,7 @@ export const ActionsCombobox = ({ value, onChange }: ActionsComboboxProps) => { > {filtered.length < 1 && ( - Action found. + Action not found )} {filtered.map((entry) => ( diff --git a/components/collections/queries/Combobox.hooks.ts b/components/collections/queries/Combobox.hooks.ts new file mode 100644 index 0000000..b2aac76 --- /dev/null +++ b/components/collections/queries/Combobox.hooks.ts @@ -0,0 +1,8 @@ +import { useState } from 'react' + +import type { QueryListItem } from './query' + +export const useQueryComboboxState = () => { + const [value, setValue] = useState(null) + return { value, onChange: (item: QueryListItem) => setValue(item) } +} diff --git a/components/collections/queries/Combobox.tsx b/components/collections/queries/Combobox.tsx new file mode 100644 index 0000000..ca65511 --- /dev/null +++ b/components/collections/queries/Combobox.tsx @@ -0,0 +1,92 @@ +import { Combobox, Transition } from '@headlessui/react' +import clsx from 'clsx' +import { FormControl } from 'components/FormControl' +import { matchSorter } from 'match-sorter' +import { Fragment, useState } from 'react' +import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' + +import type { QueryListItem } from './query' +import { QUERY_LIST } from './query' + +export interface QueryComboboxProps { + value: QueryListItem | null + onChange: (item: QueryListItem) => void +} + +export const QueryCombobox = ({ value, onChange }: QueryComboboxProps) => { + const [search, setSearch] = useState('') + + const filtered = search === '' ? QUERY_LIST : matchSorter(QUERY_LIST, search, { keys: ['id', 'name', 'description'] }) + + return ( + +
+ val?.name ?? ''} + id="message-type" + onChange={(event) => setSearch(event.target.value)} + placeholder="Select query" + /> + + + {({ open }) => + + setSearch('')} as={Fragment}> + + {filtered.length < 1 && ( + + Query not found + + )} + {filtered.map((entry) => ( + + clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active }) + } + value={entry} + > + {entry.name} + {entry.description} + + ))} + + +
+ + {value && ( +
+
+ +
+ {value.description} +
+ )} +
+ ) +} diff --git a/components/collections/queries/query.ts b/components/collections/queries/query.ts new file mode 100644 index 0000000..e9454db --- /dev/null +++ b/components/collections/queries/query.ts @@ -0,0 +1,103 @@ +import type { MinterInstance } from 'contracts/minter' +import type { SG721Instance } from 'contracts/sg721' + +export type QueryType = typeof QUERY_TYPES[number] + +export const QUERY_TYPES = [ + 'collection_info', + 'mint_price', + 'num_tokens', + 'tokens_minted_to_user', + // 'token_owners', + 'token_info', +] as const + +export interface QueryListItem { + id: QueryType + name: string + description?: string +} + +export const QUERY_LIST: QueryListItem[] = [ + { + id: 'collection_info', + name: 'Collection Info', + description: `Get information about the collection.`, + }, + { + id: 'mint_price', + name: 'Mint Price', + description: `Get the price of minting a token.`, + }, + { + id: 'num_tokens', + name: 'Mintable Number of Tokens', + description: `Get the mintable number of tokens in the collection.`, + }, + { + id: 'tokens_minted_to_user', + name: 'Tokens Minted to User', + description: `Get the number of tokens minted in the collection to a user.`, + }, + // { + // id: 'token_owners', + // name: 'Token Owners', + // description: `Get the list of users who own tokens in the collection.`, + // }, + { + id: 'token_info', + name: 'Token Info', + description: `Get information about a token in the collection.`, + }, +] + +export interface DispatchExecuteProps { + type: QueryType + [k: string]: unknown +} + +type Select = T + +export type DispatchQueryArgs = { + minterMessages?: MinterInstance + sg721Messages?: SG721Instance +} & ( + | { type: undefined } + | { type: Select<'collection_info'> } + | { type: Select<'mint_price'> } + | { type: Select<'num_tokens'> } + | { type: Select<'tokens_minted_to_user'>; address: string } + // | { type: Select<'token_owners'> } + | { type: Select<'token_info'>; tokenId: string } +) + +export const dispatchQuery = async (args: DispatchQueryArgs) => { + const { minterMessages, sg721Messages } = args + if (!minterMessages || !sg721Messages) { + throw new Error('Cannot execute actions') + } + switch (args.type) { + case 'collection_info': { + return sg721Messages.collectionInfo() + } + case 'mint_price': { + return minterMessages.getMintPrice() + } + case 'num_tokens': { + return minterMessages.getMintableNumTokens() + } + case 'tokens_minted_to_user': { + return minterMessages.getMintCount(args.address) + } + // case 'token_owners': { + // return minterMessages.updateStartTime(txSigner, args.startTime) + // } + case 'token_info': { + if (!args.tokenId) return + return sg721Messages.allNftInfo(args.tokenId) + } + default: { + throw new Error('Unknown action') + } + } +} diff --git a/pages/collections/queries.tsx b/pages/collections/queries.tsx new file mode 100644 index 0000000..39be983 --- /dev/null +++ b/pages/collections/queries.tsx @@ -0,0 +1,116 @@ +import { QueryCombobox } from 'components/collections/queries/Combobox' +import { useQueryComboboxState } from 'components/collections/queries/Combobox.hooks' +import { dispatchQuery } from 'components/collections/queries/query' +import { ContractPageHeader } from 'components/ContractPageHeader' +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 { useContracts } from 'contexts/contracts' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import { useMemo } from 'react' +import { toast } from 'react-hot-toast' +import { useQuery } from 'react-query' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const CollectionQueriesPage: NextPage = () => { + const { minter: minterContract, sg721: sg721Contract } = useContracts() + + const comboboxState = useQueryComboboxState() + const type = comboboxState.value?.id + + const sg721ContractState = useInputState({ + id: 'sg721-contract-address', + name: 'sg721-contract-address', + title: 'Sg721 Address', + subtitle: 'Address of the Sg721 contract', + }) + const sg721ContractAddress = sg721ContractState.value + + const minterContractState = useInputState({ + id: 'minter-contract-address', + name: 'minter-contract-address', + title: 'Minter Address', + subtitle: 'Address of the Minter contract', + }) + const minterContractAddress = minterContractState.value + + 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 minterMessages = useMemo( + () => minterContract?.use(minterContractAddress), + [minterContract, minterContractAddress], + ) + const sg721Messages = useMemo(() => sg721Contract?.use(sg721ContractAddress), [sg721Contract, sg721ContractAddress]) + + 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 ( +
+ + + +
+
+ + + + {showAddressField && } + {showTokenIdField && } +
+
+ + + +
+
+
+ ) +} + +export default withMetadata(CollectionQueriesPage, { center: false })