From ea9ed0382e98ec965de7974ae1e4f5a4139c050f Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann <m@kretschmann.io> Date: Sat, 28 Oct 2023 12:55:30 +0100 Subject: [PATCH] new token select ui --- package-lock.json | 9 ++++ package.json | 1 + .../Sponsor/Web3Donation/api/getBalance.ts | 9 ---- .../Sponsor/Web3Donation/api/getTokens.ts | 22 +++++++++ .../components/Input/InputGroup.module.css | 18 ++++---- .../components/Input/InputGroup.tsx | 8 ++-- .../components/Tokens/SelectItem.tsx | 33 -------------- .../Tokens/{SelectItem.css => Token.css} | 25 +++++++++-- .../Web3Donation/components/Tokens/Token.tsx | 45 +++++++++++++++++++ .../Tokens/{Select.css => TokenSelect.css} | 25 +++++++---- .../Tokens/{Select.tsx => TokenSelect.tsx} | 34 +++++++------- .../Web3Donation/components/Tokens/index.tsx | 2 +- .../Sponsor/Web3Donation/hooks/useTokens.tsx | 15 +++---- src/components/Sponsor/Web3Donation/index.tsx | 15 +++++-- .../Sponsor/Web3Donation/lib/rainbowkit.ts | 4 +- 15 files changed, 167 insertions(+), 98 deletions(-) delete mode 100644 src/components/Sponsor/Web3Donation/api/getBalance.ts create mode 100644 src/components/Sponsor/Web3Donation/api/getTokens.ts delete mode 100644 src/components/Sponsor/Web3Donation/components/Tokens/SelectItem.tsx rename src/components/Sponsor/Web3Donation/components/Tokens/{SelectItem.css => Token.css} (67%) create mode 100644 src/components/Sponsor/Web3Donation/components/Tokens/Token.tsx rename src/components/Sponsor/Web3Donation/components/Tokens/{Select.css => TokenSelect.css} (75%) rename src/components/Sponsor/Web3Donation/components/Tokens/{Select.tsx => TokenSelect.tsx} (71%) diff --git a/package-lock.json b/package-lock.json index b29859a2..731bdcc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2cb6ca7a..d19416ff 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Sponsor/Web3Donation/api/getBalance.ts b/src/components/Sponsor/Web3Donation/api/getBalance.ts deleted file mode 100644 index 5e608b97..00000000 --- a/src/components/Sponsor/Web3Donation/api/getBalance.ts +++ /dev/null @@ -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 -} diff --git a/src/components/Sponsor/Web3Donation/api/getTokens.ts b/src/components/Sponsor/Web3Donation/api/getTokens.ts new file mode 100644 index 00000000..b34ec85b --- /dev/null +++ b/src/components/Sponsor/Web3Donation/api/getTokens.ts @@ -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 +} diff --git a/src/components/Sponsor/Web3Donation/components/Input/InputGroup.module.css b/src/components/Sponsor/Web3Donation/components/Input/InputGroup.module.css index 5fa03a57..a046c9d7 100644 --- a/src/components/Sponsor/Web3Donation/components/Input/InputGroup.module.css +++ b/src/components/Sponsor/Web3Donation/components/Input/InputGroup.module.css @@ -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) { diff --git a/src/components/Sponsor/Web3Donation/components/Input/InputGroup.tsx b/src/components/Sponsor/Web3Donation/components/Input/InputGroup.tsx index 0dc0e909..2ea3bac1 100644 --- a/src/components/Sponsor/Web3Donation/components/Input/InputGroup.tsx +++ b/src/components/Sponsor/Web3Donation/components/Input/InputGroup.tsx @@ -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" diff --git a/src/components/Sponsor/Web3Donation/components/Tokens/SelectItem.tsx b/src/components/Sponsor/Web3Donation/components/Tokens/SelectItem.tsx deleted file mode 100644 index 4828e099..00000000 --- a/src/components/Sponsor/Web3Donation/components/Tokens/SelectItem.tsx +++ /dev/null @@ -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> - ) - } -) diff --git a/src/components/Sponsor/Web3Donation/components/Tokens/SelectItem.css b/src/components/Sponsor/Web3Donation/components/Tokens/Token.css similarity index 67% rename from src/components/Sponsor/Web3Donation/components/Tokens/SelectItem.css rename to src/components/Sponsor/Web3Donation/components/Tokens/Token.css index 319cc838..8122d35b 100644 --- a/src/components/Sponsor/Web3Donation/components/Tokens/SelectItem.css +++ b/src/components/Sponsor/Web3Donation/components/Tokens/Token.css @@ -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 { diff --git a/src/components/Sponsor/Web3Donation/components/Tokens/Token.tsx b/src/components/Sponsor/Web3Donation/components/Tokens/Token.tsx new file mode 100644 index 00000000..154c13a2 --- /dev/null +++ b/src/components/Sponsor/Web3Donation/components/Tokens/Token.tsx @@ -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 + } +) diff --git a/src/components/Sponsor/Web3Donation/components/Tokens/Select.css b/src/components/Sponsor/Web3Donation/components/Tokens/TokenSelect.css similarity index 75% rename from src/components/Sponsor/Web3Donation/components/Tokens/Select.css rename to src/components/Sponsor/Web3Donation/components/Tokens/TokenSelect.css index 78487447..90d512ba 100644 --- a/src/components/Sponsor/Web3Donation/components/Tokens/Select.css +++ b/src/components/Sponsor/Web3Donation/components/Tokens/TokenSelect.css @@ -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); } diff --git a/src/components/Sponsor/Web3Donation/components/Tokens/Select.tsx b/src/components/Sponsor/Web3Donation/components/Tokens/TokenSelect.tsx similarity index 71% rename from src/components/Sponsor/Web3Donation/components/Tokens/Select.tsx rename to src/components/Sponsor/Web3Donation/components/Tokens/TokenSelect.tsx index 39702560..a0de1a60 100644 --- a/src/components/Sponsor/Web3Donation/components/Tokens/Select.tsx +++ b/src/components/Sponsor/Web3Donation/components/Tokens/TokenSelect.tsx @@ -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 } diff --git a/src/components/Sponsor/Web3Donation/components/Tokens/index.tsx b/src/components/Sponsor/Web3Donation/components/Tokens/index.tsx index 3e383f07..979668a7 100644 --- a/src/components/Sponsor/Web3Donation/components/Tokens/index.tsx +++ b/src/components/Sponsor/Web3Donation/components/Tokens/index.tsx @@ -1 +1 @@ -export * from './Select' +export * from './TokenSelect' diff --git a/src/components/Sponsor/Web3Donation/hooks/useTokens.tsx b/src/components/Sponsor/Web3Donation/hooks/useTokens.tsx index 22d54130..ee4fc10b 100644 --- a/src/components/Sponsor/Web3Donation/hooks/useTokens.tsx +++ b/src/components/Sponsor/Web3Donation/hooks/useTokens.tsx @@ -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 } diff --git a/src/components/Sponsor/Web3Donation/index.tsx b/src/components/Sponsor/Web3Donation/index.tsx index d3188f46..ad2960de 100644 --- a/src/components/Sponsor/Web3Donation/index.tsx +++ b/src/components/Sponsor/Web3Donation/index.tsx @@ -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} /> )} diff --git a/src/components/Sponsor/Web3Donation/lib/rainbowkit.ts b/src/components/Sponsor/Web3Donation/lib/rainbowkit.ts index 9a4b2be1..1437ab59 100644 --- a/src/components/Sponsor/Web3Donation/lib/rainbowkit.ts +++ b/src/components/Sponsor/Web3Donation/lib/rainbowkit.ts @@ -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()] )