StargazeTools init

This commit is contained in:
Serkan Reis 2022-07-13 16:56:36 +03:00
commit 4dde6db215
142 changed files with 11833 additions and 0 deletions

14
.env.example Normal file
View 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
View File

@ -0,0 +1 @@
* @orkunkl @findolor @kaymakf

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.

View 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
View 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
View File

@ -0,0 +1,10 @@
*.log
*.tsbuildinfo
.DS_Store
.env*
.vercel
.next/
node_modules/
out/
!.env.example

4
.husky/pre-commit Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
.next/**
dist/**
node_modules/**
out/**

28
.vscode/settings.json vendored Normal file
View 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
View File

@ -0,0 +1,53 @@
<!-- markdownlint-disable MD033 MD034 MD036 MD041 -->
![stargaze-tools](./public/social.png)
# 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
View 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
View 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?:\/\//, '')
}

View 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>
)
}

View 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>
)
}

View 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
View 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>
)
}

View 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}</>
}

View 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>
)
}

View 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,
}}
/>
)
}

View 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" />
</>
)
}

View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
import { StyledInput } from './forms/StyledInput'
/** @deprecated - replace with {@link StyledInput} */
export const Input = StyledInput

View 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
View 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>
)
}

View 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
View 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
View 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>
)
}

View 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
View 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
View 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
View 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
View 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>
)
}

View 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
View 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>
)
}

View 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>
)
}

View 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
View 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
View 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
View 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>
)}
</>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
})

View 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>
)
}

View 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>
)
})

View 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) }
}

View 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>
)
}

View 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 }
}

View 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>
)
}

View 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 }
}

View 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>
)
}

View 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,
}
}

View 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" />
},
//
)

View 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>
)
},
//
)

View 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>
)
}

View 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}
/>
)
},
//
)

View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
export * from './app'
export * from './keplr'
export * from './network'

81
config/keplr.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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 }
}

View File

@ -0,0 +1,2 @@
export * from './contract'
export * from './useContract'

View 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
View 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
View 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
View 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
View 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
View 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
}

View 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

View 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"
}

View 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

View 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
View File

@ -0,0 +1,4 @@
// pages/404.js
export default function Custom404() {
return <h1>Page Not Found</h1>
}

30
pages/_app.tsx Normal file
View 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
View 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
View 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 })

View 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 })

View File

@ -0,0 +1 @@
export { default } from './instantiate'

View 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 })

View 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 })

View File

@ -0,0 +1,5 @@
const Minting = () => {
return <div>dsadsa</div>
}
export default Minting

117
pages/contracts/upload.tsx Normal file
View 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
View 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
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Some files were not shown because too many files have changed in this diff Show More