1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-11-22 01:46:51 +01:00

new token selection, reorg

This commit is contained in:
Matthias Kretschmann 2023-10-26 20:32:57 +01:00
parent d809f982f0
commit 0ca6558def
Signed by: m
GPG Key ID: 606EEEF3C479A91F
23 changed files with 327 additions and 113 deletions

View File

@ -2,7 +2,7 @@ import Web3Donation from './Web3Donation'
import config from '@config/blog.config'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { WagmiConfig } from 'wagmi'
import { wagmiConfig, chains, theme } from '@lib/rainbowkit'
import { wagmiConfig, chains, theme } from './Web3Donation/lib/rainbowkit'
import type { ReactElement } from 'react'
export default function Web3(): ReactElement {

View File

@ -1,41 +0,0 @@
import { type ReactElement } from 'react'
import Input from '@components/Input'
import Conversion from './Conversion'
import styles from './InputGroup.module.css'
export default function InputGroup({
amount,
isDisabled,
symbol,
setAmount
}: {
amount: string
isDisabled: boolean
symbol: string
setAmount(amount: string): void
}): ReactElement {
return (
<>
<div className={styles.inputGroup}>
<div className={styles.input}>
<Input
type="text"
inputMode="decimal"
pattern="[0-9.]*"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className={styles.inputInput}
disabled={isDisabled}
/>
<div className={styles.currency}>
<span>{symbol}</span>
</div>
</div>
<button className="btn btn-primary" disabled={isDisabled}>
Make it rain
</button>
</div>
<Conversion amount={amount} symbol={symbol} />
</>
)
}

View File

@ -0,0 +1,9 @@
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,18 @@
export async function getFiat({
amount,
tokenId = 'ethereum'
}: {
amount: number
tokenId?: string
}): Promise<{ [key: string]: string }> {
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${tokenId}&vs_currencies=eur%2Cusd`
const response = await fetch(url)
const json = await response.json()
if (!json) console.error(response.statusText)
const { usd, eur } = json[tokenId]
const dollar = (amount * usd).toFixed(2)
const euro = (amount * eur).toFixed(2)
return { dollar, euro }
}

View File

@ -1,6 +1,6 @@
import { render } from '@testing-library/react'
import { describe, it } from 'vitest'
import Conversion from './Conversion'
import { Conversion } from './Conversion'
describe('Conversion', () => {
it('renders without crashing', async () => {

View File

@ -1,26 +1,8 @@
import { type ReactElement, useEffect, useState } from 'react'
import styles from './Conversion.module.css'
import { getFiat } from '../../api/getFiat'
export async function getFiat({
amount,
tokenId = 'ethereum'
}: {
amount: number
tokenId?: string
}): Promise<{ [key: string]: string }> {
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${tokenId}&vs_currencies=eur%2Cusd`
const response = await fetch(url)
const json = await response.json()
if (!json) console.error(response.statusText)
const { usd, eur } = json[tokenId]
const dollar = (amount * usd).toFixed(2)
const euro = (amount * eur).toFixed(2)
return { dollar, euro }
}
export default function Conversion({
export function Conversion({
amount,
symbol
}: {

View File

@ -0,0 +1 @@
export * from './Conversion'

View File

@ -3,48 +3,28 @@
position: relative;
animation: fadeIn 0.8s ease-out backwards;
margin-top: var(--spacer);
display: flex;
}
@media (min-width: 40rem) {
/* @media (min-width: 40rem) {
.inputGroup {
display: flex;
flex-wrap: wrap;
}
}
} */
.inputGroup button {
width: 100%;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-color: var(--border-color);
}
/* .currency {
} */
@media (min-width: 40rem) {
.inputGroup button {
width: 40%;
border-top-right-radius: var(--border-radius);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 0;
}
}
.input {
position: relative;
}
@media (min-width: 40rem) {
.input {
width: 60%;
}
:global([data-theme='dark']) .currency {
border-right-color: #000;
}
.inputInput {
text-align: center;
border: 1px solid var(--border-color);
font-size: var(--font-size-large);
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 3)
calc(var(--spacer) / 3) calc(var(--spacer) * 1.7);
padding: calc(var(--spacer) / 4);
border-bottom: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
@ -68,27 +48,25 @@
border-color: var(--border-color);
}
.currency {
position: absolute;
top: 1px;
bottom: 1px;
left: 1px;
font-size: var(--font-size-small);
padding: calc(var(--spacer) / 3);
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);
display: flex;
align-items: center;
.submit {
width: 100%;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-color: var(--border-color);
}
:global([data-theme='dark']) .currency {
border-right-color: #000;
@media (min-width: 40rem) {
.submit {
width: fit-content;
border-top-right-radius: var(--border-radius);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 0;
}
}
.message {
composes: message from './index.module.css';
composes: message from '../../index.module.css';
}
@keyframes fadeIn {

View File

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import InputGroup from './InputGroup'
import { InputGroup } from '.'
const setAmount = vi.fn()

View File

@ -0,0 +1,43 @@
import { type ReactElement } from 'react'
import Input from '@components/Input'
import { Conversion } from '../Conversion'
import styles from './InputGroup.module.css'
import { TokenSelect } from '../Tokens'
export function InputGroup({
amount,
isDisabled,
symbol,
setAmount
}: {
amount: string
isDisabled: boolean
symbol: string
setAmount(amount: string): void
}): ReactElement {
return (
<>
<div className={styles.inputGroup}>
<div className={styles.currency}>
<TokenSelect />
</div>
<Input
type="text"
inputMode="decimal"
pattern="[0-9.]*"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className={styles.inputInput}
disabled={isDisabled}
/>
<button
className={`${styles.submit} btn btn-primary`}
disabled={isDisabled}
>
Make it rain
</button>
</div>
<Conversion amount={amount} symbol={symbol} />
</>
)
}

View File

@ -0,0 +1 @@
export * from './InputGroup'

View File

@ -0,0 +1,67 @@
/* reset */
button {
all: unset;
}
.SelectTrigger {
display: inline-flex;
align-items: center;
justify-content: center;
width: 70px;
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;
}
.SelectTrigger:focus {
box-shadow: 0 0 0 2px blue;
}
.SelectTrigger[data-disabled] {
opacity: 0.5;
pointer-events: none;
}
.SelectContent {
overflow: hidden;
background-color: var(--body-background-color);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
}
.SelectViewport {
padding: 0;
}
.SelectLabel {
padding: 0 25px;
font-size: var(--font-size-small);
color: var(--text-color-light);
text-transform: capitalize;
border-bottom: 1px solid var(--border-color);
}
.SelectSeparator {
height: 1px;
background-color: var(--border-color);
margin: 5px;
}
.SelectScrollButton {
display: flex;
align-items: center;
justify-content: center;
height: 25px;
background-color: var(--body-background-color);
color: var(--text-color);
cursor: default;
}

View File

@ -0,0 +1,70 @@
import * as Select from '@radix-ui/react-select'
import './Select.css'
import { SelectItem } from './SelectItem'
import { ChevronDown, ChevronsDown, ChevronsUp } from '@images/components/react'
import { useEffect, useState } from 'react'
import { getBalance } from '../../api/getBalance'
import { useAccount, useNetwork } from 'wagmi'
export function TokenSelect() {
const { address } = useAccount()
const { chain } = useNetwork()
const [tokens, setTokens] = useState()
useEffect(() => {
if (!address || !chain) return
async function getTokens() {
try {
const response = await getBalance(address)
const tokens = response.filter(
(token: any) => parseInt(token.chainId) === chain?.id
)
setTokens(tokens)
} catch (error) {
console.error((error as Error).message)
}
}
getTokens()
}, [address, chain])
return (
<Select.Root disabled={!address}>
<Select.Trigger className="SelectTrigger" aria-label="Token">
<Select.Value placeholder="…" />
<Select.Icon>
<ChevronDown />
</Select.Icon>
</Select.Trigger>
<Select.Portal style={{ zIndex: 10 }}>
<Select.Content className="SelectContent">
<Select.ScrollUpButton className="SelectScrollButton">
<ChevronsUp />
</Select.ScrollUpButton>
<Select.Viewport className="SelectViewport">
<Select.Group>
<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>
))}
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton className="SelectScrollButton">
<ChevronsDown />
</Select.ScrollDownButton>
</Select.Content>
</Select.Portal>
</Select.Root>
)
}

View File

@ -0,0 +1,52 @@
.SelectItem {
font-size: var(--font-size-small);
line-height: 1;
color: var(--text-color);
display: flex;
align-items: center;
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 4)
calc(var(--spacer) / 4) 25px;
position: relative;
user-select: none;
}
.SelectItem[data-disabled] {
opacity: 0.5;
pointer-events: none;
}
.SelectItem[data-highlighted] {
outline: none;
background-color: var(--text-color);
color: var(--body-background-color);
}
.SelectItem,
.Token {
display: flex;
align-items: center;
position: relative;
}
.Token {
min-width: 200px;
}
.Token img {
width: 32px;
height: 32px;
margin: 0;
margin-right: calc(var(--spacer) / 4);
border-radius: 50%;
border: 1px solid var(--border-color);
background: var(--border-color);
}
.SelectItemIndicator {
position: absolute;
left: 0;
width: 25px;
display: inline-flex;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,33 @@
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

@ -0,0 +1 @@
export * from './Select'

View File

@ -9,8 +9,8 @@ import {
} from 'wagmi'
import { ConnectButton } from '@rainbow-me/rainbowkit'
import Alert, { getTransactionMessage } from './Alert'
import InputGroup from './InputGroup'
import Alert, { getTransactionMessage } from './components/Alert/Alert'
import { InputGroup } from './components/Input'
import styles from './index.module.css'
export default function Web3Donation({

View File

@ -1,6 +1,6 @@
import { type Theme, getDefaultWallets } from '@rainbow-me/rainbowkit'
import { configureChains, createConfig } from 'wagmi'
import { mainnet, polygon } from 'wagmi/chains'
import { mainnet, polygon, base, bsc } 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],
[mainnet, polygon, base, bsc],
[infuraProvider({ apiKey: PUBLIC_INFURA_ID }), publicProvider()]
)

View File

@ -33,7 +33,7 @@ const coins = Object.entries(config.author).filter(
}
</style>
<LayoutBase title="Thanks" pageTitle="Say Thanks">
<LayoutBase title="Say Thanks">
<BackButton />
<div class="content">