1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-12-23 01:30:01 +01:00

new token select ui

This commit is contained in:
Matthias Kretschmann 2023-10-28 12:55:30 +01:00
parent 2f6e4f0b3c
commit ea9ed0382e
Signed by: m
GPG Key ID: 606EEEF3C479A91F
15 changed files with 167 additions and 98 deletions

9
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@astrojs/react": "^3.0.4",
"@astrojs/rss": "^3.0.0",
"@astrojs/sitemap": "^3.0.2",
"@coingecko/cryptoformat": "^0.6.0",
"@nanostores/query": "^0.2.4",
"@nanostores/react": "^0.7.1",
"@radix-ui/react-popover": "^1.0.7",
@ -851,6 +852,14 @@
"node": ">=6"
}
},
"node_modules/@coingecko/cryptoformat": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@coingecko/cryptoformat/-/cryptoformat-0.6.0.tgz",
"integrity": "sha512-XWi9gsUDrJh27NzMRJo1Ll2OzG6A9PCf+74edgPUuyyZ3VMmcVCaQppS4hK4D/ZKIPDGtgaR7AIubcfnoPwbrg==",
"engines": {
"node": ">=10"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",

View File

@ -46,6 +46,7 @@
"@astrojs/react": "^3.0.4",
"@astrojs/rss": "^3.0.0",
"@astrojs/sitemap": "^3.0.2",
"@coingecko/cryptoformat": "^0.6.0",
"@nanostores/query": "^0.2.4",
"@nanostores/react": "^0.7.1",
"@radix-ui/react-popover": "^1.0.7",

View File

@ -1,9 +0,0 @@
export async function getBalance(address: `0x${string}` | undefined) {
const url = `http://localhost:3000/api/balance?address=${address}`
// const url = `https://web3-api-kremalicious.vercel.app/api/balance?address=${address}`
const response = await fetch(url)
const json = await response.json()
if (!json) console.error(response.statusText)
return json
}

View File

@ -0,0 +1,22 @@
export type GetToken = {
address: `0x${string}`
balance: number | undefined
chainId: number
name: string | null
symbol: string | null
decimals: number | null
logo: string | null
}
export async function getTokens(
address: `0x${string}`,
chainId: number
): Promise<GetToken[]> {
// const url = `http://localhost:3000/api/balance?address=${address}&chainId=${chainId}`
const url = `https://web3-api-kremalicious.vercel.app/api/balance?address=${address}&chainId=${chainId}`
const response = await fetch(url)
const json: GetToken[] = await response.json()
if (!json) console.error(response.statusText)
return json
}

View File

@ -13,11 +13,13 @@
}
} */
/* .currency {
} */
:global([data-theme='dark']) .currency {
border-right-color: #000;
.token {
width: 80px;
background: var(--box-background-color);
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
border: 1px solid var(--border-color);
margin-right: -1px;
}
.inputInput {
@ -32,9 +34,7 @@
@media (min-width: 40rem) {
.inputInput {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: var(--border-radius);
border-radius: 0;
border-bottom: 1px solid var(--border-color);
border-right: 0;
}
@ -52,7 +52,7 @@
width: 100%;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-color: var(--border-color);
border-color: var(--link-color);
}
@media (min-width: 40rem) {

View File

@ -8,18 +8,20 @@ export function InputGroup({
amount,
isDisabled,
symbol,
setAmount
setAmount,
setToken
}: {
amount: string
isDisabled: boolean
symbol: string
setAmount(amount: string): void
setToken(token: string): void
}): ReactElement {
return (
<>
<div className={styles.inputGroup}>
<div className={styles.currency}>
<TokenSelect />
<div className={styles.token}>
<TokenSelect setToken={setToken} />
</div>
<Input
type="text"

View File

@ -1,33 +0,0 @@
import { forwardRef, type HTMLAttributes } from 'react'
import * as Select from '@radix-ui/react-select'
import classnames from 'classnames'
import './SelectItem.css'
import { Check } from '@images/components/react'
interface SelectItemProps extends HTMLAttributes<HTMLDivElement> {
value: string
icon: string
}
export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
({ children, className, value, icon, ...props }, forwardedRef) => {
return (
<Select.Item
className={classnames('SelectItem', className)}
{...props}
value={value}
ref={forwardedRef}
>
<div className="Token">
<Select.ItemText>
<img src={icon} width="32" height="32" />
</Select.ItemText>
<span>{children}</span>
</div>
<Select.ItemIndicator className="SelectItemIndicator">
<Check />
</Select.ItemIndicator>
</Select.Item>
)
}
)

View File

@ -4,8 +4,8 @@
color: var(--text-color);
display: flex;
align-items: center;
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 4)
calc(var(--spacer) / 4) 25px;
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2)
calc(var(--spacer) / 3) 25px;
position: relative;
user-select: none;
}
@ -18,6 +18,10 @@
.SelectItem[data-highlighted] {
outline: none;
background-color: var(--text-color);
}
.SelectItem[data-highlighted],
.SelectItem[data-highlighted] * {
color: var(--body-background-color);
}
@ -39,7 +43,22 @@
margin-right: calc(var(--spacer) / 4);
border-radius: 50%;
border: 1px solid var(--border-color);
background: var(--border-color);
background: var(--brand-light);
}
.TokenName,
.TokenBalance {
margin: 0;
}
.TokenName {
font-size: var(--font-size-base);
transition: none;
}
.TokenBalance {
font-size: var(--font-size-small);
font-variant: tabular-nums;
}
.SelectItemIndicator {

View File

@ -0,0 +1,45 @@
import { forwardRef, type HTMLAttributes } from 'react'
import * as Select from '@radix-ui/react-select'
import { formatCurrency } from '@coingecko/cryptoformat'
import './Token.css'
import { Check } from '@images/components/react'
import type { GetToken } from '../../api/getTokens'
interface SelectItemProps extends HTMLAttributes<HTMLDivElement> {
token: GetToken
}
export const Token = forwardRef<HTMLDivElement, SelectItemProps>(
({ className, token, ...props }, forwardedRef) => {
const balance =
token.balance && token.symbol
? formatCurrency(token.balance, token.symbol, 'en', false, {
decimalPlaces: 3,
significantFigures: 3
})
: 0
return balance && parseInt(balance) !== 0 ? (
<Select.Item
className={`${className} SelectItem`}
{...props}
value={token.address}
title={token.address}
ref={forwardedRef}
>
<div className="Token">
<Select.ItemText>
<img src={token.logo || ''} width="32" height="32" />
</Select.ItemText>
<div>
<h3 className="TokenName">{token.name}</h3>
<p className="TokenBalance">{balance}</p>
</div>
</div>
<Select.ItemIndicator className="SelectItemIndicator">
<Check />
</Select.ItemIndicator>
</Select.Item>
) : null
}
)

View File

@ -7,30 +7,37 @@ button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 70px;
width: 80px;
height: 100%;
font-size: var(--font-size-small);
line-height: 1;
padding: 0 calc(var(--spacer) / 4);
background: var(--box-background-color);
border-right: 1px solid var(--text-color-dimmed);
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
.SelectTrigger:hover {
background-color: whitesmoke;
background-color: var(--text-color);
color: var(--body-background-color);
}
.SelectTrigger:focus {
/* .SelectTrigger:focus {
box-shadow: 0 0 0 2px blue;
}
} */
.SelectTrigger[data-disabled] {
opacity: 0.5;
pointer-events: none;
}
.SelectTrigger img {
width: 32px;
height: 32px;
margin: 0;
border-radius: 50%;
border: 1px solid var(--border-color);
background: var(--brand-light);
}
.SelectContent {
overflow: hidden;
background-color: var(--body-background-color);
@ -43,9 +50,9 @@ button {
}
.SelectLabel {
padding: 0 25px;
padding: calc(var(--spacer) / 4);
font-size: var(--font-size-small);
color: var(--text-color-light);
color: var(--text-color);
text-transform: capitalize;
border-bottom: 1px solid var(--border-color);
}

View File

@ -1,14 +1,25 @@
import * as Select from '@radix-ui/react-select'
import './Select.css'
import { SelectItem } from './SelectItem'
import './TokenSelect.css'
import { Token } from './Token'
import { ChevronDown, ChevronsDown, ChevronsUp } from '@images/components/react'
import { useTokens } from '../../hooks/useTokens'
export function TokenSelect() {
export function TokenSelect({
setToken
}: {
setToken: (token: string) => void
}) {
const { data: tokens } = useTokens()
return (
<Select.Root disabled={!tokens}>
const items = tokens?.map((token) => (
<Token key={token.address} token={token} />
))
return tokens ? (
<Select.Root
defaultValue={tokens[0].address}
onValueChange={(value) => setToken(value)}
>
<Select.Trigger className="SelectTrigger" aria-label="Token">
<Select.Value placeholder="…" />
<Select.Icon>
@ -26,16 +37,7 @@ export function TokenSelect() {
<Select.Label className="SelectLabel">
In Your Wallet
</Select.Label>
{tokens?.map((token: any) => (
<SelectItem
key={token.token_address}
value={token.token_address}
icon={token.logo}
>
{token.name}
</SelectItem>
))}
{items}
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton className="SelectScrollButton">
@ -44,5 +46,5 @@ export function TokenSelect() {
</Select.Content>
</Select.Portal>
</Select.Root>
)
) : null
}

View File

@ -1 +1 @@
export * from './Select'
export * from './TokenSelect'

View File

@ -1,26 +1,23 @@
import { useState, useEffect } from 'react'
import { useAccount, useNetwork } from 'wagmi'
import { getBalance } from '../api/getBalance'
import { getTokens, type GetTokens } from '../api/getTokens'
export function useTokens() {
const { address } = useAccount()
const { chain } = useNetwork()
const [data, setData] = useState()
const [data, setData] = useState<GetTokens[]>()
const [isLoading, setIsLoading] = useState<boolean>()
const [isError, setIsError] = useState<boolean>()
useEffect(() => {
if (!address || !chain) return
async function init() {
if (!address || !chain) return
async function getTokens() {
setIsLoading(true)
try {
const response = await getBalance(address)
const tokens = response.filter(
(token: any) => parseInt(token.chainId) === chain?.id
)
const tokens = await getTokens(address, chain.id)
setData(tokens)
setIsLoading(false)
} catch (error) {
@ -29,7 +26,7 @@ export function useTokens() {
console.error((error as Error).message)
}
}
getTokens()
init()
}, [address, chain])
return { data, isLoading, isError }

View File

@ -23,17 +23,23 @@ export default function Web3Donation({
const [amount, setAmount] = useState('0.005')
const [debouncedAmount] = useDebounce(amount, 500)
const [token, setToken] = useState<string>()
const [message, setMessage] = useState<{ status: string; text: string }>()
const [transactionHash, setTransactionHash] = useState<string>()
// dummy
if (token) {
console.log(token)
}
const { config } = usePrepareSendTransaction({
chainId: chain?.id,
to: address,
value: debouncedAmount ? parseEther(debouncedAmount) : undefined
value: parseEther(debouncedAmount)
})
const { sendTransactionAsync, isError, isSuccess } =
useSendTransaction(config)
const [message, setMessage] = useState<{ status: string; text: string }>()
const [transactionHash, setTransactionHash] = useState<string>()
async function handleSendTransaction() {
setMessage({
status: 'loading',
@ -83,6 +89,7 @@ export default function Web3Donation({
amount={amount}
symbol={chain?.nativeCurrency?.symbol || 'ETH'}
setAmount={setAmount}
setToken={setToken}
isDisabled={isDisabled}
/>
)}

View File

@ -1,6 +1,6 @@
import { type Theme, getDefaultWallets } from '@rainbow-me/rainbowkit'
import { configureChains, createConfig } from 'wagmi'
import { mainnet, polygon, base, bsc } from 'wagmi/chains'
import { mainnet, polygon, base, optimism } from 'wagmi/chains'
import { infuraProvider } from 'wagmi/providers/infura'
import { publicProvider } from 'wagmi/providers/public'
@ -13,7 +13,7 @@ if (isProduction && (!PUBLIC_INFURA_ID || !PUBLIC_WALLETCONNECT_ID)) {
}
export const { chains, publicClient } = configureChains(
[mainnet, polygon, base, bsc],
[mainnet, polygon, base, optimism],
[infuraProvider({ apiKey: PUBLIC_INFURA_ID }), publicProvider()]
)