commit
57d73d9ed9
@ -1,4 +1,4 @@
|
||||
APP_VERSION=0.8.4
|
||||
APP_VERSION=0.8.3
|
||||
|
||||
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
|
||||
NEXT_PUBLIC_SG721_CODE_ID=2595
|
||||
@ -100,6 +100,3 @@ NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev
|
||||
NEXT_PUBLIC_BADGES_URL=https://badges.publicawesome.dev
|
||||
NEXT_PUBLIC_WEBSITE_URL=https://
|
||||
NEXT_PUBLIC_SYNC_COLLECTIONS_API_URL="https://..."
|
||||
|
||||
NEXT_PUBLIC_MEILISEARCH_HOST="https://search.publicawesome.dev"
|
||||
NEXT_PUBLIC_MEILISEARCH_API_KEY= "..."
|
||||
|
@ -1,79 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-implicit-coercion */
|
||||
/* eslint-disable import/no-default-export */
|
||||
/* eslint-disable tsdoc/syntax */
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export interface FieldsetBaseType {
|
||||
/**
|
||||
* The input's required id, used to link the label and input, as well as the error message.
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* Error message to show input validation.
|
||||
*/
|
||||
error?: string
|
||||
/**
|
||||
* Success message to show input validation.
|
||||
*/
|
||||
success?: string
|
||||
/**
|
||||
* Label to describe the input.
|
||||
*/
|
||||
label?: string | ReactNode
|
||||
/**
|
||||
* Hint to show optional fields or a hint to the user of what to enter in the input.
|
||||
*/
|
||||
hint?: string
|
||||
}
|
||||
|
||||
type FieldsetType = FieldsetBaseType & {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Fieldset
|
||||
* @description A fieldset component, used to share markup for labels, hints, and errors for Input components.
|
||||
*
|
||||
* @example
|
||||
* <Fieldset error={error} hint={hint} id={id} label={label}>
|
||||
* <input id={id} {...props} />
|
||||
* </Fieldset>
|
||||
*/
|
||||
export default function Fieldset({ label, hint, id, children, error, success }: FieldsetType) {
|
||||
return (
|
||||
<div>
|
||||
{!!label && (
|
||||
<div className="flex justify-between mb-1">
|
||||
<label className="block w-full text-sm font-medium text-zinc-700 dark:text-zinc-300" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
{typeof hint === 'string' && (
|
||||
<span className="text-sm text-zinc-500 dark:text-zinc-400" id={`${id}-optional`}>
|
||||
{hint}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
|
||||
{error && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-zinc-600" id={`${id}-error`}>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-zinc-500" id={`${id}-success`}>
|
||||
{success}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable import/no-default-export */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable jsx-a11y/autocomplete-valid */
|
||||
/* eslint-disable tsdoc/syntax */
|
||||
import type { PropsOf } from '@headlessui/react/dist/types'
|
||||
import type { ReactNode } from 'react'
|
||||
import { forwardRef } from 'react'
|
||||
import { classNames } from 'utils/css'
|
||||
|
||||
import type { FieldsetBaseType } from './Fieldset'
|
||||
import Fieldset from './Fieldset'
|
||||
import type { TrailingSelectProps } from './TrailingSelect'
|
||||
import TrailingSelect from './TrailingSelect'
|
||||
|
||||
/**
|
||||
* Shared styles for all input components.
|
||||
*/
|
||||
export const inputClassNames = {
|
||||
base: [
|
||||
'block w-full rounded-lg bg-white shadow-sm dark:bg-zinc-900 sm:text-sm',
|
||||
'text-white placeholder:text-zinc-500 focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-primary-500 focus:ring-0 focus:ring-offset-0',
|
||||
],
|
||||
valid: 'border-zinc-300 focus:border-zinc-300 dark:border-zinc-800 dark:focus:border-zinc-800',
|
||||
invalid: '!text-red-500 !border-red-500 focus:!border-red-500',
|
||||
success: 'text-green border-green focus:border-green',
|
||||
}
|
||||
|
||||
type InputProps = Omit<PropsOf<'input'> & FieldsetBaseType, 'className'> & {
|
||||
directory?: 'true'
|
||||
mozdirectory?: 'true'
|
||||
webkitdirectory?: 'true'
|
||||
leadingAddon?: string
|
||||
trailingAddon?: string
|
||||
trailingAddonIcon?: ReactNode
|
||||
trailingSelectProps?: TrailingSelectProps
|
||||
autoCompleteOff?: boolean
|
||||
preventAutoCapitalizeFirstLetter?: boolean
|
||||
className?: string
|
||||
icon?: JSX.Element
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Input
|
||||
* @description A standard input component, defaults to the text type.
|
||||
*
|
||||
* @example
|
||||
* // Standard input
|
||||
* <Input id="first-name" name="first-name" />
|
||||
*
|
||||
* @example
|
||||
* // Input component with label, placeholder and type email
|
||||
* <Input id="email" name="email" type="email" autoComplete="email" label="Email" placeholder="name@email.com" />
|
||||
*
|
||||
* @example
|
||||
* // Input component with label and leading and trailing addons
|
||||
* <Input
|
||||
* id="input-label-leading-trailing"
|
||||
* label="Bid"
|
||||
* placeholder="0.00"
|
||||
* leadingAddon="$"
|
||||
* trailingAddon="USD"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Input component with label and trailing select
|
||||
* const [trailingSelectValue, trailingSelectValueSet] = useState('USD');
|
||||
*
|
||||
* <Input
|
||||
* id="input-label-trailing-select"
|
||||
* label="Bid"
|
||||
* placeholder="0.00"
|
||||
* trailingSelectProps={{
|
||||
* id: 'currency',
|
||||
* label: 'Currency',
|
||||
* value: trailingSelectValue,
|
||||
* onChange: (event) => trailingSelectValueSet(event.target.value),
|
||||
* options: ['USD', 'CAD', 'EUR'],
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
(
|
||||
{
|
||||
error,
|
||||
success,
|
||||
hint,
|
||||
label,
|
||||
leadingAddon,
|
||||
trailingAddon,
|
||||
trailingAddonIcon,
|
||||
trailingSelectProps,
|
||||
id,
|
||||
className,
|
||||
type = 'text',
|
||||
autoCompleteOff = false,
|
||||
preventAutoCapitalizeFirstLetter,
|
||||
icon,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const cachedClassNames = classNames(
|
||||
...inputClassNames.base,
|
||||
className,
|
||||
error ? inputClassNames.invalid : inputClassNames.valid,
|
||||
success ? inputClassNames.success : inputClassNames.valid,
|
||||
leadingAddon && 'pl-7',
|
||||
trailingAddon && 'pr-12',
|
||||
trailingSelectProps && 'pr-16',
|
||||
icon && 'pl-10',
|
||||
)
|
||||
|
||||
const describedBy = [
|
||||
...(error ? [`${id}-error`] : []),
|
||||
...(success ? [`${id}-success`] : []),
|
||||
...(typeof hint === 'string' ? [`${id}-optional`] : []),
|
||||
...(typeof trailingAddon === 'string' ? [`${id}-addon`] : []),
|
||||
].join(' ')
|
||||
|
||||
return (
|
||||
<Fieldset error={error} hint={hint} id={id} label={label} success={success}>
|
||||
<div className="relative rounded-md shadow-sm">
|
||||
{leadingAddon && (
|
||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
||||
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm">{leadingAddon}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{icon && (
|
||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
||||
<span className="pr-10 text-zinc-500 dark:text-zinc-400 sm:text-sm">{icon}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={error ? 'true' : undefined}
|
||||
autoCapitalize={`${preventAutoCapitalizeFirstLetter ?? 'off'}`}
|
||||
autoComplete={`${autoCompleteOff ? 'off' : 'on'}`}
|
||||
className={cachedClassNames}
|
||||
id={id}
|
||||
ref={ref}
|
||||
type={type}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
{!trailingAddon && trailingSelectProps && <TrailingSelect {...trailingSelectProps} />}
|
||||
|
||||
{trailingAddon && (
|
||||
<div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
|
||||
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm" id={`${id}-addon`}>
|
||||
{trailingAddon}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{trailingAddonIcon && (
|
||||
<div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
|
||||
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm" id={`${id}-addonicon`}>
|
||||
{trailingAddonIcon}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fieldset>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export default Input
|
@ -108,15 +108,6 @@ export const Sidebar = () => {
|
||||
>
|
||||
<Link href="/collections/actions/">Collection Actions</Link>
|
||||
</li>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/snapshots') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/snapshots/">Snapshots</Link>
|
||||
</li>
|
||||
<Conditional test={NETWORK === 'mainnet'}>
|
||||
<li className={clsx('text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded')} tabIndex={-1}>
|
||||
<label
|
||||
|
@ -1,37 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable import/no-default-export */
|
||||
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { classNames } from 'utils/css'
|
||||
|
||||
export interface TrailingSelectProps {
|
||||
id: string
|
||||
label: string
|
||||
options: string[]
|
||||
value: string
|
||||
onChange: (event: ChangeEvent<HTMLSelectElement>) => void
|
||||
}
|
||||
|
||||
export default function TrailingSelect({ id, label, value, onChange, options }: TrailingSelectProps) {
|
||||
const cachedClassNames = classNames(
|
||||
'h-full rounded-md border-transparent bg-transparent py-0 pl-2 pr-7 text-zinc-500 dark:text-zinc-400 sm:text-sm',
|
||||
'focus:border-transparent focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-primary-500 focus:ring-0 focus:ring-offset-0',
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex absolute inset-y-0 right-0 items-center">
|
||||
<label className="sr-only" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<select className={cachedClassNames} id={id} name={id} onChange={onChange} value={value}>
|
||||
{/* TODO - Option values in a select are supposed to be unique, remove this comment during PR review */}
|
||||
{options.map((opt) => (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
3
env.d.ts
vendored
3
env.d.ts
vendored
@ -101,9 +101,6 @@ declare namespace NodeJS {
|
||||
readonly NEXT_PUBLIC_STARGAZE_WEBSITE_URL: string
|
||||
readonly NEXT_PUBLIC_WEBSITE_URL: string
|
||||
readonly NEXT_PUBLIC_SYNC_COLLECTIONS_API_URL: string
|
||||
|
||||
readonly NEXT_PUBLIC_MEILISEARCH_HOST: string
|
||||
readonly NEXT_PUBLIC_MEILISEARCH_API_KEY: string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,85 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-inferrable-types */
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
/* eslint-disable no-return-await */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { useQuery } from 'react-query'
|
||||
import { MEILISEARCH_API_KEY, MEILISEARCH_HOST } from 'utils/constants'
|
||||
|
||||
export default function useSearch(query: string, includeUids: string[] = [], limit: number = 4) {
|
||||
return useQuery<SearchResult[] | null>(
|
||||
['globalSearch', query],
|
||||
async () => await getSearchResults(query, includeUids, limit),
|
||||
)
|
||||
}
|
||||
|
||||
async function getSearchResults(query: string, includeUids: string[], limit: number) {
|
||||
let queries = [
|
||||
{
|
||||
indexUid: 'collections',
|
||||
sort: query === '' ? ['score_1d:desc'] : [],
|
||||
q: query ?? '',
|
||||
limit,
|
||||
},
|
||||
{
|
||||
indexUid: 'names',
|
||||
q: query ?? '',
|
||||
limit,
|
||||
},
|
||||
]
|
||||
if (includeUids.length) {
|
||||
queries = queries.filter((query) => includeUids.includes(query.indexUid))
|
||||
}
|
||||
|
||||
try {
|
||||
const response = (await fetch(`${MEILISEARCH_HOST}/multi-search`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${MEILISEARCH_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
queries,
|
||||
}),
|
||||
}).then((res) => res.json())) as SearchResponse
|
||||
|
||||
return response.results
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
indexUid: string
|
||||
hits: Hit[]
|
||||
estimatedTotalHits: number
|
||||
limit: number
|
||||
offset: number
|
||||
processingTimeMs: number
|
||||
query: string
|
||||
}
|
||||
|
||||
export interface SearchResponse {
|
||||
results: SearchResult[]
|
||||
}
|
||||
|
||||
export interface Hit {
|
||||
created_by: string
|
||||
description: string
|
||||
id: string
|
||||
collection_uri: string
|
||||
image_url: string
|
||||
minting: boolean
|
||||
minter: string
|
||||
name: string
|
||||
thumbnail_url: string
|
||||
tokens_count: number
|
||||
type: string
|
||||
// name
|
||||
address: string
|
||||
profile_picture: string
|
||||
profile_picture_thumbnail: string
|
||||
twitter_acct: string
|
||||
verified: boolean
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "stargaze-studio",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.3",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@ -24,6 +24,7 @@
|
||||
"@cosmos-kit/react": "^2.9.3",
|
||||
"@fontsource/jetbrains-mono": "^4",
|
||||
"@fontsource/roboto": "^4",
|
||||
"@headlessui/react": "^1",
|
||||
"@leapwallet/cosmos-snap-provider": "0.1.24",
|
||||
"@pinata/sdk": "^1.1.26",
|
||||
"@popperjs/core": "^2",
|
||||
@ -37,9 +38,6 @@
|
||||
"compare-versions": "^4",
|
||||
"daisyui": "^2.19.0",
|
||||
"html-to-image": "1.11.11",
|
||||
"@headlessui/react": "1.7.17",
|
||||
"@headlessui/tailwindcss": "0.2.0",
|
||||
"@heroicons/react": "2.0.18",
|
||||
"jscrypto": "^1.0.3",
|
||||
"match-sorter": "^6",
|
||||
"next": "^12",
|
||||
@ -72,7 +70,6 @@
|
||||
"object-sizeof": "^1.6.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3",
|
||||
"tailwind-merge": "1.14.0",
|
||||
"typescript": "^4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
@ -1,57 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
|
||||
/* eslint-disable jsx-a11y/img-redundant-alt */
|
||||
import { truncateAddress } from 'utils/wallet'
|
||||
|
||||
export interface ClickableCollection {
|
||||
contractAddress: string
|
||||
name: string
|
||||
media: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export default function CollectionsTable({ collections }: { collections: ClickableCollection[] }) {
|
||||
return (
|
||||
<table className="w-full divide-y divide-zinc-800 table-fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-3.5 pr-3 pl-4 text-sm text-left sm:pl-0 text-infinity-blue" scope="col">
|
||||
Name
|
||||
</th>
|
||||
<th className="py-3.5 px-3 text-sm text-left text-infinity-blue" scope="col">
|
||||
Address
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className=" bg-black">
|
||||
{collections
|
||||
? collections.map((collection) => (
|
||||
<tr
|
||||
key={collection.contractAddress}
|
||||
className="hover:bg-zinc-900 cursor-pointer"
|
||||
onClick={collection.onClick}
|
||||
>
|
||||
<td className="py-2 pr-3 pl-4 whitespace-nowrap sm:pl-0">
|
||||
<div className="flex items-center">
|
||||
<div className="shrink-0 w-11 h-11">
|
||||
<img alt="Collection Image" src={collection.media} />
|
||||
</div>
|
||||
<div className="ml-4 font-medium text-white truncate">{collection.name}</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="py-5 px-3 text-zinc-400 whitespace-nowrap">
|
||||
<div className="text-left text-white">
|
||||
{collection.contractAddress.startsWith('stars')
|
||||
? truncateAddress(collection.contractAddress)
|
||||
: collection.contractAddress}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable jsx-a11y/img-redundant-alt */
|
||||
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
|
||||
import Input from 'components/Input'
|
||||
import useSearch from 'hooks/useSearch'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useDebounce } from 'utils/debounce'
|
||||
import { truncateAddress } from 'utils/wallet'
|
||||
|
||||
interface ClickableCollection {
|
||||
contractAddress: string
|
||||
name: string
|
||||
media: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export default function SelectCollection({
|
||||
title,
|
||||
setCollection,
|
||||
}: {
|
||||
title?: string
|
||||
setCollection: (collectionAddress: string) => void
|
||||
}) {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const debouncedQuery = useDebounce<string>(search, 200)
|
||||
const collectionsQuery = useSearch(debouncedQuery, ['collections'], 10)
|
||||
const collectionsResults = useMemo(() => {
|
||||
return collectionsQuery.data?.find((searchResult) => searchResult.indexUid === 'collections')
|
||||
}, [collectionsQuery.data])
|
||||
|
||||
const clickableCollections = useMemo(() => {
|
||||
return (
|
||||
collectionsResults?.hits.map((hit) => ({
|
||||
contractAddress: hit.id,
|
||||
name: hit.name,
|
||||
media: hit.thumbnail_url || hit.image_url,
|
||||
onClick: () => {
|
||||
setCollection(hit.id)
|
||||
},
|
||||
})) ?? []
|
||||
)
|
||||
}, [collectionsResults?.hits, setCollection])
|
||||
|
||||
return (
|
||||
<div className="flex overflow-auto flex-col p-6 w-full h-[580px] bg-black border-2 border-solid border-infinity-blue">
|
||||
<h2 className="mb-4 text-lg font-bold text-infinity-blue">{title ?? 'Select the NFT collection to trade'}</h2>
|
||||
<Input
|
||||
className="mb-4 w-1/2 !rounded-none"
|
||||
icon={<MagnifyingGlassIcon className="w-5 h-5 text-zinc-400" />}
|
||||
id="collection-search"
|
||||
onChange={(e: any) => setSearch(e.target.value)}
|
||||
placeholder="Search Collections..."
|
||||
value={search}
|
||||
/>
|
||||
|
||||
<CollectionsTable collections={clickableCollections} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CollectionsTable = ({ collections }: { collections: ClickableCollection[] }) => {
|
||||
return (
|
||||
<table className="min-w-full divide-y divide-zinc-800">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-3.5 pr-3 pl-4 font-bold text-left sm:pl-0 text-infinity-blue" scope="col">
|
||||
Name
|
||||
</th>
|
||||
<th className="py-3.5 px-3 font-bold text-left text-infinity-blue" scope="col">
|
||||
Address
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className=" bg-black">
|
||||
{collections.map((collection) => (
|
||||
<tr
|
||||
key={collection.contractAddress}
|
||||
className="hover:bg-zinc-900 cursor-pointer"
|
||||
onClick={collection.onClick}
|
||||
>
|
||||
<td className="py-2 pr-3 pl-4 whitespace-nowrap sm:pl-0">
|
||||
<div className="flex items-center">
|
||||
<div className="shrink-0 w-11 h-11">
|
||||
<img alt="Collection Image" src={collection.media} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-medium text-white">{collection.name}</div>
|
||||
<div className="text-zinc-400">{truncateAddress(collection.contractAddress)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="py-5 px-3 text-zinc-400 whitespace-nowrap">
|
||||
<div className="text-left text-white">{truncateAddress(collection.contractAddress, 8, 6)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
|
||||
import Input from 'components/Input'
|
||||
import useSearch from 'hooks/useSearch'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useDebounce } from 'utils/debounce'
|
||||
|
||||
import CollectionsTable from './CollectionsTable'
|
||||
|
||||
export default function SelectCollectionModal({
|
||||
selectCollection,
|
||||
}: {
|
||||
selectCollection: (collectionAddress: string) => void
|
||||
}) {
|
||||
const [search, setSearch] = useState('')
|
||||
const [isInputFocused, setInputFocus] = useState(false)
|
||||
|
||||
const debouncedQuery = useDebounce<string>(search, 200)
|
||||
const debouncedIsInputFocused = useDebounce<boolean>(isInputFocused, 200)
|
||||
const collectionsQuery = useSearch(debouncedQuery, ['collections'], 5)
|
||||
const collectionsResults = useMemo(() => {
|
||||
return collectionsQuery.data?.find((searchResult) => searchResult.indexUid === 'collections')
|
||||
}, [collectionsQuery.data])
|
||||
|
||||
const clickableCollections = useMemo(() => {
|
||||
return (
|
||||
collectionsResults?.hits.map((hit) => ({
|
||||
contractAddress: hit.id,
|
||||
name: hit.name,
|
||||
media: hit.thumbnail_url || hit.image_url,
|
||||
onClick: () => {
|
||||
selectCollection(hit.id)
|
||||
setSearch(hit.name)
|
||||
},
|
||||
})) ?? []
|
||||
)
|
||||
}, [collectionsResults, selectCollection])
|
||||
|
||||
const handleInputFocus = () => {
|
||||
setInputFocus(true)
|
||||
}
|
||||
|
||||
const handleInputBlur = () => {
|
||||
setInputFocus(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-4 space-y-4 w-3/4 h-full bg-black rounded-md border-2 border-gray-600 border-solid md:p-6">
|
||||
<p className="text-base font-bold text-white text-start">Select the NFT collection to take a snapshot for</p>
|
||||
<Input
|
||||
className="py-2 w-full text-black dark:text-white rounded-sm md:w-72"
|
||||
icon={<MagnifyingGlassIcon className="w-5 h-5 text-zinc-400" />}
|
||||
id="collection-search"
|
||||
onBlur={handleInputBlur}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onFocus={handleInputFocus}
|
||||
placeholder="Search Collections..."
|
||||
value={search}
|
||||
/>
|
||||
|
||||
{debouncedIsInputFocused && (
|
||||
<div className="overflow-auto w-full">
|
||||
<CollectionsTable collections={clickableCollections} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
/* 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/restrict-template-expressions */
|
||||
/* eslint-disable tailwindcss/classnames-order */
|
||||
/* eslint-disable react/button-has-type */
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { AddressInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import type { NextPage } from 'next'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
// import Brand from 'public/brand/brand.svg'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
|
||||
import SelectCollectionModal from './SelectCollectionModal'
|
||||
|
||||
const Snapshots: NextPage = () => {
|
||||
const [collectionAddress, setCollectionAddress] = useState<string>('')
|
||||
const collectionAddressState = useInputState({
|
||||
id: 'collection-address',
|
||||
name: 'collection-address',
|
||||
title: 'Collection Address',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const snapshotEndpoint = `https://metabase.constellations.zone/api/public/card/b5764fb2-9a23-4ecf-866b-dec79c4c461e/query/json?parameters=%5B%7B%22type%22%3A%22category%22%2C%22value%22%3A%22${collectionAddressState.value}%22%2C%22id%22%3A%22cb34b7a8-70cf-ba86-8d9c-360b5b2fedd3%22%2C%22target%22%3A%5B%22variable%22%2C%5B%22template-tag%22%2C%22collection_addr%22%5D%5D%7D%5D`
|
||||
// function to download .json from the endpoint
|
||||
const download = (content: string, fileName: string, contentType: string) => {
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([content], { type: contentType })
|
||||
a.href = URL.createObjectURL(file)
|
||||
a.download = fileName
|
||||
a.click()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
collectionAddressState.onChange(collectionAddress)
|
||||
}, [collectionAddress])
|
||||
|
||||
return (
|
||||
<section className="px-4 pt-4 pb-16 mx-auto space-y-8 ml-8 w-full">
|
||||
<NextSeo title="Snapshots" />
|
||||
<ContractPageHeader
|
||||
description="Here you can export the snapshot of a collection's holders."
|
||||
link=""
|
||||
title="Snapshots"
|
||||
/>
|
||||
<SelectCollectionModal selectCollection={setCollectionAddress} />
|
||||
<AddressInput className="w-3/4" {...collectionAddressState} />
|
||||
<button
|
||||
className="px-4 py-2 font-bold text-white bg-stargaze rounded-md"
|
||||
onClick={() => {
|
||||
fetch(snapshotEndpoint)
|
||||
.then((response) => response.json())
|
||||
// data = [{"address":"stars15y38ehvexp6275ptmm4jj3qdds379nk07tw95r","num_owned":271}]
|
||||
// export a csv file with adding the header address,amount at the top from the array in the json
|
||||
.then((data) => {
|
||||
if (data.length === 0) {
|
||||
toast.error('No holders were found for the given collection address.')
|
||||
return
|
||||
}
|
||||
const csv = `address,amount\n${data.map((row: any) => Object.values(row).join(',')).join('\n')}`
|
||||
download(csv, 'snapshot.csv', 'text/csv')
|
||||
})
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
Export Snapshot
|
||||
</button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default withMetadata(Snapshots, { center: false })
|
@ -99,6 +99,3 @@ export const STARGAZE_URL = process.env.NEXT_PUBLIC_STARGAZE_WEBSITE_URL
|
||||
export const BLOCK_EXPLORER_URL = process.env.NEXT_PUBLIC_BLOCK_EXPLORER_URL
|
||||
export const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL
|
||||
export const SYNC_COLLECTIONS_API_URL = process.env.NEXT_PUBLIC_SYNC_COLLECTIONS_API_URL
|
||||
|
||||
export const MEILISEARCH_HOST = process.env.NEXT_PUBLIC_MEILISEARCH_HOST
|
||||
export const MEILISEARCH_API_KEY = process.env.NEXT_PUBLIC_MEILISEARCH_API_KEY
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function classNames(...classes: (false | null | undefined | string)[]) {
|
||||
return twMerge(classes.filter(Boolean).join(' '))
|
||||
}
|
@ -16,23 +16,3 @@ export const useWallet = () => {
|
||||
|
||||
return useCosmosKitChain(chain.chain_name)
|
||||
}
|
||||
|
||||
export function truncateAddress(address?: string | null, visibleFirst = 8, visibleLast = 4) {
|
||||
if (typeof address !== 'string') {
|
||||
return ''
|
||||
}
|
||||
return address
|
||||
? `${address.substring(0, visibleFirst)}...${address.substring(address.length - visibleLast, address.length)}`
|
||||
: null
|
||||
}
|
||||
|
||||
export function truncateName(name: string, maxLength = 17) {
|
||||
let truncatedName = name
|
||||
|
||||
if (name.length > maxLength) {
|
||||
const firstPart = name.substring(0, maxLength / 2)
|
||||
const secondPart = name.substring(name.length - 5) // stars
|
||||
truncatedName = `${firstPart}...${secondPart}`
|
||||
}
|
||||
return truncatedName
|
||||
}
|
||||
|
30
yarn.lock
30
yarn.lock
@ -2405,22 +2405,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@formkit/auto-animate/-/auto-animate-1.0.0-beta.6.tgz#ed7f8bc47d774a7764756646e9e3432e8be51cb3"
|
||||
integrity sha512-PVDhLAlr+B4Xb7e+1wozBUWmXa6BFU8xUPR/W/E+TsQhPS1qkAdAsJ25keEnFrcePSnXHrOsh3tiFbEToOzV9w==
|
||||
|
||||
"@headlessui/react@1.7.17":
|
||||
version "1.7.17"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.17.tgz#a0ec23af21b527c030967245fd99776aa7352bc6"
|
||||
integrity sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==
|
||||
dependencies:
|
||||
client-only "^0.0.1"
|
||||
|
||||
"@headlessui/tailwindcss@0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/tailwindcss/-/tailwindcss-0.2.0.tgz#2c55c98fd8eee4b4f21ec6eb35a014b840059eec"
|
||||
integrity sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==
|
||||
|
||||
"@heroicons/react@2.0.18":
|
||||
version "2.0.18"
|
||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.18.tgz#f80301907c243df03c7e9fd76c0286e95361f7c1"
|
||||
integrity sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw==
|
||||
"@headlessui/react@^1":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.6.0.tgz"
|
||||
integrity sha512-PlDuytBC6iDC/uMvpANm5VpRSuayyXMEeo/dNIwAZNHCfhZUqDQgLXjGu48SHsvMw22Kc3c3u9TOAMZNg+1vzw==
|
||||
|
||||
"@humanwhocodes/config-array@^0.9.2":
|
||||
version "0.9.5"
|
||||
@ -4604,11 +4592,6 @@ cli-truncate@^3.1.0:
|
||||
slice-ansi "^5.0.0"
|
||||
string-width "^5.0.0"
|
||||
|
||||
client-only@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
clsx@^1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz"
|
||||
@ -8686,11 +8669,6 @@ tabbable@^6.0.1, tabbable@^6.2.0:
|
||||
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
|
||||
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
|
||||
|
||||
tailwind-merge@1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b"
|
||||
integrity sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==
|
||||
|
||||
tailwindcss-opentype@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss-opentype/-/tailwindcss-opentype-1.1.0.tgz#68aebc6f8a24151167b0a4f372370afe375a3b38"
|
||||
|
Loading…
Reference in New Issue
Block a user