StargazeTools init
14
.env.example
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
APP_VERSION=0.1.0
|
||||||
|
|
||||||
|
NEXT_PUBLIC_CW721_BASE_CODE_ID=5
|
||||||
|
|
||||||
|
NEXT_PUBLIC_API_URL=https://
|
||||||
|
NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze
|
||||||
|
NEXT_PUBLIC_NETWORK=testnet
|
||||||
|
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
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* @orkunkl @findolor @kaymakf
|
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: '[BUG]'
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
23
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
Fixes #<ISSUE_NUMBER>
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Describe big picture changes here. This will give viewers context for the changes.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
List any technical changes.
|
||||||
|
|
||||||
|
- [ ] Change 1
|
||||||
|
|
||||||
|
## Screenshots (if appropriate):
|
||||||
|
|
||||||
|
## Testing Steps
|
||||||
|
|
||||||
|
As a reviewer, what steps should I take to verify this is working correctly?
|
||||||
|
|
||||||
|
- [ ] Step 1
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
Add links to Figma files, documentation, etc.
|
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
*.log
|
||||||
|
*.tsbuildinfo
|
||||||
|
.DS_Store
|
||||||
|
.env*
|
||||||
|
.vercel
|
||||||
|
.next/
|
||||||
|
node_modules/
|
||||||
|
out/
|
||||||
|
|
||||||
|
!.env.example
|
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
yarn lint-staged
|
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.next/**
|
||||||
|
dist/**
|
||||||
|
node_modules/**
|
||||||
|
out/**
|
28
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
},
|
||||||
|
"css.validate": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"eslint.packageManager": "yarn",
|
||||||
|
"eslint.workingDirectories": [
|
||||||
|
{
|
||||||
|
"!cwd": false,
|
||||||
|
"pattern": "packages/*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"!cwd": false,
|
||||||
|
"directory": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"npm.packageManager": "yarn"
|
||||||
|
}
|
53
README.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<!-- markdownlint-disable MD033 MD034 MD036 MD041 -->
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# stargaze-tools
|
||||||
|
|
||||||
|
- Mainnet website: https://
|
||||||
|
- Testnet website: https://
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
**Required**
|
||||||
|
|
||||||
|
- Git
|
||||||
|
- Node.js 14 or LTS
|
||||||
|
- Yarn
|
||||||
|
- Keplr Wallet browser extension
|
||||||
|
|
||||||
|
**Optional**
|
||||||
|
|
||||||
|
- S3 bucket instance (minio, etc.)
|
||||||
|
|
||||||
|
## Setup local development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# clone repository
|
||||||
|
git clone https://github.com/deus-labs/stargaze-tools.git
|
||||||
|
cd stargaze-tools
|
||||||
|
|
||||||
|
# install dependencies
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# copy env file and fill in values
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# run development server
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# (optional) lint and format project
|
||||||
|
yarn lint
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- https://docs.stargaze.zone
|
||||||
|
|
||||||
|
## Questions
|
||||||
|
|
||||||
|
- [Discord](https://discord.gg/stargaze)
|
||||||
|
- [Telegram](https://t.me/joinchat/ZQ95YmIn3AI0ODFh)
|
||||||
|
- [Twitter](https://twitter.com/stargazezone)
|
||||||
|
|
||||||
|
<img src="./public/icon.png" height="96" align="right" />
|
42
components/Alert.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
import { FaExclamationTriangle, FaInfoCircle, FaTimes } from 'react-icons/fa'
|
||||||
|
import type { IconType } from 'react-icons/lib'
|
||||||
|
|
||||||
|
export type AlertType = 'info' | 'warning' | 'error' | 'ghost'
|
||||||
|
|
||||||
|
const ALERT_ICONS_MAP: Record<AlertType, IconType | null> = {
|
||||||
|
info: FaInfoCircle,
|
||||||
|
warning: FaExclamationTriangle,
|
||||||
|
error: FaTimes,
|
||||||
|
ghost: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlertProps extends ComponentProps<'div'> {
|
||||||
|
type?: AlertType
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Alert = (props: AlertProps) => {
|
||||||
|
const { type = 'info', className, children, ...rest } = props
|
||||||
|
const AlertIcon = ALERT_ICONS_MAP[type]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'flex relative p-4 space-x-4 border-l-4',
|
||||||
|
{ 'bg-blue-500/25 border-blue-500': type === 'info' },
|
||||||
|
{ 'bg-yellow-500/25 border-yellow-500': type === 'warning' },
|
||||||
|
{ 'bg-red-500/25 border-red-500': type === 'error' },
|
||||||
|
{ 'bg-stone-500/25 border-stone-500': type === 'ghost' },
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{AlertIcon && <AlertIcon size={24} />}
|
||||||
|
<div className="flex flex-col flex-grow space-y-2">
|
||||||
|
{children}
|
||||||
|
{/* */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
35
components/Anchor.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import type { ComponentProps, ComponentType } from 'react'
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
|
export interface AnchorProps extends ComponentProps<'a'> {
|
||||||
|
external?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adaptive link component that can be used for both relative Next.js pages and
|
||||||
|
* external links, recommended for sidebar and footer links
|
||||||
|
*
|
||||||
|
* @see https://nextjs.org/docs/api-reference/next/link
|
||||||
|
*/
|
||||||
|
export function Anchor({ children, external, href = '', rel = '', ...rest }: AnchorProps) {
|
||||||
|
const isApi = href.startsWith('/api')
|
||||||
|
const isRelative = href.startsWith('/')
|
||||||
|
const isExternal = typeof external === 'boolean' ? external : isApi || !isRelative
|
||||||
|
|
||||||
|
const Wrap = (isExternal ? Fragment : Link) as ComponentType<any>
|
||||||
|
const wrapProps = isExternal ? {} : { href }
|
||||||
|
const linkProps = isExternal ? { target: '_blank', rel: `noopener noreferrer ${rel}` } : { rel }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrap {...wrapProps}>
|
||||||
|
<a {...rest} {...linkProps} href={href}>
|
||||||
|
{children ?? (href ? trimHttp(href) : null)}
|
||||||
|
</a>
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimHttp(str: string) {
|
||||||
|
return str.replace(/https?:\/\//, '')
|
||||||
|
}
|
36
components/AnchorButton.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { AnchorProps } from 'components/Anchor'
|
||||||
|
import { Anchor } from 'components/Anchor'
|
||||||
|
|
||||||
|
export type ButtonVariant = 'solid' | 'outline'
|
||||||
|
|
||||||
|
export interface AnchorButtonProps extends AnchorProps {
|
||||||
|
isDisabled?: boolean
|
||||||
|
isWide?: boolean
|
||||||
|
leftIcon?: JSX.Element
|
||||||
|
rightIcon?: JSX.Element
|
||||||
|
variant?: ButtonVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnchorButton = (props: AnchorButtonProps) => {
|
||||||
|
const { isWide, leftIcon, rightIcon, variant = 'solid', className, children, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Anchor
|
||||||
|
className={clsx(
|
||||||
|
'group flex items-center py-2 space-x-2 font-bold focus:ring',
|
||||||
|
isWide ? 'px-8' : 'px-4',
|
||||||
|
{
|
||||||
|
'bg-plumbus-60 hover:bg-plumbus-50 rounded ': variant === 'solid',
|
||||||
|
'bg-plumbus/10 hover:bg-plumbus/20 rounded border border-plumbus': variant === 'outline',
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{leftIcon}
|
||||||
|
<div className="group-hover:underline">{children}</div>
|
||||||
|
{rightIcon}
|
||||||
|
</Anchor>
|
||||||
|
)
|
||||||
|
}
|
33
components/BrandColorPicker.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { Dispatch, SetStateAction } from 'react'
|
||||||
|
|
||||||
|
export const BRAND_COLORS = ['plumbus', 'black', 'white'] as const
|
||||||
|
export type BrandColor = typeof BRAND_COLORS[number]
|
||||||
|
|
||||||
|
export interface BrandColorPickerProps {
|
||||||
|
onChange: Dispatch<SetStateAction<BrandColor>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BrandColorPicker = ({ onChange }: BrandColorPickerProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm font-bold">Change color:</span>
|
||||||
|
{BRAND_COLORS.map((color) => (
|
||||||
|
<button
|
||||||
|
key={`change-color-${color}`}
|
||||||
|
className={clsx(
|
||||||
|
'w-8 h-8 rounded border border-white/20',
|
||||||
|
'hover:ring-2 focus:ring-2 ring-white/50 transition',
|
||||||
|
{
|
||||||
|
'bg-plumbus': color === 'plumbus',
|
||||||
|
'bg-black': color === 'black',
|
||||||
|
'bg-white': color === 'white',
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onClick={() => onChange(color)}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
53
components/BrandPreview.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { BrandColor } from 'components/BrandColorPicker'
|
||||||
|
import { BrandColorPicker } from 'components/BrandColorPicker'
|
||||||
|
import type { ComponentType, SVGProps } from 'react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { FaDownload } from 'react-icons/fa'
|
||||||
|
|
||||||
|
export interface BrandPreviewProps {
|
||||||
|
name: string
|
||||||
|
id?: string
|
||||||
|
url?: string
|
||||||
|
Asset: ComponentType<SVGProps<SVGSVGElement>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BrandPreview = ({ name, id = '', url, Asset }: BrandPreviewProps) => {
|
||||||
|
const [color, setColor] = useState<BrandColor>('plumbus')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center space-x-4">
|
||||||
|
<a className="text-2xl font-bold hover:underline" href={`#${id}`} id={id}>
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
{url && (
|
||||||
|
<a
|
||||||
|
className={clsx(
|
||||||
|
'flex items-center py-2 px-4 space-x-2 bg-plumbus-60 rounded',
|
||||||
|
'hover:bg-plumbus-70 transition hover:translate-y-[-2px]',
|
||||||
|
)}
|
||||||
|
download
|
||||||
|
href={`/${url}`}
|
||||||
|
>
|
||||||
|
<FaDownload />
|
||||||
|
<span className="font-bold">Download SVG</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center items-center p-16 max-h-[400px] bg-black/20 rounded">
|
||||||
|
<Asset
|
||||||
|
className={clsx('transition', {
|
||||||
|
'text-plumbus': color === 'plumbus',
|
||||||
|
'text-black': color === 'black',
|
||||||
|
'text-white': color === 'white',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<BrandColorPicker onChange={setColor} />
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
41
components/Button.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
import { CgSpinnerAlt } from 'react-icons/cg'
|
||||||
|
|
||||||
|
export type ButtonVariant = 'solid' | 'outline'
|
||||||
|
|
||||||
|
export interface ButtonProps extends ComponentProps<'button'> {
|
||||||
|
isDisabled?: boolean
|
||||||
|
isLoading?: boolean
|
||||||
|
isWide?: boolean
|
||||||
|
leftIcon?: JSX.Element
|
||||||
|
rightIcon?: JSX.Element
|
||||||
|
variant?: ButtonVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = (props: ButtonProps) => {
|
||||||
|
const { isDisabled, isLoading, isWide, leftIcon, rightIcon, variant = 'solid', className, children, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'group flex items-center py-2 space-x-2 font-bold focus:ring',
|
||||||
|
isWide ? 'px-8' : 'px-4',
|
||||||
|
{
|
||||||
|
'bg-plumbus-60 hover:bg-plumbus-50 rounded ': variant === 'solid',
|
||||||
|
'bg-plumbus/10 hover:bg-plumbus/20 rounded border border-plumbus': variant === 'outline',
|
||||||
|
'opacity-50 cursor-not-allowed pointer-events-none': isDisabled,
|
||||||
|
'animate-pulse cursor-wait pointer-events-none': isLoading,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
type="button"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{isLoading ? <CgSpinnerAlt className="animate-spin" /> : leftIcon}
|
||||||
|
<div>{children}</div>
|
||||||
|
{!isLoading && rightIcon}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
11
components/Conditional.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
export interface ConditionalProps {
|
||||||
|
test: boolean
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Conditional = ({ test, children }: ConditionalProps) => {
|
||||||
|
if (!test) return null
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
21
components/ContractPageHeader.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Anchor } from 'components/Anchor'
|
||||||
|
|
||||||
|
import { PageHeader } from './PageHeader'
|
||||||
|
|
||||||
|
interface ContractPageHeaderProps {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
link: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContractPageHeader = ({ title, description, link }: ContractPageHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<PageHeader title={title}>
|
||||||
|
{description} Learn more in the{' '}
|
||||||
|
<Anchor className="font-bold text-plumbus hover:underline" href={link}>
|
||||||
|
documentation
|
||||||
|
</Anchor>
|
||||||
|
.
|
||||||
|
</PageHeader>
|
||||||
|
)
|
||||||
|
}
|
29
components/DefaultAppSeo.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { meta } from 'config/meta'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { DefaultSeo } from 'next-seo'
|
||||||
|
import { WEBSITE_URL } from 'utils/constants'
|
||||||
|
|
||||||
|
export const DefaultAppSeo = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaultSeo
|
||||||
|
canonical={meta.url + (router.asPath || '')}
|
||||||
|
defaultTitle={meta.name}
|
||||||
|
description={meta.description}
|
||||||
|
openGraph={{
|
||||||
|
title: meta.name,
|
||||||
|
description: meta.description,
|
||||||
|
type: 'website',
|
||||||
|
site_name: meta.name,
|
||||||
|
images: [{ url: `${WEBSITE_URL}/social.png` }],
|
||||||
|
}}
|
||||||
|
titleTemplate={`%s | ${meta.name}`}
|
||||||
|
twitter={{
|
||||||
|
cardType: 'summary_large_image',
|
||||||
|
handle: meta.twitter.username,
|
||||||
|
site: meta.twitter.username,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
162
components/FaviconsMetaTags.jsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
export function FaviconsMetaTags() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<link href="/assets/favicon.ico" rel="shortcut icon" />
|
||||||
|
<link href="/assets/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png" />
|
||||||
|
<link href="/assets/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png" />
|
||||||
|
<link href="/assets/favicon-48x48.png" rel="icon" sizes="48x48" type="image/png" />
|
||||||
|
<link href="/assets/manifest.webmanifest" rel="manifest" />
|
||||||
|
<meta content="yes" name="mobile-web-app-capable" />
|
||||||
|
<meta content="#F0827D" name="theme-color" />
|
||||||
|
<meta content="StargazeTools" name="application-name" />
|
||||||
|
<link href="/assets/apple-touch-icon-57x57.png" rel="apple-touch-icon" sizes="57x57" />
|
||||||
|
<link href="/assets/apple-touch-icon-60x60.png" rel="apple-touch-icon" sizes="60x60" />
|
||||||
|
<link href="/assets/apple-touch-icon-72x72.png" rel="apple-touch-icon" sizes="72x72" />
|
||||||
|
<link href="/assets/apple-touch-icon-76x76.png" rel="apple-touch-icon" sizes="76x76" />
|
||||||
|
<link href="/assets/apple-touch-icon-114x114.png" rel="apple-touch-icon" sizes="114x114" />
|
||||||
|
<link href="/assets/apple-touch-icon-120x120.png" rel="apple-touch-icon" sizes="120x120" />
|
||||||
|
<link href="/assets/apple-touch-icon-144x144.png" rel="apple-touch-icon" sizes="144x144" />
|
||||||
|
<link href="/assets/apple-touch-icon-152x152.png" rel="apple-touch-icon" sizes="152x152" />
|
||||||
|
<link href="/assets/apple-touch-icon-167x167.png" rel="apple-touch-icon" sizes="167x167" />
|
||||||
|
<link href="/assets/apple-touch-icon-180x180.png" rel="apple-touch-icon" sizes="180x180" />
|
||||||
|
<link href="/assets/apple-touch-icon-1024x1024.png" rel="apple-touch-icon" sizes="1024x1024" />
|
||||||
|
<meta content="yes" name="apple-mobile-web-app-capable" />
|
||||||
|
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style" />
|
||||||
|
<meta content="StargazeTools" name="apple-mobile-web-app-title" />
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-640x1136.png"
|
||||||
|
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1136x640.png"
|
||||||
|
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-750x1334.png"
|
||||||
|
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1334x750.png"
|
||||||
|
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1125x2436.png"
|
||||||
|
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2436x1125.png"
|
||||||
|
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1170x2532.png"
|
||||||
|
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2532x1170.png"
|
||||||
|
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-828x1792.png"
|
||||||
|
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1792x828.png"
|
||||||
|
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1242x2688.png"
|
||||||
|
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2688x1242.png"
|
||||||
|
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1242x2208.png"
|
||||||
|
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2208x1242.png"
|
||||||
|
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1284x2778.png"
|
||||||
|
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2778x1284.png"
|
||||||
|
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1536x2048.png"
|
||||||
|
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2048x1536.png"
|
||||||
|
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1620x2160.png"
|
||||||
|
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2160x1620.png"
|
||||||
|
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1668x2388.png"
|
||||||
|
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2388x1668.png"
|
||||||
|
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-1668x2224.png"
|
||||||
|
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2224x1668.png"
|
||||||
|
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2048x2732.png"
|
||||||
|
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="/assets/apple-touch-startup-image-2732x2048.png"
|
||||||
|
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||||
|
rel="apple-touch-startup-image"
|
||||||
|
/>
|
||||||
|
<meta content="#F0827D" name="msapplication-TileColor" />
|
||||||
|
<meta content="/assets/mstile-144x144.png" name="msapplication-TileImage" />
|
||||||
|
<meta content="/assets/browserconfig.xml" name="msapplication-config" />
|
||||||
|
<link href="/assets/yandex-browser-manifest.json" rel="yandex-tableau-widget" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
30
components/FormControl.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps, ElementType, ReactNode } from 'react'
|
||||||
|
|
||||||
|
export interface FormControlProps extends ComponentProps<'div'> {
|
||||||
|
title: string
|
||||||
|
subtitle?: ReactNode
|
||||||
|
htmlId?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
labelAs?: ElementType
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormControl = (props: FormControlProps) => {
|
||||||
|
const { title, subtitle, htmlId, isRequired, children, className, labelAs: Label = 'label', ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx('flex flex-col space-y-2', className)} {...rest}>
|
||||||
|
<Label className="flex flex-col space-y-1" htmlFor={htmlId}>
|
||||||
|
<span
|
||||||
|
className={clsx('font-bold first-letter:capitalize', {
|
||||||
|
"after:text-red-500 after:content-['_*']": isRequired,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
{subtitle && <span className="text-sm text-white/50">{subtitle}</span>}
|
||||||
|
</Label>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
23
components/FormGroup.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
export interface FormGroupProps {
|
||||||
|
title: string
|
||||||
|
subtitle: ReactNode
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormGroup = (props: FormGroupProps) => {
|
||||||
|
const { title, subtitle, children } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex p-4 space-x-4 w-full">
|
||||||
|
<div className="flex flex-col w-1/3">
|
||||||
|
<label className="flex flex-col space-y-1">
|
||||||
|
<span className="font-bold">{title}</span>
|
||||||
|
{subtitle && <span className="text-sm text-white/50">{subtitle}</span>}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4 w-2/3">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
32
components/HomeCard.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { Anchor } from 'components/Anchor'
|
||||||
|
import type { ComponentProps, ReactNode } from 'react'
|
||||||
|
import { FaArrowRight } from 'react-icons/fa'
|
||||||
|
|
||||||
|
export interface HomeCardProps extends ComponentProps<'div'> {
|
||||||
|
title: string
|
||||||
|
link?: string
|
||||||
|
linkText?: string
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HomeCard = (props: HomeCardProps) => {
|
||||||
|
const { title, link, linkText, children, className, ...rest } = props
|
||||||
|
return (
|
||||||
|
<div className={clsx('flex relative flex-col space-y-4', className)} {...rest}>
|
||||||
|
<h2 className="font-heading text-xl font-bold">{title}</h2>
|
||||||
|
<p className="flex-grow text-white/75">{children}</p>
|
||||||
|
{link && (
|
||||||
|
<Anchor
|
||||||
|
className={clsx(
|
||||||
|
'flex before:absolute before:inset-0 items-center space-x-1',
|
||||||
|
'font-bold text-plumbus hover:underline',
|
||||||
|
)}
|
||||||
|
href={link}
|
||||||
|
>
|
||||||
|
<span>{linkText ?? 'Open Link'}</span> <FaArrowRight size={12} />
|
||||||
|
</Anchor>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
4
components/Input.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { StyledInput } from './forms/StyledInput'
|
||||||
|
|
||||||
|
/** @deprecated - replace with {@link StyledInput} */
|
||||||
|
export const Input = StyledInput
|
20
components/InputDateTime.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { DateTimePickerProps } from 'react-datetime-picker/dist/entry.nostyle'
|
||||||
|
import DateTimePicker from 'react-datetime-picker/dist/entry.nostyle'
|
||||||
|
import { FaCalendar, FaTimes } from 'react-icons/fa'
|
||||||
|
|
||||||
|
export const InputDateTime = ({ className, ...rest }: DateTimePickerProps) => {
|
||||||
|
return (
|
||||||
|
<DateTimePicker
|
||||||
|
calendarIcon={<FaCalendar className="text-white hover:text-white/80" />}
|
||||||
|
className={clsx(
|
||||||
|
'bg-white/10 rounded border-2 border-white/20 form-input',
|
||||||
|
'placeholder:text-white/50',
|
||||||
|
'focus:ring focus:ring-plumbus-20',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
clearIcon={<FaTimes className="text-plumbus-40 hover:text-plumbus-60" />}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
44
components/Issuebar.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { Anchor } from 'components/Anchor'
|
||||||
|
import { Button } from 'components/Button'
|
||||||
|
import { toggleSidebar, useSidebarStore } from 'contexts/sidebar'
|
||||||
|
import { FaChevronRight } from 'react-icons/fa'
|
||||||
|
import { FiLink2 } from 'react-icons/fi'
|
||||||
|
import { links } from 'utils/links'
|
||||||
|
|
||||||
|
export const Issuebar = () => {
|
||||||
|
const { isOpen } = useSidebarStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(isOpen ? 'min-w-[230px] max-w-[230px]' : 'min-w-[20px] max-w-[20px]', 'relative z-10')}>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'overflow-auto min-w-[230px] max-w-[230px] no-scrollbar',
|
||||||
|
'bg-black/50 border-l-[1px] border-l-plumbus-light',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-y-4 p-6 pt-8 min-h-screen">
|
||||||
|
<div>This is a beta version of StargazeTools.</div>
|
||||||
|
<div>We are open for your feedback and suggestions!</div>
|
||||||
|
<Anchor href={`${links.GitHub}/issues/new/choose`}>
|
||||||
|
<Button rightIcon={<FiLink2 />} variant="outline">
|
||||||
|
Submit an issue
|
||||||
|
</Button>
|
||||||
|
</Anchor>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* sidebar toggle */}
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'absolute top-[32px] left-[-12px] p-1 w-[24px] h-[24px]',
|
||||||
|
'text-black bg-plumbus-light rounded-full',
|
||||||
|
'hover:bg-plumbus',
|
||||||
|
)}
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
>
|
||||||
|
<FaChevronRight className={clsx('mx-auto', { 'rotate-180': !isOpen })} size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
74
components/JsonPreview.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { FaChevronDown, FaRegClipboard, FaTimes } from 'react-icons/fa'
|
||||||
|
import { copy } from 'utils/clipboard'
|
||||||
|
|
||||||
|
import { Tooltip } from './Tooltip'
|
||||||
|
|
||||||
|
export interface JsonPreviewProps {
|
||||||
|
title?: string
|
||||||
|
content: unknown
|
||||||
|
|
||||||
|
initialState?: boolean
|
||||||
|
onClose?: () => void
|
||||||
|
isCopyable?: boolean
|
||||||
|
isToggleable?: boolean
|
||||||
|
|
||||||
|
/** @deprecated use {@link isCopyable} prop */
|
||||||
|
copyable?: boolean
|
||||||
|
/** @deprecated use {@link initialState} prop */
|
||||||
|
isVisible?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const JsonPreview = ({
|
||||||
|
copyable = false,
|
||||||
|
isVisible = true,
|
||||||
|
|
||||||
|
title = 'JSON Preview',
|
||||||
|
content,
|
||||||
|
|
||||||
|
initialState = isVisible,
|
||||||
|
onClose,
|
||||||
|
isCopyable = copyable,
|
||||||
|
isToggleable = true,
|
||||||
|
}: JsonPreviewProps) => {
|
||||||
|
const [show, setShow] = useState(() => initialState)
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (isToggleable) setShow((prev) => !prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative bg-stone-800/80 rounded border-2 border-white/20 divide-y-2 divide-white/20">
|
||||||
|
<div className="flex items-center py-1 px-2 space-x-2">
|
||||||
|
<span className="text-sm font-bold">{title}</span>
|
||||||
|
<div className="flex-grow" />
|
||||||
|
{isToggleable && (
|
||||||
|
<Tooltip label={show ? 'Hide content' : 'Show content'}>
|
||||||
|
<button onClick={toggle} type="button">
|
||||||
|
<FaChevronDown className={show ? 'rotate-180' : ''} size={16} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{isCopyable && (
|
||||||
|
<Tooltip label="Copy to clipboard">
|
||||||
|
<button onClick={() => void copy(JSON.stringify(content))} type="button">
|
||||||
|
<FaRegClipboard size={16} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{onClose && (
|
||||||
|
<Tooltip label="Close">
|
||||||
|
<button className="text-plumbus" onClick={onClose} type="button">
|
||||||
|
<FaTimes size={20} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{show && (
|
||||||
|
<div className="overflow-auto p-2 font-mono text-sm">
|
||||||
|
<pre>{JSON.stringify(content, null, 2).trim()}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
57
components/Layout.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { FaDesktop } from 'react-icons/fa'
|
||||||
|
import type { PageMetadata } from 'utils/layout'
|
||||||
|
|
||||||
|
import { DefaultAppSeo } from './DefaultAppSeo'
|
||||||
|
// import { Issuebar } from './Issuebar'
|
||||||
|
import { Sidebar } from './Sidebar'
|
||||||
|
|
||||||
|
export interface LayoutProps {
|
||||||
|
metadata?: PageMetadata
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Layout = ({ children, metadata = {} }: LayoutProps) => {
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden relative">
|
||||||
|
<Head>
|
||||||
|
<meta content="minimum-scale=1, initial-scale=1, width=device-width" name="viewport" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<DefaultAppSeo />
|
||||||
|
|
||||||
|
{/* plumbus confetti */}
|
||||||
|
<div className="fixed inset-0 -z-10 pointer-events-none stargaze-gradient-bg opacity-50">
|
||||||
|
<img alt="plumbus carina-nebula" className="fixed top-0 right-0 w-full h-[calc(100vh+180px)]" src="/carina-nebula.png" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* actual layout container */}
|
||||||
|
<div className="hidden sm:flex">
|
||||||
|
<Sidebar />
|
||||||
|
<div className="overflow-auto relative flex-grow h-screen no-scrollbar">
|
||||||
|
<main
|
||||||
|
className={clsx('mx-auto max-w-7xl', {
|
||||||
|
'flex flex-col justify-center items-center':
|
||||||
|
typeof metadata.center === 'boolean' ? metadata.center : true,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{/* <Issuebar /> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-center items-center p-8 space-y-4 h-screen text-center bg-black/50 sm:hidden">
|
||||||
|
<FaDesktop size={48} />
|
||||||
|
<h1 className="text-2xl font-bold">Unsupported Viewport</h1>
|
||||||
|
<p>
|
||||||
|
StargazeTools is best viewed on the big screen.
|
||||||
|
<br />
|
||||||
|
Please open StargazeTools on your tablet or desktop browser.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
28
components/LinkTab.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { Anchor } from 'components/Anchor'
|
||||||
|
|
||||||
|
export interface LinkTabProps {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
href: string
|
||||||
|
isActive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkTab = (props: LinkTabProps) => {
|
||||||
|
const { title, description, href, isActive } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Anchor
|
||||||
|
className={clsx(
|
||||||
|
'isolate p-4 space-y-1 border-2',
|
||||||
|
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||||
|
isActive ? 'border-plumbus' : 'border-transparent',
|
||||||
|
isActive ? 'bg-plumbus/5 hover:bg-plumbus/10' : 'hover:bg-white/5',
|
||||||
|
)}
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
<h4 className="font-bold">{title}</h4>
|
||||||
|
<span className="text-sm text-white/80 line-clamp-2">{description}</span>
|
||||||
|
</Anchor>
|
||||||
|
)
|
||||||
|
}
|
19
components/LinkTabs.data.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { LinkTabProps } from './LinkTab'
|
||||||
|
|
||||||
|
export const cw721BaseLinkTabs: LinkTabProps[] = [
|
||||||
|
{
|
||||||
|
title: 'Instantiate',
|
||||||
|
description: `Create a new CW721 Base contract`,
|
||||||
|
href: '/contracts/cw721/base/instantiate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Query',
|
||||||
|
description: `Dispatch queries with your CW721 Base contract`,
|
||||||
|
href: '/contracts/cw721/base/query',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Execute',
|
||||||
|
description: `Execute CW721 Base contract actions`,
|
||||||
|
href: '/contracts/cw721/base/execute',
|
||||||
|
},
|
||||||
|
]
|
24
components/LinkTabs.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
import type { LinkTabProps } from './LinkTab'
|
||||||
|
import { LinkTab } from './LinkTab'
|
||||||
|
|
||||||
|
export interface LinkTabsProps {
|
||||||
|
data: LinkTabProps[]
|
||||||
|
activeIndex?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkTabs = ({ data, activeIndex }: LinkTabsProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'grid before:absolute relative grid-flow-col items-stretch rounded',
|
||||||
|
'before:inset-x-0 before:bottom-0 before:border-b-2 before:border-white/25',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<LinkTab key={index} {...item} isActive={index === activeIndex} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
83
components/Modal.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { Button } from 'components/Button'
|
||||||
|
import { Radio } from 'components/Radio'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { FaAsterisk } from 'react-icons/fa'
|
||||||
|
|
||||||
|
export const Modal = () => {
|
||||||
|
const [showModal, setShowModal] = useState(true)
|
||||||
|
const [isButtonDisabled, setIsButtonDisabled] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (localStorage.getItem('disclaimer')) {
|
||||||
|
setShowModal(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const accept = () => {
|
||||||
|
localStorage.setItem('disclaimer', '1')
|
||||||
|
setShowModal(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showModal ? (
|
||||||
|
<div className="flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 justify-center items-center outline-none focus:outline-none">
|
||||||
|
<div className="relative my-6 mx-auto w-auto max-w-3xl">
|
||||||
|
<div className="flex relative flex-col w-full bg-stone-800 rounded-lg border-[1px] border-slate-200/20 border-solid outline-none focus:outline-none shadow-lg">
|
||||||
|
<div className="flex justify-between items-start p-5 rounded-t border-b border-slate-200/20 border-solid">
|
||||||
|
<h3 className="text-3xl font-bold">Before using StargazeTools...</h3>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex-auto p-6 my-4">
|
||||||
|
<p className="text-lg leading-relaxed">
|
||||||
|
StargazeTools is a decentralized application where
|
||||||
|
individuals or communities can use smart contract dashboards
|
||||||
|
to create, mint and manage NFT collections.
|
||||||
|
<br />
|
||||||
|
StargazeTools is made up of free, public, and open-source
|
||||||
|
software that is built on top of Stargaze Network.
|
||||||
|
StargazeTools only provides tools for any of the mentioned
|
||||||
|
functionalities above and inside the dApp. Anyone can create
|
||||||
|
or mint NFT collections on StargazeTools and StargazeTools
|
||||||
|
does not audit or have any discretion on how these
|
||||||
|
collections are put to use. <br />
|
||||||
|
<br />
|
||||||
|
AS DESCRIBED IN THE DISCLAIMER, STARGAZETOOLS IS PROVIDED
|
||||||
|
“AS IS”, AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY
|
||||||
|
KIND. No developer or entity involved in creating the
|
||||||
|
StargazeTools will be liable for any claims or damages
|
||||||
|
whatsoever associated with your use, inability to use, or
|
||||||
|
your interaction with other users of the StargazeTools,
|
||||||
|
including any direct, indirect, incidental, special,
|
||||||
|
exemplary, punitive or consequential damages, or loss of
|
||||||
|
profits, tokens, or anything else.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Radio
|
||||||
|
checked={!isButtonDisabled}
|
||||||
|
htmlFor="disclaimer-accept"
|
||||||
|
id="disclaimer-accept"
|
||||||
|
onChange={() => setIsButtonDisabled(false)}
|
||||||
|
subtitle=""
|
||||||
|
title="I've read the disclaimer and I understand the risks of using StargazeTools."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end items-center p-6 mt-1">
|
||||||
|
<Button
|
||||||
|
className={clsx({ 'opacity-50': isButtonDisabled })}
|
||||||
|
disabled={isButtonDisabled}
|
||||||
|
isWide
|
||||||
|
leftIcon={<FaAsterisk />}
|
||||||
|
onClick={accept}
|
||||||
|
>
|
||||||
|
Enter StargazeTools
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
15
components/PageHeader.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
export interface PageHeaderProps {
|
||||||
|
title: string
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageHeader = ({ title, children }: PageHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="font-heading text-4xl font-bold">{title}</h1>
|
||||||
|
<p>{children}</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
41
components/Radio.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { ChangeEventHandler, ReactNode } from 'react'
|
||||||
|
|
||||||
|
export interface RadioProps<T = string> {
|
||||||
|
id: string
|
||||||
|
htmlFor: T
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
checked: boolean
|
||||||
|
onChange: ChangeEventHandler<HTMLInputElement> | undefined
|
||||||
|
selectSingle?: boolean
|
||||||
|
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Radio = (props: RadioProps) => {
|
||||||
|
const { id, htmlFor, title, subtitle, checked, onChange, children, selectSingle = false } = props
|
||||||
|
return (
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
{/* radio element */}
|
||||||
|
<input
|
||||||
|
checked={checked}
|
||||||
|
className="mt-1 w-4 h-4 text-plumbus focus:ring-plumbus cursor-pointer form-radio"
|
||||||
|
id={`${htmlFor}-${id}`}
|
||||||
|
name={selectSingle ? id : htmlFor}
|
||||||
|
onChange={onChange}
|
||||||
|
type="radio"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col flex-grow space-y-2">
|
||||||
|
{/* radio description */}
|
||||||
|
<label className="group cursor-pointer" htmlFor={`${htmlFor}-${id}`}>
|
||||||
|
<span className="block font-bold group-hover:underline">{title}</span>
|
||||||
|
<span className="block text-sm whitespace-pre-wrap">{subtitle}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* children if checked */}
|
||||||
|
{checked ? children : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
52
components/SearchInput.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
import { FaSearch } from 'react-icons/fa'
|
||||||
|
|
||||||
|
export interface SearchInputProps extends Omit<ComponentProps<'input'>, 'children'> {
|
||||||
|
_container?: ComponentProps<'div'>
|
||||||
|
value: string
|
||||||
|
onClear: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchInput = (props: SearchInputProps) => {
|
||||||
|
const { _container, value, onClear, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative" {..._container}>
|
||||||
|
{/* search icon as label */}
|
||||||
|
<label
|
||||||
|
aria-label="Search"
|
||||||
|
className="flex absolute inset-y-0 left-4 items-center text-white/50"
|
||||||
|
htmlFor={props.id}
|
||||||
|
>
|
||||||
|
<FaSearch size={16} />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* main search input element */}
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
'py-2 pr-14 pl-10 w-[36ch] form-input placeholder-white/50',
|
||||||
|
'bg-white/10 rounded border-2 border-white/25 focus:ring focus:ring-plumbus',
|
||||||
|
)}
|
||||||
|
placeholder="Search..."
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* clear button, visible when search value exists */}
|
||||||
|
{value.length > 0 && (
|
||||||
|
<div className="flex absolute inset-y-0 right-2 items-center">
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'py-1 px-2 text-xs font-bold text-plumbus',
|
||||||
|
'hover:bg-plumbus/10 rounded border border-plumbus',
|
||||||
|
)}
|
||||||
|
onClick={onClear}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
88
components/Sidebar.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { Anchor } from 'components/Anchor'
|
||||||
|
import { useWallet } from 'contexts/wallet'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import BrandText from 'public/brand/brand-text.svg'
|
||||||
|
import { footerLinks, links, socialsLinks } from 'utils/links'
|
||||||
|
|
||||||
|
import { SidebarLayout } from './SidebarLayout'
|
||||||
|
import { WalletLoader } from './WalletLoader'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ text: 'Create Collection', href: `/collection/create` },
|
||||||
|
{ text: 'CW721 Base', href: `/contracts/cw721/base` },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const Sidebar = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
const wallet = useWallet()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarLayout>
|
||||||
|
{/* Stargaze brand as home button */}
|
||||||
|
<Anchor href="/" onContextMenu={(e) => [e.preventDefault(), router.push('/brand')]}>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-1/2 h-16',
|
||||||
|
'rounded border-2 border-white/20 border-dashed'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Home{/* <BrandText className="text-plumbus hover:text-plumbus-light transition" /> */}
|
||||||
|
</div>
|
||||||
|
</Anchor>
|
||||||
|
|
||||||
|
{/* wallet button */}
|
||||||
|
<WalletLoader />
|
||||||
|
|
||||||
|
{/* main navigation routes */}
|
||||||
|
{routes.map(({ text, href }) => (
|
||||||
|
<Anchor
|
||||||
|
key={href}
|
||||||
|
className={clsx(
|
||||||
|
'py-2 px-4 -mx-4 uppercase', // styling
|
||||||
|
'hover:bg-white/5 transition-colors', // hover styling
|
||||||
|
{ 'font-bold text-plumbus': router.asPath.startsWith(href) }, // active route styling
|
||||||
|
// { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling
|
||||||
|
)}
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Anchor>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="flex-grow" />
|
||||||
|
|
||||||
|
{/* Stargaze network status */}
|
||||||
|
<div className="text-sm">Network: {wallet.network}</div>
|
||||||
|
|
||||||
|
{/* footer reference links */}
|
||||||
|
<ul className="text-sm list-disc list-inside">
|
||||||
|
{footerLinks.map(({ href, text }) => (
|
||||||
|
<li key={href}>
|
||||||
|
<Anchor className="hover:text-plumbus hover:underline" href={href}>
|
||||||
|
{text}
|
||||||
|
</Anchor>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* footer attribution */}
|
||||||
|
<div className="text-xs text-white/50">
|
||||||
|
StargazeTools {process.env.APP_VERSION} <br />
|
||||||
|
Made by{' '}
|
||||||
|
<Anchor className="text-plumbus hover:underline" href={links.deuslabs}>
|
||||||
|
deus labs
|
||||||
|
</Anchor>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* footer social links */}
|
||||||
|
<div className="flex gap-x-6 items-center text-white/75">
|
||||||
|
{socialsLinks.map(({ Icon, href, text }) => (
|
||||||
|
<Anchor key={href} className="hover:text-plumbus" href={href}>
|
||||||
|
<Icon aria-label={text} size={20} />
|
||||||
|
</Anchor>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SidebarLayout>
|
||||||
|
)
|
||||||
|
}
|
47
components/SidebarLayout.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { toggleSidebar, useSidebarStore } from 'contexts/sidebar'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { FaChevronLeft } from 'react-icons/fa'
|
||||||
|
|
||||||
|
export interface SidebarLayoutProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarLayout = ({ children }: SidebarLayoutProps) => {
|
||||||
|
const { isOpen } = useSidebarStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(isOpen ? 'min-w-[250px] max-w-[250px]' : 'min-w-[20px] max-w-[20px]', 'relative z-10')}>
|
||||||
|
{/* fixed component */}
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'overflow-auto fixed top-0 left-0 min-w-[250px] max-w-[250px] no-scrollbar',
|
||||||
|
'bg-black/50 border-r-[1px] border-r-plumbus-light',
|
||||||
|
{ 'translate-x-[-230px]': !isOpen },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* inner component */}
|
||||||
|
<div
|
||||||
|
className={clsx('flex flex-col gap-y-4 p-8 min-h-screen', {
|
||||||
|
invisible: !isOpen,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* sidebar toggle */}
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'absolute top-[32px] right-[-12px] p-1 w-[24px] h-[24px]',
|
||||||
|
'text-black bg-plumbus-light rounded-full',
|
||||||
|
'hover:bg-plumbus',
|
||||||
|
)}
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<FaChevronLeft className={clsx('mx-auto', { 'rotate-180': !isOpen })} size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
32
components/StackedList.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps, ReactNode } from 'react'
|
||||||
|
|
||||||
|
export interface StackedListProps extends ComponentProps<'dl'> {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StackedList = (props: StackedListProps) => {
|
||||||
|
const { className, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl
|
||||||
|
className={clsx('bg-white/5 rounded border-2 border-white/25', 'divide-y-2 divide-white/25', className)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StackedListItemProps extends ComponentProps<'dt'> {
|
||||||
|
name: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
StackedList.Item = function StackedListItem(props: StackedListItemProps) {
|
||||||
|
const { name, className, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-3 py-3 px-4 hover:bg-white/5">
|
||||||
|
<dd className="font-medium text-white/50">{name}</dd>
|
||||||
|
<dt className={clsx('col-span-2', className)} {...rest} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
25
components/Stats.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
export interface StatsProps {
|
||||||
|
title: string
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Stats = ({ title, children }: StatsProps) => {
|
||||||
|
return (
|
||||||
|
<div className={clsx('flex flex-col p-6 space-y-1', 'bg-white/10 rounded border-2 border-white/20 backdrop-blur')}>
|
||||||
|
<div className="font-bold text-white/50">{title}</div>
|
||||||
|
<div className="text-4xl font-bold">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatsDenomProps {
|
||||||
|
text: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
Stats.Denom = function StatsDenom({ text }: StatsDenomProps) {
|
||||||
|
/* Slice the first character because we get "u" for the denom */
|
||||||
|
return <span className="font-mono text-xl">{text?.slice(1)}</span>
|
||||||
|
}
|
17
components/TextArea.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
|
||||||
|
export const TextArea = (props: ComponentProps<'textarea'>) => {
|
||||||
|
const { className, ...rest } = props
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={clsx(
|
||||||
|
'bg-white/10 rounded border-2 border-white/20 form-input',
|
||||||
|
'placeholder:text-white/50',
|
||||||
|
'focus:ring focus:ring-plumbus-20',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
44
components/Tooltip.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps, ReactElement, ReactNode } from 'react'
|
||||||
|
import { cloneElement, useState } from 'react'
|
||||||
|
import { usePopper } from 'react-popper'
|
||||||
|
|
||||||
|
export interface TooltipProps extends ComponentProps<'div'> {
|
||||||
|
label: ReactNode
|
||||||
|
children: ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
|
||||||
|
const [referenceElement, setReferenceElement] = useState(null)
|
||||||
|
const [popperElement, setPopperElement] = useState<any>(null)
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
|
||||||
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
|
placement: 'top',
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* children with attached ref and mouse events */}
|
||||||
|
{cloneElement(children, {
|
||||||
|
...children.props,
|
||||||
|
ref: setReferenceElement,
|
||||||
|
onMouseOver: () => setShow(true),
|
||||||
|
onMouseOut: () => setShow(false),
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* popper element */}
|
||||||
|
{show && (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
{...attributes.popper}
|
||||||
|
className={clsx('py-1 px-2 m-1 text-sm bg-black/80 rounded shadow-md', props.className)}
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={{ ...styles.popper, ...props.style }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
18
components/TooltipIcon.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Tooltip } from 'components/Tooltip'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { FaRegQuestionCircle } from 'react-icons/fa'
|
||||||
|
import type { IconBaseProps } from 'react-icons/lib'
|
||||||
|
|
||||||
|
interface TooltipIconProps extends IconBaseProps {
|
||||||
|
label: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TooltipIcon = ({ label, ...rest }: TooltipIconProps) => {
|
||||||
|
return (
|
||||||
|
<Tooltip label={label}>
|
||||||
|
<span>
|
||||||
|
<FaRegQuestionCircle className="cursor-help" {...rest} />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
44
components/TransactionHash.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { NETWORK } from 'utils/constants'
|
||||||
|
import { links } from 'utils/links'
|
||||||
|
|
||||||
|
import { AnchorButton } from './AnchorButton'
|
||||||
|
import { StyledInput } from './forms/StyledInput'
|
||||||
|
|
||||||
|
interface TransactionHashProps {
|
||||||
|
hash: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransactionHash = ({ hash, className }: TransactionHashProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'bg-white/10 rounded border-2 border-white/20 form-input',
|
||||||
|
'focus:ring focus:ring-plumbus-20',
|
||||||
|
hash !== '' ? 'text-white/100' : 'text-white/50',
|
||||||
|
'flex justify-end items-center',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<StyledInput
|
||||||
|
className={clsx(
|
||||||
|
'flex-auto w-fit',
|
||||||
|
'bg-white/5 rounded border-0 border-white/20 focus:ring-0 form-input',
|
||||||
|
hash !== '' ? 'text-white/100' : 'text-white/50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
value={hash || 'Waiting for execution...'}
|
||||||
|
/>
|
||||||
|
<AnchorButton
|
||||||
|
className={clsx('ml-2 text-white', hash === '' ? 'text-white/30 bg-opacity-20 hover:bg-opacity-10' : '')}
|
||||||
|
href={`${links.Explorer}/tx${NETWORK === 'mainnet' ? 's' : ''}/${hash}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (hash === '') e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Go to Explorer
|
||||||
|
</AnchorButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
32
components/WalletButton.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
import { BiWallet } from 'react-icons/bi'
|
||||||
|
import { FaSpinner } from 'react-icons/fa'
|
||||||
|
|
||||||
|
export interface WalletButtonProps extends ComponentProps<'button'> {
|
||||||
|
isLoading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WalletButton = forwardRef<HTMLButtonElement, WalletButtonProps>(function WalletButton(
|
||||||
|
{ className, children, isLoading, ...props },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'flex gap-x-2 items-center text-sm font-bold uppercase truncate', // content styling
|
||||||
|
'py-2 px-4 rounded border border-plumbus', // button styling
|
||||||
|
'hover:bg-white/10 transition-colors', // hover styling
|
||||||
|
{ 'cursor-wait': isLoading }, // loading styling
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
type="button"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{isLoading ? <FaSpinner className="animate-spin" size={16} /> : <BiWallet size={16} />}
|
||||||
|
<span>{isLoading ? 'Loading...' : children}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})
|
78
components/WalletLoader.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Popover, Transition } from '@headlessui/react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { useWallet, useWalletStore } from 'contexts/wallet'
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import { FaCopy, FaPowerOff, FaRedo } from 'react-icons/fa'
|
||||||
|
import { copy } from 'utils/clipboard'
|
||||||
|
import { convertDenomToReadable } from 'utils/convertDenomToReadable'
|
||||||
|
import { getShortAddress } from 'utils/getShortAddress'
|
||||||
|
|
||||||
|
import { WalletButton } from './WalletButton'
|
||||||
|
import { WalletPanelButton } from './WalletPanelButton'
|
||||||
|
|
||||||
|
export const WalletLoader = () => {
|
||||||
|
const { address, balance, connect, disconnect, initializing: isLoading, initialized: isReady } = useWallet()
|
||||||
|
|
||||||
|
const displayName = useWalletStore((store) => store.name || getShortAddress(store.address))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover className="my-8">
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<div className="grid -mx-4">
|
||||||
|
{!isReady && (
|
||||||
|
<WalletButton className="w-full" isLoading={isLoading} onClick={() => void connect()}>
|
||||||
|
Connect Wallet
|
||||||
|
</WalletButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isReady && (
|
||||||
|
<Popover.Button as={WalletButton} className="w-full" isLoading={isLoading}>
|
||||||
|
{displayName}
|
||||||
|
</Popover.Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 -translate-y-1"
|
||||||
|
enterTo="opacity-100 translate-y-0"
|
||||||
|
leave="transition ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
|
leaveTo="opacity-0 -translate-y-1"
|
||||||
|
>
|
||||||
|
<Popover.Panel
|
||||||
|
className={clsx(
|
||||||
|
'absolute inset-x-4 mt-2',
|
||||||
|
'bg-stone-800/80 rounded shadow-lg shadow-black/90 backdrop-blur-sm',
|
||||||
|
'flex flex-col items-stretch text-sm divide-y divide-white/10',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center py-2 px-4 space-y-1 text-center">
|
||||||
|
<span className="py-px px-2 mb-2 font-mono text-xs text-white/50 rounded-full border border-white/25">
|
||||||
|
{getShortAddress(address)}
|
||||||
|
</span>
|
||||||
|
<div className="font-bold">Your Balances</div>
|
||||||
|
{balance.map((val) => (
|
||||||
|
<span key={`balance-${val.denom}`}>
|
||||||
|
{convertDenomToReadable(val.amount)} {val.denom.slice(1, val.denom.length)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<WalletPanelButton Icon={FaCopy} onClick={() => void copy(address)}>
|
||||||
|
Copy wallet address
|
||||||
|
</WalletPanelButton>
|
||||||
|
<WalletPanelButton Icon={FaRedo} onClick={() => void connect()}>
|
||||||
|
Reconnect
|
||||||
|
</WalletPanelButton>
|
||||||
|
<WalletPanelButton Icon={FaPowerOff} onClick={() => [disconnect(), close()]}>
|
||||||
|
Disconnect
|
||||||
|
</WalletPanelButton>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
25
components/WalletPanelButton.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
import type { IconType } from 'react-icons/lib'
|
||||||
|
|
||||||
|
export interface WalletPanelButtonProps extends ComponentProps<'button'> {
|
||||||
|
Icon: IconType
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WalletPanelButton = forwardRef<HTMLButtonElement, WalletPanelButtonProps>(function WalletPanelButton(
|
||||||
|
{ className, children, Icon, ...rest },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx('flex items-center py-2 px-4 space-x-4 hover:bg-white/5', className)}
|
||||||
|
ref={ref}
|
||||||
|
type="button"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
<span className="text-left">{children}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})
|
7
components/contracts/cw721/base/ExecuteCombobox.hooks.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import type { ExecuteListItem } from 'utils/contracts/cw721/base/execute'
|
||||||
|
|
||||||
|
export const useExecuteComboboxState = () => {
|
||||||
|
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
||||||
|
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
||||||
|
}
|
92
components/contracts/cw721/base/ExecuteCombobox.tsx
Normal file
@ -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 { ExecuteListItem } from 'utils/contracts/cw721/base/execute'
|
||||||
|
import { EXECUTE_LIST } from 'utils/contracts/cw721/base/execute'
|
||||||
|
|
||||||
|
export interface ExecuteComboboxProps {
|
||||||
|
value: ExecuteListItem | null
|
||||||
|
onChange: (item: ExecuteListItem) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
|
const filtered =
|
||||||
|
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox
|
||||||
|
as={FormControl}
|
||||||
|
htmlId="message-type"
|
||||||
|
labelAs={Combobox.Label}
|
||||||
|
onChange={onChange}
|
||||||
|
subtitle="Contract execute message type"
|
||||||
|
title="Message Type"
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<Combobox.Input
|
||||||
|
className={clsx(
|
||||||
|
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
||||||
|
'placeholder:text-white/50',
|
||||||
|
'focus:ring focus:ring-plumbus-20',
|
||||||
|
)}
|
||||||
|
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
||||||
|
id="message-type"
|
||||||
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
|
placeholder="Select message type"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Combobox.Button
|
||||||
|
className={clsx(
|
||||||
|
'flex absolute inset-y-0 right-0 items-center p-4',
|
||||||
|
'opacity-50 hover:opacity-100 active:opacity-100',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
||||||
|
</Combobox.Button>
|
||||||
|
|
||||||
|
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
||||||
|
<Combobox.Options
|
||||||
|
className={clsx(
|
||||||
|
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
||||||
|
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
||||||
|
'divide-y divide-stone-500/50',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{filtered.length < 1 && (
|
||||||
|
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
||||||
|
Message type not found.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filtered.map((entry) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={entry.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active })
|
||||||
|
}
|
||||||
|
value={entry}
|
||||||
|
>
|
||||||
|
<span className="font-bold">{entry.name}</span>
|
||||||
|
<span className="max-w-md text-sm">{entry.description}</span>
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{value && (
|
||||||
|
<div className="flex space-x-2 text-white/50">
|
||||||
|
<div className="mt-1">
|
||||||
|
<FaInfoCircle className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">{value.description}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
)
|
||||||
|
}
|
31
components/forms/AddressBalances.hooks.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { uid } from 'utils/random'
|
||||||
|
|
||||||
|
import type { Balance } from './AddressBalances'
|
||||||
|
|
||||||
|
export function useAddressBalancesState() {
|
||||||
|
const [record, setRecord] = useState<Record<string, Balance>>(() => ({
|
||||||
|
[uid()]: { address: '', amount: '0' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
const entries = useMemo(() => Object.entries(record), [record])
|
||||||
|
const values = useMemo(() => Object.values(record), [record])
|
||||||
|
|
||||||
|
function add(balance: Balance = { address: '', amount: '0' }) {
|
||||||
|
setRecord((prev) => ({ ...prev, [uid()]: balance }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(key: string, balance = record[key]) {
|
||||||
|
setRecord((prev) => ({ ...prev, [key]: balance }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(key: string) {
|
||||||
|
return setRecord((prev) => {
|
||||||
|
const latest = { ...prev }
|
||||||
|
delete latest[key]
|
||||||
|
return latest
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entries, values, add, update, remove }
|
||||||
|
}
|
89
components/forms/AddressBalances.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||||
|
import { useEffect, useId, useMemo } from 'react'
|
||||||
|
import { FaMinus, FaPlus } from 'react-icons/fa'
|
||||||
|
|
||||||
|
import { useInputState } from './FormInput.hooks'
|
||||||
|
|
||||||
|
export interface Balance {
|
||||||
|
address: string
|
||||||
|
amount: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressBalancesProps {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
entries: [string, Balance][]
|
||||||
|
onAdd: () => void
|
||||||
|
onChange: (key: string, balance: Balance) => void
|
||||||
|
onRemove: (key: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddressBalances(props: AddressBalancesProps) {
|
||||||
|
const { title, subtitle, isRequired, entries, onAdd, onChange, onRemove } = props
|
||||||
|
return (
|
||||||
|
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
||||||
|
{entries.map(([id], i) => (
|
||||||
|
<AddressBalance
|
||||||
|
key={`ib-${id}`}
|
||||||
|
id={id}
|
||||||
|
isLast={i === entries.length - 1}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onChange={onChange}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressBalanceProps {
|
||||||
|
id: string
|
||||||
|
isLast: boolean
|
||||||
|
onAdd: AddressBalancesProps['onAdd']
|
||||||
|
onChange: AddressBalancesProps['onChange']
|
||||||
|
onRemove: AddressBalancesProps['onRemove']
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddressBalance({ id, isLast, onAdd, onChange, onRemove }: AddressBalanceProps) {
|
||||||
|
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
||||||
|
|
||||||
|
const htmlId = useId()
|
||||||
|
|
||||||
|
const addressState = useInputState({
|
||||||
|
id: `ib-address-${htmlId}`,
|
||||||
|
name: `ib-address-${htmlId}`,
|
||||||
|
title: `Wallet Address`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const amountState = useInputState({
|
||||||
|
id: `ib-balance-${htmlId}`,
|
||||||
|
name: `ib-balance-${htmlId}`,
|
||||||
|
title: `Balance`,
|
||||||
|
placeholder: '0',
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange(id, {
|
||||||
|
address: addressState.value,
|
||||||
|
amount: amountState.value,
|
||||||
|
})
|
||||||
|
}, [addressState.value, amountState.value, id])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid relative grid-cols-[1fr_1fr_auto] space-x-2">
|
||||||
|
<AddressInput {...addressState} />
|
||||||
|
<NumberInput {...amountState} />
|
||||||
|
<div className="flex justify-end items-end pb-2 w-8">
|
||||||
|
<button
|
||||||
|
className="flex justify-center items-center p-2 bg-plumbus-80 hover:bg-plumbus-60 rounded-full"
|
||||||
|
onClick={() => (isLast ? onAdd() : onRemove(id))}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
31
components/forms/AddressList.hooks.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { uid } from 'utils/random'
|
||||||
|
|
||||||
|
import type { Address } from './AddressList'
|
||||||
|
|
||||||
|
export function useAddressListState() {
|
||||||
|
const [record, setRecord] = useState<Record<string, Address>>(() => ({
|
||||||
|
[uid()]: { address: '' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
const entries = useMemo(() => Object.entries(record), [record])
|
||||||
|
const values = useMemo(() => Object.values(record), [record])
|
||||||
|
|
||||||
|
function add(balance: Address = { address: '' }) {
|
||||||
|
setRecord((prev) => ({ ...prev, [uid()]: balance }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(key: string, balance = record[key]) {
|
||||||
|
setRecord((prev) => ({ ...prev, [key]: balance }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(key: string) {
|
||||||
|
return setRecord((prev) => {
|
||||||
|
const latest = { ...prev }
|
||||||
|
delete latest[key]
|
||||||
|
return latest
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entries, values, add, update, remove }
|
||||||
|
}
|
79
components/forms/AddressList.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import { AddressInput } from 'components/forms/FormInput'
|
||||||
|
import { useEffect, useId, useMemo } from 'react'
|
||||||
|
import { FaMinus, FaPlus } from 'react-icons/fa'
|
||||||
|
|
||||||
|
import { useInputState } from './FormInput.hooks'
|
||||||
|
|
||||||
|
export interface Address {
|
||||||
|
address: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressListProps {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
entries: [string, Address][]
|
||||||
|
onAdd: () => void
|
||||||
|
onChange: (key: string, address: Address) => void
|
||||||
|
onRemove: (key: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddressList(props: AddressListProps) {
|
||||||
|
const { title, subtitle, isRequired, entries, onAdd, onChange, onRemove } = props
|
||||||
|
return (
|
||||||
|
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
||||||
|
{entries.map(([id], i) => (
|
||||||
|
<Address
|
||||||
|
key={`ib-${id}`}
|
||||||
|
id={id}
|
||||||
|
isLast={i === entries.length - 1}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onChange={onChange}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressProps {
|
||||||
|
id: string
|
||||||
|
isLast: boolean
|
||||||
|
onAdd: AddressListProps['onAdd']
|
||||||
|
onChange: AddressListProps['onChange']
|
||||||
|
onRemove: AddressListProps['onRemove']
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps) {
|
||||||
|
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
||||||
|
|
||||||
|
const htmlId = useId()
|
||||||
|
|
||||||
|
const addressState = useInputState({
|
||||||
|
id: `ib-address-${htmlId}`,
|
||||||
|
name: `ib-address-${htmlId}`,
|
||||||
|
title: ``,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange(id, {
|
||||||
|
address: addressState.value,
|
||||||
|
})
|
||||||
|
}, [addressState.value, id])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid relative grid-cols-[1fr_auto] space-x-2">
|
||||||
|
<AddressInput {...addressState} />
|
||||||
|
<div className="flex justify-end items-end pb-2 w-8">
|
||||||
|
<button
|
||||||
|
className="flex justify-center items-center p-2 bg-plumbus-80 hover:bg-plumbus-60 rounded-full"
|
||||||
|
onClick={() => (isLast ? onAdd() : onRemove(id))}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
48
components/forms/FormInput.hooks.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { ChangeEvent } from 'react'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export interface UseInputStateProps {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
defaultValue?: string
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInputState = ({ defaultValue, ...args }: UseInputStateProps) => {
|
||||||
|
const [value, setValue] = useState<string>(() => defaultValue ?? '')
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultValue) setValue(defaultValue)
|
||||||
|
}, [defaultValue])
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
onChange: (obj: string | ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
setValue(typeof obj === 'string' ? obj : obj.target.value)
|
||||||
|
},
|
||||||
|
...args,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseNumberInputStateProps {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
defaultValue?: number
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNumberInputState = (args: UseNumberInputStateProps) => {
|
||||||
|
const [value, setValue] = useState<number>(() => args.defaultValue ?? 0)
|
||||||
|
useEffect(() => {
|
||||||
|
if (args.defaultValue) setValue(args.defaultValue)
|
||||||
|
}, [args.defaultValue])
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
onChange: (obj: number | ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setValue(typeof obj === 'number' ? obj : obj.target.valueAsNumber)
|
||||||
|
},
|
||||||
|
...args,
|
||||||
|
}
|
||||||
|
}
|
77
components/forms/FormInput.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import { StyledInput } from 'components/forms/StyledInput'
|
||||||
|
import type { ComponentPropsWithRef } from 'react'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
|
interface BaseProps {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type SlicedInputProps = Omit<ComponentPropsWithRef<'input'>, keyof BaseProps>
|
||||||
|
|
||||||
|
export type FormInputProps = BaseProps & SlicedInputProps
|
||||||
|
|
||||||
|
export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||||
|
function FormInput(props, ref) {
|
||||||
|
const { id, name, title, subtitle, isRequired, ...rest } = props
|
||||||
|
return (
|
||||||
|
<FormControl htmlId={id} isRequired={isRequired} subtitle={subtitle} title={title}>
|
||||||
|
<StyledInput id={id} name={name} ref={ref} {...rest} />
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
//
|
||||||
|
)
|
||||||
|
|
||||||
|
export const AddressInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||||
|
function AddressInput(props, ref) {
|
||||||
|
return (
|
||||||
|
<FormInput
|
||||||
|
{...props}
|
||||||
|
placeholder={props.placeholder || 'stars1234567890abcdefghijklmnopqrstuvwxyz...'}
|
||||||
|
ref={ref}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
//
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ValidatorAddressInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||||
|
function ValidatorAddressInput(props, ref) {
|
||||||
|
return (
|
||||||
|
<FormInput
|
||||||
|
{...props}
|
||||||
|
placeholder={props.placeholder || 'starsvaloper1234567890abcdefghijklmnopqrstuvwxyz...'}
|
||||||
|
ref={ref}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
//
|
||||||
|
)
|
||||||
|
|
||||||
|
export const NumberInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||||
|
function NumberInput(props, ref) {
|
||||||
|
return <FormInput {...props} ref={ref} type="number" />
|
||||||
|
},
|
||||||
|
//
|
||||||
|
)
|
||||||
|
|
||||||
|
export const TextInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||||
|
function TextInput(props, ref) {
|
||||||
|
return <FormInput {...props} ref={ref} type="text" />
|
||||||
|
},
|
||||||
|
//
|
||||||
|
)
|
||||||
|
|
||||||
|
export const UrlInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||||
|
function UrlInput(props, ref) {
|
||||||
|
return <FormInput {...props} ref={ref} type="url" />
|
||||||
|
},
|
||||||
|
//
|
||||||
|
)
|
54
components/forms/FormTextArea.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import type { ComponentPropsWithRef } from 'react'
|
||||||
|
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
||||||
|
|
||||||
|
import { JsonValidStatus } from './JsonValidStatus'
|
||||||
|
import { StyledTextArea } from './StyledTextArea'
|
||||||
|
|
||||||
|
interface BaseProps {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SlicedInputProps = Omit<ComponentPropsWithRef<'textarea'>, keyof BaseProps>
|
||||||
|
|
||||||
|
export type FormTextAreaProps = BaseProps & SlicedInputProps
|
||||||
|
|
||||||
|
export const FormTextArea = forwardRef<HTMLTextAreaElement, FormTextAreaProps>(
|
||||||
|
function FormTextArea(props, ref) {
|
||||||
|
const { id, name, title, subtitle, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl htmlId={id} subtitle={subtitle} title={title}>
|
||||||
|
<StyledTextArea id={id} name={name} ref={ref} {...rest} />
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
//
|
||||||
|
)
|
||||||
|
|
||||||
|
export const JsonTextArea = forwardRef<HTMLTextAreaElement, FormTextAreaProps>(
|
||||||
|
function JsonTextArea(props, ref) {
|
||||||
|
const { id, name, title, subtitle, className, ...rest } = props
|
||||||
|
|
||||||
|
const innerRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
useImperativeHandle(ref, () => innerRef.current!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl htmlId={id} subtitle={subtitle} title={title}>
|
||||||
|
<StyledTextArea
|
||||||
|
className={clsx('min-h-[8rem] font-mono text-sm', className)}
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
ref={innerRef}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
<JsonValidStatus textAreaRef={innerRef} />
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
//
|
||||||
|
)
|
47
components/forms/JsonValidStatus.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { RefObject } from 'react'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { FaCheckCircle, FaTimesCircle } from 'react-icons/fa'
|
||||||
|
import { parseJson } from 'utils/json'
|
||||||
|
|
||||||
|
export interface JsonValidStatusProps {
|
||||||
|
textAreaRef: RefObject<HTMLTextAreaElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JsonValidStatus({ textAreaRef }: JsonValidStatusProps) {
|
||||||
|
const [valid, setValid] = useState<boolean | null>(null)
|
||||||
|
const ValidIcon = valid ? FaCheckCircle : FaTimesCircle
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listen = () => {
|
||||||
|
if (!textAreaRef.current) return
|
||||||
|
try {
|
||||||
|
if (textAreaRef.current.value.length > 0) {
|
||||||
|
parseJson(textAreaRef.current.value)
|
||||||
|
setValid(true)
|
||||||
|
} else {
|
||||||
|
setValid(null)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setValid(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listen()
|
||||||
|
textAreaRef.current?.addEventListener('input', listen)
|
||||||
|
return () => textAreaRef.current?.removeEventListener('input', listen)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (valid === null) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx('flex items-center space-x-1 text-sm', {
|
||||||
|
'text-green-500': valid,
|
||||||
|
'text-red-500': !valid,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ValidIcon />
|
||||||
|
<span>{valid ? 'valid json content' : 'invalid json content'}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
21
components/forms/StyledInput.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
|
export const StyledInput = forwardRef<HTMLInputElement, ComponentProps<'input'>>(
|
||||||
|
function Input({ className, ...rest }, ref) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
'bg-white/10 rounded border-2 border-white/20 form-input',
|
||||||
|
'placeholder:text-white/50',
|
||||||
|
'focus:ring focus:ring-plumbus-20',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
//
|
||||||
|
)
|
21
components/forms/StyledTextArea.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
|
export const StyledTextArea = forwardRef<HTMLTextAreaElement, ComponentProps<'textarea'>>(
|
||||||
|
function Input({ className, ...rest }, ref) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={clsx(
|
||||||
|
'bg-white/10 rounded border-2 border-white/20 form-input',
|
||||||
|
'placeholder:text-white/50',
|
||||||
|
'focus:ring focus:ring-plumbus-20',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
//
|
||||||
|
)
|
27
config/app.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export interface MappedCoin {
|
||||||
|
readonly denom: string
|
||||||
|
readonly fractionalDigits: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CoinMap = Readonly<Record<string, MappedCoin>>
|
||||||
|
|
||||||
|
export interface FeeOptions {
|
||||||
|
upload: number
|
||||||
|
exec: number
|
||||||
|
init: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppConfig {
|
||||||
|
readonly chainId: string
|
||||||
|
readonly chainName: string
|
||||||
|
readonly addressPrefix: string
|
||||||
|
readonly rpcUrl: string
|
||||||
|
readonly httpUrl?: string
|
||||||
|
readonly faucetUrl?: string
|
||||||
|
readonly feeToken: string
|
||||||
|
readonly stakingToken: string
|
||||||
|
readonly coinMap: CoinMap
|
||||||
|
readonly gasPrice: number
|
||||||
|
readonly fees: FeeOptions
|
||||||
|
readonly codeId?: number
|
||||||
|
}
|
17
config/favicons.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"path": "/assets/",
|
||||||
|
"appName": "StargazeTools",
|
||||||
|
"appShortName": "StargazeTools",
|
||||||
|
"appDescription": "Stargaze Tools is built to provide useful smart contract interfaces that helps you build and deploy your own NFT collection in no time.",
|
||||||
|
"developerName": "StargazeTools",
|
||||||
|
"developerURL": "https://",
|
||||||
|
"background": "#FFC27D",
|
||||||
|
"theme_color": "#FFC27D",
|
||||||
|
"icons": {
|
||||||
|
"android": true,
|
||||||
|
"appleIcon": true,
|
||||||
|
"appleStartup": true,
|
||||||
|
"favicons": true,
|
||||||
|
"windows": true
|
||||||
|
}
|
||||||
|
}
|
3
config/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './app'
|
||||||
|
export * from './keplr'
|
||||||
|
export * from './network'
|
81
config/keplr.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { ChainInfo } from '@keplr-wallet/types'
|
||||||
|
|
||||||
|
import type { AppConfig } from './app'
|
||||||
|
|
||||||
|
export interface KeplrCoin {
|
||||||
|
readonly coinDenom: string
|
||||||
|
readonly coinMinimalDenom: string
|
||||||
|
readonly coinDecimals: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeplrConfig {
|
||||||
|
readonly chainId: string
|
||||||
|
readonly chainName: string
|
||||||
|
readonly rpc: string
|
||||||
|
readonly rest?: string
|
||||||
|
readonly bech32Config: {
|
||||||
|
readonly bech32PrefixAccAddr: string
|
||||||
|
readonly bech32PrefixAccPub: string
|
||||||
|
readonly bech32PrefixValAddr: string
|
||||||
|
readonly bech32PrefixValPub: string
|
||||||
|
readonly bech32PrefixConsAddr: string
|
||||||
|
readonly bech32PrefixConsPub: string
|
||||||
|
}
|
||||||
|
readonly currencies: readonly KeplrCoin[]
|
||||||
|
readonly feeCurrencies: readonly KeplrCoin[]
|
||||||
|
readonly stakeCurrency: KeplrCoin
|
||||||
|
readonly gasPriceStep: {
|
||||||
|
readonly low: number
|
||||||
|
readonly average: number
|
||||||
|
readonly high: number
|
||||||
|
}
|
||||||
|
readonly bip44: { readonly coinType: number }
|
||||||
|
readonly coinType: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const keplrConfig = (config: AppConfig): ChainInfo => ({
|
||||||
|
chainId: config.chainId,
|
||||||
|
chainName: config.chainName,
|
||||||
|
rpc: config.rpcUrl,
|
||||||
|
rest: config.httpUrl!,
|
||||||
|
bech32Config: {
|
||||||
|
bech32PrefixAccAddr: `${config.addressPrefix}`,
|
||||||
|
bech32PrefixAccPub: `${config.addressPrefix}pub`,
|
||||||
|
bech32PrefixValAddr: `${config.addressPrefix}valoper`,
|
||||||
|
bech32PrefixValPub: `${config.addressPrefix}valoperpub`,
|
||||||
|
bech32PrefixConsAddr: `${config.addressPrefix}valcons`,
|
||||||
|
bech32PrefixConsPub: `${config.addressPrefix}valconspub`,
|
||||||
|
},
|
||||||
|
currencies: [
|
||||||
|
{
|
||||||
|
coinDenom: config.coinMap[config.feeToken].denom,
|
||||||
|
coinMinimalDenom: config.feeToken,
|
||||||
|
coinDecimals: config.coinMap[config.feeToken].fractionalDigits,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
coinDenom: config.coinMap[config.stakingToken].denom,
|
||||||
|
coinMinimalDenom: config.stakingToken,
|
||||||
|
coinDecimals: config.coinMap[config.stakingToken].fractionalDigits,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
feeCurrencies: [
|
||||||
|
{
|
||||||
|
coinDenom: config.coinMap[config.feeToken].denom,
|
||||||
|
coinMinimalDenom: config.feeToken,
|
||||||
|
coinDecimals: config.coinMap[config.feeToken].fractionalDigits,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stakeCurrency: {
|
||||||
|
coinDenom: config.coinMap[config.stakingToken].denom,
|
||||||
|
coinMinimalDenom: config.stakingToken,
|
||||||
|
coinDecimals: config.coinMap[config.stakingToken].fractionalDigits,
|
||||||
|
},
|
||||||
|
gasPriceStep: {
|
||||||
|
low: config.gasPrice / 2,
|
||||||
|
average: config.gasPrice,
|
||||||
|
high: config.gasPrice * 2,
|
||||||
|
},
|
||||||
|
bip44: { coinType: 118 },
|
||||||
|
coinType: 118,
|
||||||
|
features: ['ibc-transfer', 'cosmwasm', 'ibc-go'],
|
||||||
|
})
|
11
config/meta.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import faviconsJson from './favicons.json'
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
name: faviconsJson.appName,
|
||||||
|
description: faviconsJson.appDescription,
|
||||||
|
domain: 'stargaze.tools',
|
||||||
|
url: faviconsJson.developerURL,
|
||||||
|
twitter: {
|
||||||
|
username: '@stargazetools',
|
||||||
|
},
|
||||||
|
}
|
42
config/network.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type { AppConfig } from './app'
|
||||||
|
|
||||||
|
export const mainnetConfig: AppConfig = {
|
||||||
|
chainId: 'stargaze-1',
|
||||||
|
chainName: 'Stargaze',
|
||||||
|
addressPrefix: 'stars',
|
||||||
|
rpcUrl: 'https://rpc.stargaze-apis.com/',
|
||||||
|
feeToken: 'ustars',
|
||||||
|
stakingToken: 'ustars',
|
||||||
|
coinMap: {
|
||||||
|
ustars: { denom: 'STARS', fractionalDigits: 6 },
|
||||||
|
},
|
||||||
|
gasPrice: 0.025,
|
||||||
|
fees: {
|
||||||
|
upload: 1500000,
|
||||||
|
init: 500000,
|
||||||
|
exec: 200000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testnetConfig: AppConfig = {
|
||||||
|
chainId: 'elgafar-1',
|
||||||
|
chainName: 'elgafar-1',
|
||||||
|
addressPrefix: 'stars',
|
||||||
|
rpcUrl: 'https://rpc.elgafar-1.stargaze-apis.com/',
|
||||||
|
feeToken: 'ustars',
|
||||||
|
stakingToken: 'ustars',
|
||||||
|
coinMap: {
|
||||||
|
ustars: { denom: 'STARS', fractionalDigits: 6 },
|
||||||
|
},
|
||||||
|
gasPrice: 0.025,
|
||||||
|
fees: {
|
||||||
|
upload: 1500000,
|
||||||
|
init: 500000,
|
||||||
|
exec: 200000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getConfig = (network: string): AppConfig => {
|
||||||
|
if (network === 'mainnet') return mainnetConfig
|
||||||
|
return testnetConfig
|
||||||
|
}
|
9
config/react-query.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { QueryClient } from 'react-query'
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
notifyOnChangeProps: 'tracked',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
60
contexts/contracts.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { UseCW721BaseContractProps } from 'contracts/cw721/base'
|
||||||
|
import { useCW721BaseContract } from 'contracts/cw721/base'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import type { State } from 'zustand'
|
||||||
|
import create from 'zustand'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contracts store type definitions
|
||||||
|
*/
|
||||||
|
export interface ContractsStore extends State {
|
||||||
|
cw721Base: UseCW721BaseContractProps | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contracts store default values as a separate variable for reusability
|
||||||
|
*/
|
||||||
|
export const defaultValues: ContractsStore = {
|
||||||
|
cw721Base: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entrypoint for contracts store using {@link defaultValues}
|
||||||
|
*/
|
||||||
|
export const useContracts = create<ContractsStore>(() => ({
|
||||||
|
...defaultValues,
|
||||||
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contracts store provider to easily mount {@link ContractsSubscription}
|
||||||
|
* to listen/subscribe to contract changes
|
||||||
|
*/
|
||||||
|
export const ContractsProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<ContractsSubscription />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contracts store subscriptions (side effects)
|
||||||
|
*
|
||||||
|
* TODO: refactor all contract logics to zustand store
|
||||||
|
*/
|
||||||
|
const ContractsSubscription = () => {
|
||||||
|
const cw721Base = useCW721BaseContract()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
useContracts.setState({
|
||||||
|
cw721Base,
|
||||||
|
})
|
||||||
|
}, [
|
||||||
|
cw721Base,
|
||||||
|
//
|
||||||
|
])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
13
contexts/sidebar.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import create from 'zustand'
|
||||||
|
|
||||||
|
export const useSidebarStore = create(() => ({ isOpen: true }))
|
||||||
|
|
||||||
|
export const openSidebar = () => {
|
||||||
|
useSidebarStore.setState({ isOpen: true })
|
||||||
|
}
|
||||||
|
export const closeSidebar = () => {
|
||||||
|
useSidebarStore.setState({ isOpen: false })
|
||||||
|
}
|
||||||
|
export const toggleSidebar = () => {
|
||||||
|
useSidebarStore.setState((prev) => ({ isOpen: !prev.isOpen }))
|
||||||
|
}
|
289
contexts/wallet.tsx
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||||
|
import { Decimal } from '@cosmjs/math'
|
||||||
|
import type { OfflineSigner } from '@cosmjs/proto-signing'
|
||||||
|
import type { Coin } from '@cosmjs/stargate'
|
||||||
|
import type { AppConfig } from 'config'
|
||||||
|
import { getConfig, keplrConfig } from 'config'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { createTrackedSelector } from 'react-tracked'
|
||||||
|
import { NETWORK } from 'utils/constants'
|
||||||
|
import type { State } from 'zustand'
|
||||||
|
import create from 'zustand'
|
||||||
|
import { subscribeWithSelector } from 'zustand/middleware'
|
||||||
|
|
||||||
|
export interface KeplrWalletStore extends State {
|
||||||
|
accountNumber: number
|
||||||
|
address: string
|
||||||
|
balance: Coin[]
|
||||||
|
client: SigningCosmWasmClient | undefined
|
||||||
|
config: AppConfig
|
||||||
|
initialized: boolean
|
||||||
|
initializing: boolean
|
||||||
|
name: string
|
||||||
|
network: string
|
||||||
|
signer: OfflineSigner | undefined
|
||||||
|
|
||||||
|
readonly clear: () => void
|
||||||
|
|
||||||
|
readonly connect: (walletChange?: boolean | 'focus') => Promise<void>
|
||||||
|
|
||||||
|
readonly disconnect: () => void | Promise<void>
|
||||||
|
|
||||||
|
readonly getClient: () => SigningCosmWasmClient
|
||||||
|
readonly getSigner: () => OfflineSigner
|
||||||
|
|
||||||
|
readonly init: (signer?: OfflineSigner) => void
|
||||||
|
|
||||||
|
readonly refreshBalance: (address?: string, balance?: Coin[]) => Promise<void>
|
||||||
|
|
||||||
|
readonly setNetwork: (network: string) => void
|
||||||
|
|
||||||
|
readonly updateSigner: (singer: OfflineSigner) => void
|
||||||
|
|
||||||
|
readonly setQueryClient: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibility export for references still using `WalletContextType`
|
||||||
|
*
|
||||||
|
* @deprecated replace with {@link KeplrWalletStore}
|
||||||
|
*/
|
||||||
|
export type WalletContextType = KeplrWalletStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keplr wallet store default values as a separate variable for reusability
|
||||||
|
*/
|
||||||
|
const defaultStates = {
|
||||||
|
accountNumber: 0,
|
||||||
|
address: '',
|
||||||
|
balance: [],
|
||||||
|
client: undefined,
|
||||||
|
config: getConfig(NETWORK),
|
||||||
|
initialized: false,
|
||||||
|
initializing: true,
|
||||||
|
name: '',
|
||||||
|
network: NETWORK,
|
||||||
|
signer: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entrypoint for keplr wallet store using {@link defaultStates}
|
||||||
|
*/
|
||||||
|
export const useWalletStore = create(
|
||||||
|
subscribeWithSelector<KeplrWalletStore>((set, get) => ({
|
||||||
|
...defaultStates,
|
||||||
|
clear: () => set({ ...defaultStates }),
|
||||||
|
connect: async (walletChange = false) => {
|
||||||
|
try {
|
||||||
|
if (walletChange !== 'focus') set({ initializing: true })
|
||||||
|
const { config, init } = get()
|
||||||
|
const signer = await loadKeplrWallet(config)
|
||||||
|
init(signer)
|
||||||
|
if (walletChange) set({ initializing: false })
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error(err?.message)
|
||||||
|
set({ initializing: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disconnect: () => {
|
||||||
|
window.localStorage.clear()
|
||||||
|
get().clear()
|
||||||
|
set({ initializing: false })
|
||||||
|
},
|
||||||
|
getClient: () => get().client!,
|
||||||
|
getSigner: () => get().signer!,
|
||||||
|
init: (signer) => set({ signer }),
|
||||||
|
refreshBalance: async (address = get().address, balance = get().balance) => {
|
||||||
|
const { client, config } = get()
|
||||||
|
if (!client) return
|
||||||
|
balance.length = 0
|
||||||
|
for (const denom in config.coinMap) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const coin = await client.getBalance(address, denom)
|
||||||
|
if (coin) balance.push(coin)
|
||||||
|
}
|
||||||
|
set({ balance })
|
||||||
|
},
|
||||||
|
setNetwork: (network) => set({ network }),
|
||||||
|
updateSigner: (signer) => set({ signer }),
|
||||||
|
setQueryClient: async () => {
|
||||||
|
try {
|
||||||
|
const client = (await createQueryClient()) as SigningCosmWasmClient
|
||||||
|
set({ client })
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error(err?.message)
|
||||||
|
set({ initializing: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxied keplr wallet store which only rerenders on called state values.
|
||||||
|
*
|
||||||
|
* Recommended if only consuming state; to set states, use {@link useWalletStore.setState}.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* // this will rerender if any state values has changed
|
||||||
|
* const { name } = useWalletStore()
|
||||||
|
*
|
||||||
|
* // this will rerender if only `name` has changed
|
||||||
|
* const { name } = useWallet()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const useWallet = createTrackedSelector<KeplrWalletStore>(useWalletStore)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keplr wallet store provider to easily mount {@link WalletSubscription}
|
||||||
|
* to listen/subscribe various state changes.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const WalletProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<WalletSubscription />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keplr wallet subscriptions (side effects)
|
||||||
|
*/
|
||||||
|
const WalletSubscription = () => {
|
||||||
|
/**
|
||||||
|
* Dispatch reconnecting wallet on first mount and register events to refresh
|
||||||
|
* on keystore change and window refocus.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const walletAddress = window.localStorage.getItem('wallet_address')
|
||||||
|
if (walletAddress) {
|
||||||
|
void useWalletStore.getState().connect()
|
||||||
|
} else {
|
||||||
|
useWalletStore.setState({ initializing: false })
|
||||||
|
useWalletStore.getState().setQueryClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenChange = () => {
|
||||||
|
void useWalletStore.getState().connect(true)
|
||||||
|
}
|
||||||
|
const listenFocus = () => {
|
||||||
|
if (walletAddress) void useWalletStore.getState().connect('focus')
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keplr_keystorechange', listenChange)
|
||||||
|
window.addEventListener('focus', listenFocus)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keplr_keystorechange', listenChange)
|
||||||
|
window.removeEventListener('focus', listenFocus)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch signer changes to initialize client state.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
return useWalletStore.subscribe(
|
||||||
|
(x) => x.signer,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
async (signer) => {
|
||||||
|
try {
|
||||||
|
if (!signer) {
|
||||||
|
useWalletStore.setState({
|
||||||
|
client: (await createQueryClient()) as SigningCosmWasmClient,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
useWalletStore.setState({
|
||||||
|
client: await createClient({ signer }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch client changes to refresh balance and sync wallet states.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
return useWalletStore.subscribe(
|
||||||
|
(x) => x.client,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
async (client) => {
|
||||||
|
const { config, refreshBalance, signer } = useWalletStore.getState()
|
||||||
|
if (!signer || !client) return
|
||||||
|
if (!window.keplr) {
|
||||||
|
throw new Error('window.keplr not found')
|
||||||
|
}
|
||||||
|
const balance: Coin[] = []
|
||||||
|
const address = (await signer.getAccounts())[0].address
|
||||||
|
const account = await client.getAccount(address)
|
||||||
|
const key = await window.keplr.getKey(config.chainId)
|
||||||
|
await refreshBalance(address, balance)
|
||||||
|
window.localStorage.setItem('wallet_address', address)
|
||||||
|
useWalletStore.setState({
|
||||||
|
accountNumber: account?.accountNumber || 0,
|
||||||
|
address,
|
||||||
|
balance,
|
||||||
|
initialized: true,
|
||||||
|
initializing: false,
|
||||||
|
name: key.name || '',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to create signing client based on {@link useWalletStore} resolved
|
||||||
|
* config state.
|
||||||
|
*
|
||||||
|
* @param arg - Object argument requiring `signer`
|
||||||
|
*/
|
||||||
|
const createClient = ({ signer }: { signer: OfflineSigner }) => {
|
||||||
|
const { config } = useWalletStore.getState()
|
||||||
|
return SigningCosmWasmClient.connectWithSigner(config.rpcUrl, signer, {
|
||||||
|
gasPrice: {
|
||||||
|
amount: Decimal.fromUserInput('0.0025', 100),
|
||||||
|
denom: config.feeToken,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createQueryClient = () => {
|
||||||
|
const { config } = useWalletStore.getState()
|
||||||
|
return SigningCosmWasmClient.connect(config.rpcUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to load keplr wallet signer.
|
||||||
|
*
|
||||||
|
* @param config - Application configuration
|
||||||
|
*/
|
||||||
|
const loadKeplrWallet = async (config: AppConfig) => {
|
||||||
|
if (!window.getOfflineSigner || !window.keplr || !window.getOfflineSignerAuto) {
|
||||||
|
throw new Error('Keplr extension is not available')
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.keplr.experimentalSuggestChain(keplrConfig(config))
|
||||||
|
await window.keplr.enable(config.chainId)
|
||||||
|
|
||||||
|
const signer = await window.getOfflineSignerAuto(config.chainId)
|
||||||
|
Object.assign(signer, {
|
||||||
|
signAmino: (signer as any).signAmino ?? (signer as any).sign,
|
||||||
|
})
|
||||||
|
|
||||||
|
return signer
|
||||||
|
}
|
458
contracts/cw721/base/contract.ts
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||||
|
import { toBase64, toUtf8 } from '@cosmjs/encoding'
|
||||||
|
import type { Coin } from '@cosmjs/proto-signing'
|
||||||
|
// import { isDeliverTxFailure } from '@cosmjs/stargate'
|
||||||
|
// import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'
|
||||||
|
// import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
|
||||||
|
import { getExecuteFee } from 'utils/fees'
|
||||||
|
|
||||||
|
const jsonToBinary = (json: Record<string, unknown>): string => {
|
||||||
|
return toBase64(toUtf8(JSON.stringify(json)))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Expiration = { at_height: number } | { at_time: string } | { never: object }
|
||||||
|
|
||||||
|
// interface ExecuteWithSignDataResponse {
|
||||||
|
// signed: TxRaw
|
||||||
|
// txHash: string
|
||||||
|
// }
|
||||||
|
|
||||||
|
export interface InstantiateResponse {
|
||||||
|
readonly contractAddress: string
|
||||||
|
readonly transactionHash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CW721BaseInstance {
|
||||||
|
readonly contractAddress: string
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
ownerOf: (tokenId: string, includeExpired?: boolean) => Promise<any>
|
||||||
|
approval: (tokenId: string, spender: string, includeExpired?: boolean) => Promise<any>
|
||||||
|
approvals: (tokenId: string, includeExpired?: boolean) => Promise<any>
|
||||||
|
allOperators: (owner: string, includeExpired?: boolean, startAfter?: string, limit?: number) => Promise<any>
|
||||||
|
numTokens: () => Promise<any>
|
||||||
|
contractInfo: () => Promise<any>
|
||||||
|
nftInfo: (tokenId: string) => Promise<any>
|
||||||
|
allNftInfo: (tokenId: string, includeExpired?: boolean) => Promise<any>
|
||||||
|
tokens: (owner: string, startAfter?: string, limit?: number) => Promise<any>
|
||||||
|
allTokens: (startAfter?: string, limit?: number) => Promise<any>
|
||||||
|
minter: () => Promise<any>
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
transferNft: (recipient: string, tokenId: string) => Promise<string>
|
||||||
|
sendNft: (contract: string, tokenId: string, msg: Record<string, unknown>) => Promise<string>
|
||||||
|
approve: (spender: string, tokenId: string, expires?: Expiration) => Promise<string>
|
||||||
|
revoke: (spender: string, tokenId: string) => Promise<string>
|
||||||
|
approveAll: (operator: string, expires?: Expiration) => Promise<string>
|
||||||
|
revokeAll: (operator: string) => Promise<string>
|
||||||
|
mint: (tokenId: string, owner: string, tokenUri?: string) => Promise<string>
|
||||||
|
burn: (tokenId: string) => Promise<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CW721BaseMessages {
|
||||||
|
transferNft: (contractAddress: string, recipient: string, tokenId: string) => TransferNftMessage
|
||||||
|
sendNft: (contractAddress: string, contract: string, tokenId: string, msg: Record<string, unknown>) => SendNFtMessage
|
||||||
|
approve: (contractAddress: string, spender: string, tokenId: string, expires?: Expiration) => ApproveMessage
|
||||||
|
revoke: (contractAddress: string, spender: string, tokenId: string) => RevokeMessage
|
||||||
|
approveAll: (contractAddress: string, operator: string, expires?: Expiration) => ApproveAllMessage
|
||||||
|
revokeAll: (contractAddress: string, operator: string) => RevokeAllMessage
|
||||||
|
mint: (contractAddress: string, tokenId: string, owner: string, tokenUri?: string) => MintMessage
|
||||||
|
burn: (contractAddress: string, tokenId: string) => BurnMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransferNftMessage {
|
||||||
|
sender: string
|
||||||
|
contract: string
|
||||||
|
msg: {
|
||||||
|
transfer_nft: {
|
||||||
|
recipient: string
|
||||||
|
token_id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funds: Coin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendNFtMessage {
|
||||||
|
sender: string
|
||||||
|
contract: string
|
||||||
|
msg: {
|
||||||
|
send_nft: {
|
||||||
|
contract: string
|
||||||
|
token_id: string
|
||||||
|
msg: Record<string, unknown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funds: Coin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApproveMessage {
|
||||||
|
sender: string
|
||||||
|
contract: string
|
||||||
|
msg: {
|
||||||
|
approve: {
|
||||||
|
spender: string
|
||||||
|
token_id: string
|
||||||
|
expires?: Expiration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funds: Coin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RevokeMessage {
|
||||||
|
sender: string
|
||||||
|
contract: string
|
||||||
|
msg: {
|
||||||
|
revoke: {
|
||||||
|
spender: string
|
||||||
|
token_id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funds: Coin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApproveAllMessage {
|
||||||
|
sender: string
|
||||||
|
contract: string
|
||||||
|
msg: {
|
||||||
|
approve_all: {
|
||||||
|
operator: string
|
||||||
|
expires?: Expiration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funds: Coin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RevokeAllMessage {
|
||||||
|
sender: string
|
||||||
|
contract: string
|
||||||
|
msg: {
|
||||||
|
revoke_all: {
|
||||||
|
operator: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funds: Coin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MintMessage {
|
||||||
|
sender: string
|
||||||
|
contract: string
|
||||||
|
msg: {
|
||||||
|
mint: {
|
||||||
|
token_id: string
|
||||||
|
owner: string
|
||||||
|
token_uri?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funds: Coin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BurnMessage {
|
||||||
|
sender: string
|
||||||
|
contract: string
|
||||||
|
msg: {
|
||||||
|
burn: {
|
||||||
|
token_id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funds: Coin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CW721BaseContract {
|
||||||
|
instantiate: (
|
||||||
|
senderAddress: string,
|
||||||
|
codeId: number,
|
||||||
|
initMsg: Record<string, unknown>,
|
||||||
|
label: string,
|
||||||
|
admin?: string,
|
||||||
|
) => Promise<InstantiateResponse>
|
||||||
|
|
||||||
|
use: (contractAddress: string) => CW721BaseInstance
|
||||||
|
|
||||||
|
messages: () => CW721BaseMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CW721Base = (client: SigningCosmWasmClient, txSigner: string): CW721BaseContract => {
|
||||||
|
const fee = getExecuteFee()
|
||||||
|
|
||||||
|
const use = (contractAddress: string): CW721BaseInstance => {
|
||||||
|
const ownerOf = async (tokenId: string, includeExpired?: boolean) => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
owner_of: { token_id: tokenId, include_expired: includeExpired },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const approval = async (tokenId: string, spender: string, includeExpired?: boolean) => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
approval: { token_id: tokenId, spender, include_expired: includeExpired },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const approvals = async (tokenId: string, includeExpired?: boolean) => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
approvals: { token_id: tokenId, include_expired: includeExpired },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const allOperators = async (owner: string, includeExpired?: boolean, startAfter?: string, limit?: number) => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
all_operators: { owner, include_expired: includeExpired, start_after: startAfter, limit },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const numTokens = async () => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
num_tokens: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractInfo = async () => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
contract_info: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const nftInfo = async (tokenId: string) => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
nft_info: { token_id: tokenId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const allNftInfo = async (tokenId: string, includeExpired?: boolean) => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
all_nft_info: { token_id: tokenId, include_expired: includeExpired },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = async (owner: string, startAfter?: string, limit?: number) => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
tokens: { owner, start_after: startAfter, limit },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTokens = async (startAfter?: string, limit?: number) => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
all_tokens: { start_after: startAfter, limit },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const minter = async () => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
minter: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const transferNft = async (recipient: string, tokenId: string): Promise<string> => {
|
||||||
|
const result = await client.execute(
|
||||||
|
txSigner,
|
||||||
|
contractAddress,
|
||||||
|
{ transfer_nft: { recipient, token_id: tokenId } },
|
||||||
|
fee,
|
||||||
|
)
|
||||||
|
return result.transactionHash
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendNft = async (contract: string, tokenId: string, msg: Record<string, unknown>): Promise<string> => {
|
||||||
|
const result = await client.execute(
|
||||||
|
txSigner,
|
||||||
|
contractAddress,
|
||||||
|
{ send_nft: { contract, token_id: tokenId, msg: jsonToBinary(msg) } },
|
||||||
|
fee,
|
||||||
|
)
|
||||||
|
return result.transactionHash
|
||||||
|
}
|
||||||
|
|
||||||
|
const approve = async (spender: string, tokenId: string, expires?: Expiration): Promise<string> => {
|
||||||
|
const result = await client.execute(
|
||||||
|
txSigner,
|
||||||
|
contractAddress,
|
||||||
|
{ approve: { spender, token_id: tokenId, expires } },
|
||||||
|
fee,
|
||||||
|
)
|
||||||
|
return result.transactionHash
|
||||||
|
}
|
||||||
|
|
||||||
|
const revoke = async (spender: string, tokenId: string): Promise<string> => {
|
||||||
|
const result = await client.execute(txSigner, contractAddress, { revoke: { spender, token_id: tokenId } }, fee)
|
||||||
|
return result.transactionHash
|
||||||
|
}
|
||||||
|
|
||||||
|
const approveAll = async (operator: string, expires?: Expiration): Promise<string> => {
|
||||||
|
const result = await client.execute(txSigner, contractAddress, { approve_all: { operator, expires } }, fee)
|
||||||
|
return result.transactionHash
|
||||||
|
}
|
||||||
|
|
||||||
|
const revokeAll = async (operator: string): Promise<string> => {
|
||||||
|
const result = await client.execute(txSigner, contractAddress, { revoke_all: { operator } }, fee)
|
||||||
|
return result.transactionHash
|
||||||
|
}
|
||||||
|
|
||||||
|
const mint = async (tokenId: string, owner: string, tokenUri?: string): Promise<string> => {
|
||||||
|
const result = await client.execute(
|
||||||
|
txSigner,
|
||||||
|
contractAddress,
|
||||||
|
{ mint: { token_id: tokenId, owner, token_uri: tokenUri } },
|
||||||
|
fee,
|
||||||
|
)
|
||||||
|
return result.transactionHash
|
||||||
|
}
|
||||||
|
|
||||||
|
const burn = async (tokenId: string): Promise<string> => {
|
||||||
|
const result = await client.execute(txSigner, contractAddress, { burn: { token_id: tokenId } }, fee)
|
||||||
|
return result.transactionHash
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contractAddress,
|
||||||
|
ownerOf,
|
||||||
|
approval,
|
||||||
|
approvals,
|
||||||
|
allOperators,
|
||||||
|
numTokens,
|
||||||
|
contractInfo,
|
||||||
|
nftInfo,
|
||||||
|
allNftInfo,
|
||||||
|
tokens,
|
||||||
|
allTokens,
|
||||||
|
minter,
|
||||||
|
transferNft,
|
||||||
|
sendNft,
|
||||||
|
approve,
|
||||||
|
revoke,
|
||||||
|
approveAll,
|
||||||
|
revokeAll,
|
||||||
|
mint,
|
||||||
|
burn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const instantiate = async (
|
||||||
|
senderAddress: string,
|
||||||
|
codeId: number,
|
||||||
|
initMsg: Record<string, unknown>,
|
||||||
|
label: string,
|
||||||
|
admin?: string,
|
||||||
|
): Promise<InstantiateResponse> => {
|
||||||
|
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', {
|
||||||
|
memo: '',
|
||||||
|
admin,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
contractAddress: result.contractAddress,
|
||||||
|
transactionHash: result.transactionHash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = () => {
|
||||||
|
const transferNft = (contractAddress: string, recipient: string, tokenId: string): TransferNftMessage => {
|
||||||
|
return {
|
||||||
|
sender: txSigner,
|
||||||
|
contract: contractAddress,
|
||||||
|
msg: {
|
||||||
|
transfer_nft: { recipient, token_id: tokenId },
|
||||||
|
},
|
||||||
|
funds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendNft = (
|
||||||
|
contractAddress: string,
|
||||||
|
contract: string,
|
||||||
|
tokenId: string,
|
||||||
|
msg: Record<string, unknown>,
|
||||||
|
): SendNFtMessage => {
|
||||||
|
return {
|
||||||
|
sender: txSigner,
|
||||||
|
contract: contractAddress,
|
||||||
|
msg: {
|
||||||
|
send_nft: { contract, token_id: tokenId, msg },
|
||||||
|
},
|
||||||
|
funds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const approve = (
|
||||||
|
contractAddress: string,
|
||||||
|
spender: string,
|
||||||
|
tokenId: string,
|
||||||
|
expires?: Expiration,
|
||||||
|
): ApproveMessage => {
|
||||||
|
return {
|
||||||
|
sender: txSigner,
|
||||||
|
contract: contractAddress,
|
||||||
|
msg: {
|
||||||
|
approve: { spender, token_id: tokenId, expires },
|
||||||
|
},
|
||||||
|
funds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const revoke = (contractAddress: string, spender: string, tokenId: string): RevokeMessage => {
|
||||||
|
return {
|
||||||
|
sender: txSigner,
|
||||||
|
contract: contractAddress,
|
||||||
|
msg: {
|
||||||
|
revoke: { spender, token_id: tokenId },
|
||||||
|
},
|
||||||
|
funds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const approveAll = (contractAddress: string, operator: string, expires?: Expiration): ApproveAllMessage => {
|
||||||
|
return {
|
||||||
|
sender: txSigner,
|
||||||
|
contract: contractAddress,
|
||||||
|
msg: {
|
||||||
|
approve_all: { operator, expires },
|
||||||
|
},
|
||||||
|
funds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const revokeAll = (contractAddress: string, operator: string): RevokeAllMessage => {
|
||||||
|
return {
|
||||||
|
sender: txSigner,
|
||||||
|
contract: contractAddress,
|
||||||
|
msg: {
|
||||||
|
revoke_all: { operator },
|
||||||
|
},
|
||||||
|
funds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mint = (contractAddress: string, tokenId: string, owner: string, tokenUri?: string): MintMessage => {
|
||||||
|
return {
|
||||||
|
sender: txSigner,
|
||||||
|
contract: contractAddress,
|
||||||
|
msg: {
|
||||||
|
mint: { token_id: tokenId, owner, token_uri: tokenUri },
|
||||||
|
},
|
||||||
|
funds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const burn = (contractAddress: string, tokenId: string): BurnMessage => {
|
||||||
|
return {
|
||||||
|
sender: txSigner,
|
||||||
|
contract: contractAddress,
|
||||||
|
msg: {
|
||||||
|
burn: { token_id: tokenId },
|
||||||
|
},
|
||||||
|
funds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
transferNft,
|
||||||
|
sendNft,
|
||||||
|
approve,
|
||||||
|
revoke,
|
||||||
|
approveAll,
|
||||||
|
revokeAll,
|
||||||
|
mint,
|
||||||
|
burn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { instantiate, use, messages }
|
||||||
|
}
|
2
contracts/cw721/base/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './contract'
|
||||||
|
export * from './useContract'
|
73
contracts/cw721/base/useContract.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { useWallet } from 'contexts/wallet'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import type { CW721BaseContract, CW721BaseInstance, CW721BaseMessages } from './contract'
|
||||||
|
import { CW721Base as initContract } from './contract'
|
||||||
|
|
||||||
|
interface InstantiateResponse {
|
||||||
|
readonly contractAddress: string
|
||||||
|
readonly transactionHash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCW721BaseContractProps {
|
||||||
|
instantiate: (
|
||||||
|
codeId: number,
|
||||||
|
initMsg: Record<string, unknown>,
|
||||||
|
label: string,
|
||||||
|
admin?: string,
|
||||||
|
) => Promise<InstantiateResponse>
|
||||||
|
use: (customAddress: string) => CW721BaseInstance | undefined
|
||||||
|
updateContractAddress: (contractAddress: string) => void
|
||||||
|
messages: () => CW721BaseMessages | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCW721BaseContract(): UseCW721BaseContractProps {
|
||||||
|
const wallet = useWallet()
|
||||||
|
|
||||||
|
const [address, setAddress] = useState<string>('')
|
||||||
|
const [CW721Base, setCW721Base] = useState<CW721BaseContract>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAddress(localStorage.getItem('contract_address') || '')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cw721BaseContract = initContract(wallet.getClient(), wallet.address)
|
||||||
|
setCW721Base(cw721BaseContract)
|
||||||
|
}, [wallet])
|
||||||
|
|
||||||
|
const updateContractAddress = (contractAddress: string) => {
|
||||||
|
setAddress(contractAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
const instantiate = useCallback(
|
||||||
|
(codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<InstantiateResponse> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!CW721Base) {
|
||||||
|
reject(new Error('Contract is not initialized.'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
CW721Base.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[CW721Base, wallet],
|
||||||
|
)
|
||||||
|
|
||||||
|
const use = useCallback(
|
||||||
|
(customAddress = ''): CW721BaseInstance | undefined => {
|
||||||
|
return CW721Base?.use(address || customAddress)
|
||||||
|
},
|
||||||
|
[CW721Base, address],
|
||||||
|
)
|
||||||
|
|
||||||
|
const messages = useCallback((): CW721BaseMessages | undefined => {
|
||||||
|
return CW721Base?.messages()
|
||||||
|
}, [CW721Base])
|
||||||
|
|
||||||
|
return {
|
||||||
|
instantiate,
|
||||||
|
use,
|
||||||
|
updateContractAddress,
|
||||||
|
messages,
|
||||||
|
}
|
||||||
|
}
|
41
env.d.ts
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
const Component: (props: import('react').SVGProps<SVGSVGElement>) => JSX.Element
|
||||||
|
export default Component
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'react-datetime-picker/dist/entry.nostyle' {
|
||||||
|
export { default } from 'react-datetime-picker'
|
||||||
|
export * from 'react-datetime-picker'
|
||||||
|
}
|
||||||
|
|
||||||
|
declare namespace NodeJS {
|
||||||
|
declare interface ProcessEnv {
|
||||||
|
readonly APP_VERSION: string
|
||||||
|
|
||||||
|
readonly NEXT_PUBLIC_CW721_BASE_CODE_ID: string
|
||||||
|
|
||||||
|
readonly NEXT_PUBLIC_API_URL: string
|
||||||
|
readonly NEXT_PUBLIC_BLOCK_EXPLORER_URL: string
|
||||||
|
readonly NEXT_PUBLIC_NETWORK: string
|
||||||
|
readonly NEXT_PUBLIC_WEBSITE_URL: string
|
||||||
|
|
||||||
|
readonly NEXT_PUBLIC_S3_BUCKET: string
|
||||||
|
readonly NEXT_PUBLIC_S3_ENDPOINT: string
|
||||||
|
readonly NEXT_PUBLIC_S3_KEY: string
|
||||||
|
readonly NEXT_PUBLIC_S3_REGION: string
|
||||||
|
readonly NEXT_PUBLIC_S3_SECRET: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeplrWindow = import('@keplr-wallet/types/src/window').Window
|
||||||
|
|
||||||
|
declare interface Window extends KeplrWindow {
|
||||||
|
confetti?: (obj: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const __DEV__: boolean
|
||||||
|
declare const __PROD__: boolean
|
||||||
|
|
||||||
|
/* eslint-enable import/no-default-export */
|
19
hooks/useInterval.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export const useInterval = (callback: () => void, delay: number) => {
|
||||||
|
const savedCallback = useRef(callback)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
savedCallback.current = callback
|
||||||
|
}, [callback])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function tick() {
|
||||||
|
savedCallback.current()
|
||||||
|
}
|
||||||
|
if (delay !== null) {
|
||||||
|
const id = setInterval(tick, delay)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}
|
||||||
|
}, [delay])
|
||||||
|
}
|
5
next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
37
next.config.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const packageJson = require('./package.json')
|
||||||
|
|
||||||
|
const LOCALHOST_URL = `http://localhost:${process.env.PORT || 3000}`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("next").NextConfig}
|
||||||
|
* @see https://nextjs.org/docs/api-reference/next.config.js/introduction
|
||||||
|
*/
|
||||||
|
const nextConfig = {
|
||||||
|
env: {
|
||||||
|
APP_VERSION: packageJson.version,
|
||||||
|
NEXT_PUBLIC_WEBSITE_URL:
|
||||||
|
process.env.NODE_ENV === 'development' ? LOCALHOST_URL : process.env.NEXT_PUBLIC_WEBSITE_URL,
|
||||||
|
},
|
||||||
|
reactStrictMode: true,
|
||||||
|
trailingSlash: true,
|
||||||
|
webpack(config, { dev, webpack }) {
|
||||||
|
// svgr integration
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.svg$/i,
|
||||||
|
issuer: /\.[jt]sx?$/,
|
||||||
|
use: ['@svgr/webpack'],
|
||||||
|
})
|
||||||
|
// predefined constants
|
||||||
|
config.plugins.push(
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
__DEV__: dev,
|
||||||
|
__PROD__: !dev,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
80
package.json
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "stargaze-tools",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "next build",
|
||||||
|
"dev": "next dev",
|
||||||
|
"lint": "eslint --fix \"**/*.{js,cjs,mjs,jsx,ts,tsx}\" && tsc --noEmit --pretty",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"start": "next start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3",
|
||||||
|
"@cosmjs/cosmwasm-stargate": "^0",
|
||||||
|
"@cosmjs/encoding": "^0",
|
||||||
|
"@cosmjs/math": "^0",
|
||||||
|
"@cosmjs/proto-signing": "^0",
|
||||||
|
"@cosmjs/stargate": "^0",
|
||||||
|
"@fontsource/jetbrains-mono": "^4",
|
||||||
|
"@fontsource/roboto": "^4",
|
||||||
|
"@headlessui/react": "^1",
|
||||||
|
"@keplr-wallet/cosmos": "^0.9.16",
|
||||||
|
"@popperjs/core": "^2",
|
||||||
|
"@svgr/webpack": "^6",
|
||||||
|
"@tailwindcss/forms": "^0",
|
||||||
|
"@tailwindcss/line-clamp": "^0",
|
||||||
|
"axios": "^0",
|
||||||
|
"clsx": "^1",
|
||||||
|
"compare-versions": "^4",
|
||||||
|
"match-sorter": "^6",
|
||||||
|
"next": "^12",
|
||||||
|
"next-seo": "^4",
|
||||||
|
"react": "^18",
|
||||||
|
"react-datetime-picker": "^3",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"react-hook-form": "^7",
|
||||||
|
"react-hot-toast": "^2",
|
||||||
|
"react-icons": "^4",
|
||||||
|
"react-popper": "^2",
|
||||||
|
"react-query": "^3",
|
||||||
|
"react-tracked": "^1",
|
||||||
|
"scheduler": "^0",
|
||||||
|
"zustand": "^3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^14",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-datetime-picker": "^3",
|
||||||
|
"autoprefixer": "^10",
|
||||||
|
"husky": "^7",
|
||||||
|
"lint-staged": "^12",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3",
|
||||||
|
"typescript": "^4"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"@stargaze-tools/eslint-config"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
".next",
|
||||||
|
"node_modules",
|
||||||
|
"out",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
"root": true
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{json,md}": [
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"**/*.{js,cjs,mjs,jsx,ts,tsx}": [
|
||||||
|
"eslint --fix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": "@stargaze-tools/prettier-config",
|
||||||
|
"private": true
|
||||||
|
}
|
71
packages/eslint-config/index.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
const tsConfig = fs.existsSync('tsconfig.json')
|
||||||
|
? path.resolve('tsconfig.json')
|
||||||
|
: fs.existsSync('types/tsconfig.json')
|
||||||
|
? path.resolve('types/tsconfig.json')
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
const eslintConfig = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
require.resolve('@vercel/style-guide/eslint/_base'),
|
||||||
|
require.resolve('@vercel/style-guide/eslint/react'),
|
||||||
|
require.resolve('@vercel/style-guide/eslint/next'),
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
plugins: ['simple-import-sort', 'tailwindcss', 'unused-imports'],
|
||||||
|
rules: {
|
||||||
|
'@next/next/no-img-element': ['off'],
|
||||||
|
'import/extensions': ['off'],
|
||||||
|
'import/newline-after-import': ['warn'],
|
||||||
|
'import/order': ['off'],
|
||||||
|
'no-console': ['warn'],
|
||||||
|
'no-unused-vars': ['off'],
|
||||||
|
'react/function-component-definition': ['off'],
|
||||||
|
'react/jsx-sort-props': ['warn', { reservedFirst: ['key'] }],
|
||||||
|
'simple-import-sort/exports': ['warn'],
|
||||||
|
'simple-import-sort/imports': ['warn'],
|
||||||
|
'sort-imports': ['off'],
|
||||||
|
'tailwindcss/classnames-order': ['warn'],
|
||||||
|
'unicorn/filename-case': ['error', { cases: { camelCase: true, kebabCase: true, pascalCase: true } }],
|
||||||
|
'unused-imports/no-unused-imports': 'error',
|
||||||
|
'unused-imports/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.d.ts', '**/*.ts', '**/*.tsx'],
|
||||||
|
extends: [require.resolve('@vercel/style-guide/eslint/typescript'), 'plugin:prettier/recommended'],
|
||||||
|
parserOptions: {
|
||||||
|
project: tsConfig,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-misused-promises': ['warn'],
|
||||||
|
'@typescript-eslint/no-unsafe-argument': ['warn'],
|
||||||
|
'@typescript-eslint/no-unsafe-assignment': ['warn'],
|
||||||
|
'@typescript-eslint/no-unsafe-member-access': ['warn'],
|
||||||
|
'@typescript-eslint/no-unsafe-return': ['warn'],
|
||||||
|
'@typescript-eslint/no-unused-vars': ['off'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['pages/**/*.{js,jsx,ts,tsx}', 'next.config.{js,cjs,mjs}'],
|
||||||
|
rules: {
|
||||||
|
'import/no-default-export': ['off'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = eslintConfig
|
31
packages/eslint-config/package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "@stargaze-tools/eslint-config",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7",
|
||||||
|
"@vercel/style-guide": "^3",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-plugin-prettier": "^4",
|
||||||
|
"eslint-plugin-simple-import-sort": "^7",
|
||||||
|
"eslint-plugin-tailwindcss": "^3",
|
||||||
|
"eslint-plugin-unused-imports": "^2",
|
||||||
|
"prettier": "^2",
|
||||||
|
"typescript": "^4"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"./index.js"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"root": true
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,json,md}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": "@stargaze-tools/prettier-config",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
12
packages/prettier-config/index.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
const prettierConfig = {
|
||||||
|
endOfLine: 'auto',
|
||||||
|
printWidth: 120,
|
||||||
|
semi: false,
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'all',
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = prettierConfig
|
26
packages/prettier-config/package.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@stargaze-tools/prettier-config",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"prettier": "^2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/prettier": "^2"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"@stargaze-tools/eslint-config"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"root": true
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,json,md}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": "./index.js",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
4
pages/404.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// pages/404.js
|
||||||
|
export default function Custom404() {
|
||||||
|
return <h1>Page Not Found</h1>
|
||||||
|
}
|
30
pages/_app.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import '@fontsource/jetbrains-mono/latin.css'
|
||||||
|
import '@fontsource/roboto/latin.css'
|
||||||
|
import '../styles/globals.css'
|
||||||
|
import '../styles/datepicker.css'
|
||||||
|
|
||||||
|
import { Layout } from 'components/Layout'
|
||||||
|
import { Modal } from 'components/Modal'
|
||||||
|
import { queryClient } from 'config/react-query'
|
||||||
|
import { ContractsProvider } from 'contexts/contracts'
|
||||||
|
import { WalletProvider } from 'contexts/wallet'
|
||||||
|
import type { AppProps } from 'next/app'
|
||||||
|
import { Toaster } from 'react-hot-toast'
|
||||||
|
import { QueryClientProvider } from 'react-query'
|
||||||
|
import { getComponentMetadata } from 'utils/layout'
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<WalletProvider>
|
||||||
|
<ContractsProvider>
|
||||||
|
<Toaster position="top-right" />
|
||||||
|
<Layout metadata={getComponentMetadata(Component)}>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
<Modal />
|
||||||
|
</Layout>
|
||||||
|
</ContractsProvider>
|
||||||
|
</WalletProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
22
pages/_document.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { FaviconsMetaTags } from 'components/FaviconsMetaTags'
|
||||||
|
import NextDocument, { Head, Html, Main, NextScript } from 'next/document'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export default class CustomDocument extends NextDocument {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head>
|
||||||
|
<meta charSet="UTF-8" />
|
||||||
|
<meta content="ie=edge" httpEquiv="X-UA-Compatible" />
|
||||||
|
<FaviconsMetaTags />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<body className="font-sans antialiased">
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
48
pages/brand.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { BrandPreviewProps } from 'components/BrandPreview'
|
||||||
|
import { BrandPreview } from 'components/BrandPreview'
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
import { NextSeo } from 'next-seo'
|
||||||
|
import { withMetadata } from 'utils/layout'
|
||||||
|
|
||||||
|
const ASSETS: BrandPreviewProps[] = [
|
||||||
|
{
|
||||||
|
name: 'StargazeTools',
|
||||||
|
id: 'brand',
|
||||||
|
url: 'brand/brand.svg',
|
||||||
|
Asset: dynamic(() => import('public/brand/brand.svg')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'StargazeTools Bust',
|
||||||
|
id: 'brand-bust',
|
||||||
|
url: 'brand/brand-bust.svg',
|
||||||
|
Asset: dynamic(() => import('public/brand/brand-bust.svg')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'StargazeTools Text',
|
||||||
|
id: 'brand-text',
|
||||||
|
url: 'brand/brand-text.svg',
|
||||||
|
Asset: dynamic(() => import('public/brand/brand-text.svg')),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const BrandPage: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<section className="p-8 pb-16 space-y-8">
|
||||||
|
<NextSeo title="Brand Assets" />
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h1 className="text-4xl font-bold">Brand Assets</h1>
|
||||||
|
<p>View and download StargazeTools brand assets</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-white/20" />
|
||||||
|
|
||||||
|
{ASSETS.map((props, i) => (
|
||||||
|
<BrandPreview key={`asset-${i}`} {...props} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMetadata(BrandPage, { center: false })
|
148
pages/contracts/cw721/base/execute.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { Button } from 'components/Button'
|
||||||
|
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||||
|
import { ExecuteCombobox } from 'components/contracts/cw721/base/ExecuteCombobox'
|
||||||
|
import { useExecuteComboboxState } from 'components/contracts/cw721/base/ExecuteCombobox.hooks'
|
||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import { AddressInput } from 'components/forms/FormInput'
|
||||||
|
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||||
|
import { JsonTextArea } from 'components/forms/FormTextArea'
|
||||||
|
import { JsonPreview } from 'components/JsonPreview'
|
||||||
|
import { LinkTabs } from 'components/LinkTabs'
|
||||||
|
import { cw721BaseLinkTabs } from 'components/LinkTabs.data'
|
||||||
|
import { TransactionHash } from 'components/TransactionHash'
|
||||||
|
import { useContracts } from 'contexts/contracts'
|
||||||
|
import { useWallet } from 'contexts/wallet'
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
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 type { DispatchExecuteArgs } from 'utils/contracts/cw721/base/execute'
|
||||||
|
import { dispatchExecute, isEitherType, previewExecutePayload } from 'utils/contracts/cw721/base/execute'
|
||||||
|
import { parseJson } from 'utils/json'
|
||||||
|
import { withMetadata } from 'utils/layout'
|
||||||
|
import { links } from 'utils/links'
|
||||||
|
|
||||||
|
const CW721BaseExecutePage: NextPage = () => {
|
||||||
|
const { cw721Base: contract } = useContracts()
|
||||||
|
const wallet = useWallet()
|
||||||
|
const [lastTx, setLastTx] = useState('')
|
||||||
|
|
||||||
|
const comboboxState = useExecuteComboboxState()
|
||||||
|
const type = comboboxState.value?.id
|
||||||
|
|
||||||
|
const contractState = useInputState({
|
||||||
|
id: 'contract-address',
|
||||||
|
name: 'contract-address',
|
||||||
|
title: 'CW721 Contract Address',
|
||||||
|
subtitle: 'Address of the CW721 contract',
|
||||||
|
})
|
||||||
|
|
||||||
|
const messageState = useInputState({
|
||||||
|
id: 'message',
|
||||||
|
name: 'message',
|
||||||
|
title: 'Message',
|
||||||
|
subtitle: 'Message to execute on the contract',
|
||||||
|
defaultValue: JSON.stringify({ key: 'value' }, null, 2),
|
||||||
|
})
|
||||||
|
|
||||||
|
const recipientState = useInputState({
|
||||||
|
id: 'recipient-address',
|
||||||
|
name: 'recipient',
|
||||||
|
title: 'Recipient Address',
|
||||||
|
subtitle: 'Address of the recipient',
|
||||||
|
})
|
||||||
|
|
||||||
|
const tokenIdState = useInputState({
|
||||||
|
id: 'token-id',
|
||||||
|
name: 'token-id',
|
||||||
|
title: 'Token ID',
|
||||||
|
subtitle: 'Identifier of the token',
|
||||||
|
placeholder: 'some_token_id',
|
||||||
|
})
|
||||||
|
|
||||||
|
const showMessageField = type === 'send_nft'
|
||||||
|
const showRecipientField = isEitherType(type, [
|
||||||
|
'transfer_nft',
|
||||||
|
'send_nft',
|
||||||
|
'approve',
|
||||||
|
'revoke',
|
||||||
|
'approve_all',
|
||||||
|
'revoke_all',
|
||||||
|
'mint',
|
||||||
|
])
|
||||||
|
const showTokenIdField = isEitherType(type, ['transfer_nft', 'send_nft', 'approve', 'revoke', 'mint', 'burn'])
|
||||||
|
|
||||||
|
const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
|
||||||
|
const payload: DispatchExecuteArgs = {
|
||||||
|
contract: contractState.value,
|
||||||
|
messages,
|
||||||
|
msg: parseJson(messageState.value) || {},
|
||||||
|
recipient: recipientState.value,
|
||||||
|
txSigner: wallet.address,
|
||||||
|
type,
|
||||||
|
tokenId: tokenIdState.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isLoading, mutate } = useMutation(
|
||||||
|
async (event: FormEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!type) {
|
||||||
|
throw new Error('Please select message type!')
|
||||||
|
}
|
||||||
|
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) => {
|
||||||
|
console.error(error)
|
||||||
|
toast.error(String(error))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-6 px-12 space-y-4">
|
||||||
|
<NextSeo title="Execute CW721 Base Contract" />
|
||||||
|
<ContractPageHeader
|
||||||
|
description="CW721 Base is a specification for non fungible tokens based on CosmWasm."
|
||||||
|
link={links['Docs CW721 Base']}
|
||||||
|
title="CW721 Base Contract"
|
||||||
|
/>
|
||||||
|
<LinkTabs activeIndex={2} data={cw721BaseLinkTabs} />
|
||||||
|
|
||||||
|
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<AddressInput {...contractState} />
|
||||||
|
<ExecuteCombobox {...comboboxState} />
|
||||||
|
{showRecipientField && <AddressInput {...recipientState} />}
|
||||||
|
{showTokenIdField && <AddressInput {...tokenIdState} />}
|
||||||
|
{showMessageField && <JsonTextArea {...messageState} />}
|
||||||
|
</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>
|
||||||
|
<FormControl subtitle="View current message to be sent" title="Payload Preview">
|
||||||
|
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMetadata(CW721BaseExecutePage, { center: false })
|
1
pages/contracts/cw721/base/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './instantiate'
|
113
pages/contracts/cw721/base/instantiate.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { Alert } from 'components/Alert'
|
||||||
|
import { Button } from 'components/Button'
|
||||||
|
import { Conditional } from 'components/Conditional'
|
||||||
|
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||||
|
import { FormGroup } from 'components/FormGroup'
|
||||||
|
import { TextInput } from 'components/forms/FormInput'
|
||||||
|
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||||
|
import { JsonPreview } from 'components/JsonPreview'
|
||||||
|
import { LinkTabs } from 'components/LinkTabs'
|
||||||
|
import { cw721BaseLinkTabs } from 'components/LinkTabs.data'
|
||||||
|
import { useContracts } from 'contexts/contracts'
|
||||||
|
import { useWallet } from 'contexts/wallet'
|
||||||
|
import type { InstantiateResponse } from 'contracts/cw721/base'
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import { NextSeo } from 'next-seo'
|
||||||
|
import type { FormEvent } from 'react'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { FaAsterisk } from 'react-icons/fa'
|
||||||
|
import { useMutation } from 'react-query'
|
||||||
|
import { CW721_BASE_CODE_ID } from 'utils/constants'
|
||||||
|
import { withMetadata } from 'utils/layout'
|
||||||
|
import { links } from 'utils/links'
|
||||||
|
|
||||||
|
const CW721BaseInstantiatePage: NextPage = () => {
|
||||||
|
const wallet = useWallet()
|
||||||
|
const contract = useContracts().cw721Base
|
||||||
|
|
||||||
|
const nameState = useInputState({
|
||||||
|
id: 'name',
|
||||||
|
name: 'name',
|
||||||
|
title: 'Name',
|
||||||
|
placeholder: 'My Awesome CW721 Contract',
|
||||||
|
})
|
||||||
|
|
||||||
|
const symbolState = useInputState({
|
||||||
|
id: 'symbol',
|
||||||
|
name: 'symbol',
|
||||||
|
title: 'Symbol',
|
||||||
|
placeholder: 'AWSM',
|
||||||
|
})
|
||||||
|
|
||||||
|
const minterState = useInputState({
|
||||||
|
id: 'minter-address',
|
||||||
|
name: 'minterAddress',
|
||||||
|
title: 'Minter Address',
|
||||||
|
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data, isLoading, mutate } = useMutation(
|
||||||
|
async (event: FormEvent): Promise<InstantiateResponse | null> => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!contract) {
|
||||||
|
throw new Error('Smart contract connection failed')
|
||||||
|
}
|
||||||
|
const msg = {
|
||||||
|
name: nameState.value,
|
||||||
|
symbol: symbolState.value,
|
||||||
|
minter: minterState.value,
|
||||||
|
}
|
||||||
|
return toast.promise(
|
||||||
|
contract.instantiate(CW721_BASE_CODE_ID, msg, 'StargazeTools CW721 Base Contract', wallet.address),
|
||||||
|
{
|
||||||
|
loading: 'Instantiating contract...',
|
||||||
|
error: 'Instantiation failed!',
|
||||||
|
success: 'Instantiation success!',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(String(error))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const txHash = data?.transactionHash
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="py-6 px-12 space-y-4" onSubmit={mutate}>
|
||||||
|
<NextSeo title="Instantiate CW721 Base Contract" />
|
||||||
|
<ContractPageHeader
|
||||||
|
description="CW721 Base is a specification for non fungible tokens based on CosmWasm."
|
||||||
|
link={links['Docs CW721 Base']}
|
||||||
|
title="CW721 Base Contract"
|
||||||
|
/>
|
||||||
|
<LinkTabs activeIndex={0} data={cw721BaseLinkTabs} />
|
||||||
|
|
||||||
|
<Conditional test={Boolean(data)}>
|
||||||
|
<Alert type="info">
|
||||||
|
<b>Instantiate success!</b> Here is the transaction result containing the contract address and the transaction
|
||||||
|
hash.
|
||||||
|
</Alert>
|
||||||
|
<JsonPreview content={data} title="Transaction Result" />
|
||||||
|
<br />
|
||||||
|
</Conditional>
|
||||||
|
|
||||||
|
<FormGroup subtitle="Basic information about your new contract" title="Contract Details">
|
||||||
|
<TextInput isRequired {...nameState} />
|
||||||
|
<TextInput isRequired {...symbolState} />
|
||||||
|
<TextInput isRequired {...minterState} />
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<div className="flex items-center p-4">
|
||||||
|
<div className="flex-grow" />
|
||||||
|
<Button isLoading={isLoading} isWide rightIcon={<FaAsterisk />} type="submit">
|
||||||
|
Instantiate Contract
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMetadata(CW721BaseInstantiatePage, { center: false })
|
140
pages/contracts/cw721/base/query.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { Conditional } from 'components/Conditional'
|
||||||
|
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import { AddressInput } from 'components/forms/FormInput'
|
||||||
|
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||||
|
import { JsonPreview } from 'components/JsonPreview'
|
||||||
|
import { LinkTabs } from 'components/LinkTabs'
|
||||||
|
import { cw721BaseLinkTabs } from 'components/LinkTabs.data'
|
||||||
|
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 { useEffect, useState } from 'react'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { useQuery } from 'react-query'
|
||||||
|
import type { QueryType } from 'utils/contracts/cw721/base/query'
|
||||||
|
import { dispatchQuery, QUERY_LIST } from 'utils/contracts/cw721/base/query'
|
||||||
|
import { withMetadata } from 'utils/layout'
|
||||||
|
import { links } from 'utils/links'
|
||||||
|
|
||||||
|
const CW721QueryPage: NextPage = () => {
|
||||||
|
const { cw721Base: contract } = useContracts()
|
||||||
|
const wallet = useWallet()
|
||||||
|
|
||||||
|
const contractState = useInputState({
|
||||||
|
id: 'contract-address',
|
||||||
|
name: 'contract-address',
|
||||||
|
title: 'CW721 contract Address',
|
||||||
|
subtitle: 'Address of the CW721 contract',
|
||||||
|
})
|
||||||
|
const address = contractState.value
|
||||||
|
|
||||||
|
const ownerState = useInputState({
|
||||||
|
id: 'owner-address',
|
||||||
|
name: 'owner-address',
|
||||||
|
title: 'Owner Address',
|
||||||
|
subtitle: 'Address of the owner',
|
||||||
|
})
|
||||||
|
const ownerAddress = ownerState.value
|
||||||
|
|
||||||
|
const tokenIdState = useInputState({
|
||||||
|
id: 'token-id',
|
||||||
|
name: 'token-id',
|
||||||
|
title: 'Token ID',
|
||||||
|
subtitle: 'Identifier of the token',
|
||||||
|
placeholder: 'some_token_id',
|
||||||
|
})
|
||||||
|
const tokenId = tokenIdState.value
|
||||||
|
|
||||||
|
const [type, setType] = useState<QueryType>('owner_of')
|
||||||
|
|
||||||
|
const addressVisible = type === 'approval' || type === 'all_operators' || type === 'tokens'
|
||||||
|
const tokenVisible =
|
||||||
|
type === 'owner_of' || type === 'approval' || type === 'approvals' || type === 'nft_info' || type === 'all_nft_info'
|
||||||
|
|
||||||
|
const { data: response } = useQuery(
|
||||||
|
[address, type, contract, wallet, ownerAddress, tokenId] as const,
|
||||||
|
async ({ queryKey }) => {
|
||||||
|
const [_address, _type, _contract, _wallet, _ownerAddress, _tokenId] = queryKey
|
||||||
|
const messages = contract?.use(_address)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
const ownerAddress = _ownerAddress || _wallet.address
|
||||||
|
|
||||||
|
const result = await dispatchQuery({
|
||||||
|
ownerAddress,
|
||||||
|
tokenId,
|
||||||
|
messages,
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: null,
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.message)
|
||||||
|
},
|
||||||
|
enabled: Boolean(address && type && contract && wallet),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (address.length > 0) {
|
||||||
|
void router.replace({ query: { contractAddress: address } })
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [address])
|
||||||
|
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="Query CW721 Base Contract" />
|
||||||
|
<ContractPageHeader
|
||||||
|
description="CW721 Base is a specification for non fungible tokens based on CosmWasm."
|
||||||
|
link={links['Docs CW721 Base']}
|
||||||
|
title="CW721 Base Contract"
|
||||||
|
/>
|
||||||
|
<LinkTabs activeIndex={1} data={cw721BaseLinkTabs} />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 p-4 space-x-8">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<AddressInput {...contractState} />
|
||||||
|
<FormControl htmlId="contract-query-type" subtitle="Type of query to be dispatched" title="Query Type">
|
||||||
|
<select
|
||||||
|
className={clsx(
|
||||||
|
'bg-white/10 rounded border-2 border-white/20 form-select',
|
||||||
|
'placeholder:text-white/50',
|
||||||
|
'focus:ring focus:ring-plumbus-20',
|
||||||
|
)}
|
||||||
|
id="contract-query-type"
|
||||||
|
name="query-type"
|
||||||
|
onChange={(e) => setType(e.target.value as QueryType)}
|
||||||
|
>
|
||||||
|
{QUERY_LIST.map(({ id, name }) => (
|
||||||
|
<option key={`query-${id}`} value={id}>
|
||||||
|
{name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FormControl>
|
||||||
|
<Conditional test={addressVisible}>
|
||||||
|
<AddressInput {...ownerState} />
|
||||||
|
</Conditional>
|
||||||
|
<Conditional test={tokenVisible}>
|
||||||
|
<AddressInput {...tokenIdState} />
|
||||||
|
</Conditional>
|
||||||
|
</div>
|
||||||
|
<JsonPreview content={address ? { type, response } : null} title="Query Response" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMetadata(CW721QueryPage, { center: false })
|
5
pages/contracts/index.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const Minting = () => {
|
||||||
|
return <div>dsadsa</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Minting
|
117
pages/contracts/upload.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { Alert } from 'components/Alert'
|
||||||
|
import { Button } from 'components/Button'
|
||||||
|
import { Conditional } from 'components/Conditional'
|
||||||
|
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||||
|
import { JsonPreview } from 'components/JsonPreview'
|
||||||
|
import { useWallet } from 'contexts/wallet'
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import { NextSeo } from 'next-seo'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { FaAsterisk } from 'react-icons/fa'
|
||||||
|
import { withMetadata } from 'utils/layout'
|
||||||
|
|
||||||
|
const UploadContract: NextPage = () => {
|
||||||
|
const { getClient, address } = useWallet()
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [transactionResult, setTransactionResult] = useState<any>()
|
||||||
|
const [wasmFile, setWasmFile] = useState<File | null>(null)
|
||||||
|
const [wasmByteArray, setWasmByteArray] = useState<Uint8Array | null>(null)
|
||||||
|
|
||||||
|
const inputFile = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!e.target.files) return
|
||||||
|
setWasmFile(e.target.files[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasmFile) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||||
|
const byteArray = new Uint8Array(e.target.result as ArrayBuffer)
|
||||||
|
setWasmByteArray(byteArray)
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(wasmFile)
|
||||||
|
}
|
||||||
|
}, [wasmFile])
|
||||||
|
|
||||||
|
const upload = async () => {
|
||||||
|
try {
|
||||||
|
if (!wasmFile || !wasmByteArray) return toast.error('No file selected.')
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
|
||||||
|
const result = await client.upload(address, wasmByteArray, 'auto')
|
||||||
|
|
||||||
|
setTransactionResult({
|
||||||
|
transactionHash: result.transactionHash,
|
||||||
|
codeId: result.codeId,
|
||||||
|
originalSize: result.originalSize,
|
||||||
|
compressedSize: result.compressedSize,
|
||||||
|
originalChecksum: result.originalChecksum,
|
||||||
|
compressedChecksum: result.compressedChecksum,
|
||||||
|
})
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
} catch (err: any) {
|
||||||
|
setLoading(false)
|
||||||
|
toast.error(err.message, { style: { maxWidth: 'none' } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-6 px-12 space-y-4">
|
||||||
|
<NextSeo title="Upload Contract" />
|
||||||
|
<ContractPageHeader
|
||||||
|
description="Here you can upload a contract on Stargaze Network."
|
||||||
|
link=""
|
||||||
|
title="Upload Contract"
|
||||||
|
/>
|
||||||
|
<div className="inset-x-0 bottom-0 border-b-2 border-white/25" />
|
||||||
|
|
||||||
|
<Conditional test={Boolean(transactionResult)}>
|
||||||
|
<Alert type="info">
|
||||||
|
<b>Upload success!</b> Here is the transaction result containing the code ID, transaction hash and other data.
|
||||||
|
</Alert>
|
||||||
|
<JsonPreview content={transactionResult} title="Transaction Result" />
|
||||||
|
<br />
|
||||||
|
</Conditional>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'flex relative justify-center items-center space-y-4 h-32',
|
||||||
|
'rounded border-2 border-white/20 border-dashed',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
accept=".wasm"
|
||||||
|
className={clsx(
|
||||||
|
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||||
|
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||||
|
)}
|
||||||
|
onChange={onFileChange}
|
||||||
|
ref={inputFile}
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pb-6">
|
||||||
|
<Button isDisabled={!wasmFile} isLoading={loading} isWide leftIcon={<FaAsterisk />} onClick={upload}>
|
||||||
|
Upload Contract
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMetadata(UploadContract, { center: false })
|
75
pages/index.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { HomeCard } from 'components/HomeCard'
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import Brand from 'public/brand/brand.svg'
|
||||||
|
import { withMetadata } from 'utils/layout'
|
||||||
|
|
||||||
|
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">
|
||||||
|
{/* <Brand className="w-full text-plumbus" /> */}
|
||||||
|
</div>
|
||||||
|
<h1 className="font-heading text-4xl font-bold">Welcome!</h1>
|
||||||
|
<p className="text-xl">
|
||||||
|
Looking for a fast and efficient way to build an NFT collection?
|
||||||
|
Stargaze Tools is the solution.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Stargaze Tools is built to provide useful smart contract interfaces that
|
||||||
|
helps you build and deploy your own NFT collection in no time.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div className="grid gap-8 md:grid-cols-2">
|
||||||
|
<HomeCard
|
||||||
|
title="Create"
|
||||||
|
link="/collection/minter"
|
||||||
|
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
|
||||||
|
>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||||
|
</HomeCard>
|
||||||
|
<HomeCard
|
||||||
|
title="My Collections"
|
||||||
|
link="/collections"
|
||||||
|
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
|
||||||
|
>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||||
|
</HomeCard>
|
||||||
|
{/*
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-bold">Smart Contract Dashboard</h2>
|
||||||
|
<p className="text-white/75">
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dicta
|
||||||
|
asperiores quis soluta recusandae sequi adipisci quod tempora modi,
|
||||||
|
debitis beatae tempore accusantium, esse itaque quaerat obcaecati
|
||||||
|
quia totam necessitatibus voluptas!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-bold">Something Cool</h2>
|
||||||
|
<p className="text-white/75">
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Fuga
|
||||||
|
accusantium distinctio dignissimos maxime vero illum explicabo
|
||||||
|
officiis. Pariatur magni, enim qui itaque atque quibusdam debitis
|
||||||
|
iste delectus deserunt dolores quisquam?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-bold">Adding Value</h2>
|
||||||
|
<p className="text-white/75">
|
||||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quasi enim
|
||||||
|
minus voluptates dicta, eum debitis iusto commodi delectus itaque
|
||||||
|
qui, unde adipisci. Esse eveniet dolorem consequatur tempore at
|
||||||
|
voluptates aut?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
*/}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMetadata(HomePage, { center: false })
|
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
BIN
public/assets/android-chrome-144x144.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
public/assets/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
public/assets/android-chrome-256x256.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
public/assets/android-chrome-36x36.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
public/assets/android-chrome-384x384.png
Normal file
After Width: | Height: | Size: 283 KiB |
BIN
public/assets/android-chrome-48x48.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
public/assets/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 459 KiB |
BIN
public/assets/android-chrome-72x72.png
Normal file
After Width: | Height: | Size: 33 KiB |