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()]
 )