mirror of
https://github.com/kremalicious/blog.git
synced 2024-11-22 09:56:51 +01:00
fixes
This commit is contained in:
parent
a6f01ed2aa
commit
afb2b16e69
24
src/features/Web3/components/Form/Form.module.css
Normal file
24
src/features/Web3/components/Form/Form.module.css
Normal file
@ -0,0 +1,24 @@
|
||||
.web3 {
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
margin-bottom: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.form {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
min-height: 165px;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
color: var(--text-color-light);
|
||||
font-size: var(--font-size-small);
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
margin-bottom: calc(var(--spacer) / 6);
|
||||
}
|
||||
|
||||
.disclaimer code {
|
||||
background: none;
|
||||
color: var(--text-color);
|
||||
padding-left: 2px;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { test, expect } from 'vitest'
|
||||
import { render, fireEvent, screen } from '@testing-library/react'
|
||||
import Web3Form from '.'
|
||||
import { Web3Form } from './Form'
|
||||
|
||||
test('Web3Donation component', async () => {
|
||||
render(<Web3Form />)
|
48
src/features/Web3/components/Form/Form.tsx
Normal file
48
src/features/Web3/components/Form/Form.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { type ReactElement, useEffect } from 'react'
|
||||
import { useAccount } from 'wagmi'
|
||||
import { InputGroup } from '../Input'
|
||||
import styles from './Form.module.css'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $selectedToken, $isInitSend, $amount } from '@features/Web3/stores'
|
||||
import siteConfig from '@config/blog.config'
|
||||
import { Send } from '../Send'
|
||||
import { RainbowKit } from '../RainbowKit/RainbowKit'
|
||||
|
||||
export function Web3Form(): ReactElement {
|
||||
const { address: account } = useAccount()
|
||||
const selectedToken = useStore($selectedToken)
|
||||
const isInitSend = useStore($isInitSend)
|
||||
const amount = useStore($amount)
|
||||
|
||||
const isDisabled = !account
|
||||
|
||||
// reset amount whenever token changes
|
||||
useEffect(() => {
|
||||
if (!selectedToken) return
|
||||
$amount.set('')
|
||||
}, [selectedToken])
|
||||
|
||||
return (
|
||||
<div className={styles.web3}>
|
||||
{isInitSend ? (
|
||||
<Send />
|
||||
) : (
|
||||
<form
|
||||
className={styles.form}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
if (amount === '' || amount === '0') return
|
||||
$isInitSend.set(true)
|
||||
}}
|
||||
>
|
||||
<RainbowKit />
|
||||
<InputGroup isDisabled={isDisabled} />
|
||||
<div className={styles.disclaimer}>
|
||||
Sends tokens to my account{' '}
|
||||
<code>{siteConfig.author.ether.ens}</code>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,45 +1 @@
|
||||
import { type ReactElement, useEffect } from 'react'
|
||||
import { useAccount } from 'wagmi'
|
||||
import { ConnectButton } from '@rainbow-me/rainbowkit'
|
||||
import { InputGroup } from '../Input'
|
||||
import styles from './index.module.css'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $selectedToken, $isInitSend, $amount } from '@features/Web3/stores'
|
||||
import siteConfig from '@config/blog.config'
|
||||
import { Send } from '../Send'
|
||||
|
||||
export default function Web3Form(): ReactElement {
|
||||
const { address: account } = useAccount()
|
||||
const selectedToken = useStore($selectedToken)
|
||||
const isInitSend = useStore($isInitSend)
|
||||
const amount = useStore($amount)
|
||||
|
||||
const isDisabled = !account
|
||||
|
||||
// reset amount whenever token changes
|
||||
useEffect(() => {
|
||||
if (!selectedToken) return
|
||||
$amount.set('')
|
||||
}, [selectedToken])
|
||||
|
||||
return isInitSend ? (
|
||||
<Send />
|
||||
) : (
|
||||
<form
|
||||
className={styles.web3}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
if (amount === '' || amount === '0') return
|
||||
$isInitSend.set(true)
|
||||
}}
|
||||
>
|
||||
<div className={styles.rainbowkit}>
|
||||
<ConnectButton chainStatus="full" showBalance={false} />
|
||||
</div>
|
||||
<InputGroup isDisabled={isDisabled} />
|
||||
<div className={styles.disclaimer}>
|
||||
Sends tokens to my account <code>{siteConfig.author.ether.ens}</code>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
export * from './Form'
|
||||
|
@ -1,9 +1,8 @@
|
||||
.inputGroup {
|
||||
--height: 60px;
|
||||
--height: 50px;
|
||||
|
||||
margin: auto;
|
||||
position: relative;
|
||||
animation: fadeIn 0.8s ease-out backwards;
|
||||
margin-top: calc(var(--spacer) / 3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -51,10 +50,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.inputInput::-webkit-inner-spin-button {
|
||||
margin-left: -1rem;
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .inputInput {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
@ -85,13 +80,3 @@
|
||||
color: var(--text-color);
|
||||
border-color: var(--text-color-light);
|
||||
} */
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import Input from '@components/Input'
|
||||
import { Conversion } from '../Conversion'
|
||||
import styles from './InputGroup.module.css'
|
||||
import { TokenSelect } from '../TokenSelect'
|
||||
import { $amount, $isInitSend } from '@features/Web3/stores'
|
||||
import { $amount, $isInitSend, $selectedToken } from '@features/Web3/stores'
|
||||
import { useStore } from '@nanostores/react'
|
||||
|
||||
export function InputGroup({
|
||||
@ -12,6 +12,7 @@ export function InputGroup({
|
||||
isDisabled: boolean
|
||||
}): ReactElement {
|
||||
const amount = useStore($amount)
|
||||
const selectedToken = useStore($selectedToken)
|
||||
|
||||
function handleChange(newAmount: string) {
|
||||
$amount.set(newAmount)
|
||||
@ -37,7 +38,7 @@ export function InputGroup({
|
||||
|
||||
<button
|
||||
className={`${styles.submit} btn btn-primary`}
|
||||
disabled={isDisabled || !amount}
|
||||
disabled={isDisabled || !amount || !selectedToken}
|
||||
onClick={() => $isInitSend.set(true)}
|
||||
>
|
||||
Preview
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { formatEther } from 'viem'
|
||||
import { formatEther, formatUnits } from 'viem'
|
||||
import { useAccount, useEnsName, useNetwork } from 'wagmi'
|
||||
import type {
|
||||
SendTransactionArgs,
|
||||
@ -30,7 +30,12 @@ export function Data({
|
||||
(txConfig as SendTransactionArgs)?.value ||
|
||||
(txConfig as WriteContractPreparedArgs)?.request?.args?.[1] ||
|
||||
'0'
|
||||
const displayAmountFromConfig = formatEther(value as bigint)
|
||||
const displayAmountFromConfig =
|
||||
selectedToken?.decimals === 18
|
||||
? formatEther(value as bigint)
|
||||
: selectedToken?.decimals
|
||||
? formatUnits(value as bigint, selectedToken.decimals)
|
||||
: '0'
|
||||
|
||||
return (
|
||||
<table className={styles.table} aria-disabled={isDisabled}>
|
||||
|
@ -1,11 +1,3 @@
|
||||
.web3 {
|
||||
margin: calc(var(--spacer) / 2) auto calc(var(--spacer) / 4) auto;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
min-height: 165px;
|
||||
}
|
||||
|
||||
.rainbowkit button > div {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
@ -52,50 +44,3 @@
|
||||
border: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
color: var(--text-color-light);
|
||||
font-size: var(--font-size-small);
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
margin-bottom: calc(var(--spacer) / 6);
|
||||
}
|
||||
|
||||
.disclaimer code {
|
||||
background: none;
|
||||
color: var(--text-color);
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: var(--font-size-small);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message::after {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
animation: ellipsis steps(4, end) 1s infinite;
|
||||
|
||||
/* ascii code for the ellipsis character */
|
||||
content: '\2026';
|
||||
width: 0;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.success {
|
||||
composes: message;
|
||||
color: green;
|
||||
}
|
||||
|
||||
.success::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes ellipsis {
|
||||
to {
|
||||
width: 0.75rem;
|
||||
}
|
||||
}
|
10
src/features/Web3/components/RainbowKit/RainbowKit.tsx
Normal file
10
src/features/Web3/components/RainbowKit/RainbowKit.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { ConnectButton } from '@rainbow-me/rainbowkit'
|
||||
import styles from './RainbowKit.module.css'
|
||||
|
||||
export function RainbowKit() {
|
||||
return (
|
||||
<div className={styles.rainbowkit}>
|
||||
<ConnectButton chainStatus="full" showBalance={false} />
|
||||
</div>
|
||||
)
|
||||
}
|
0
src/features/Web3/components/RainbowKit/index.tsx
Normal file
0
src/features/Web3/components/RainbowKit/index.tsx
Normal file
@ -1,3 +0,0 @@
|
||||
.send {
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $txHash } from '@features/Web3/stores'
|
||||
import styles from './Send.module.css'
|
||||
import { Success } from '../Success'
|
||||
import { Preview } from '../Preview'
|
||||
|
||||
export function Send() {
|
||||
const txHash = useStore($txHash)
|
||||
|
||||
return <div className={styles.send}>{txHash ? <Success /> : <Preview />}</div>
|
||||
return txHash ? <Success /> : <Preview />
|
||||
}
|
||||
|
@ -21,8 +21,8 @@
|
||||
}
|
||||
|
||||
.TokenLogo {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
margin-right: calc(var(--spacer) / 4);
|
||||
border: 1px solid var(--border-color);
|
||||
@ -34,8 +34,8 @@
|
||||
|
||||
.TokenLogo img {
|
||||
margin: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,7 @@ import { Icon as ChevronsDown } from '@images/components/react/ChevronsDown'
|
||||
import { Icon as ChevronsUp } from '@images/components/react/ChevronsUp'
|
||||
import { useFetchTokens } from '@features/Web3/hooks/useFetchTokens'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $setTokens, $tokens } from '@features/Web3/stores'
|
||||
import {
|
||||
$selectedToken,
|
||||
$setSelectedToken
|
||||
} from '@features/Web3/stores/selectedToken'
|
||||
import { $tokens, $selectedToken } from '@features/Web3/stores'
|
||||
import { Loader } from '@components/Loader'
|
||||
import { useAccount } from 'wagmi'
|
||||
import { useEffect } from 'react'
|
||||
@ -28,19 +24,20 @@ export function TokenSelect() {
|
||||
function handleValueChange(value: `0x${string}`) {
|
||||
const token = tokens?.find((token) => token.address === value)
|
||||
if (!token) return
|
||||
$setSelectedToken(token)
|
||||
$selectedToken.set(token)
|
||||
}
|
||||
|
||||
// reset when no account connected
|
||||
// TODO: reset when no account connected
|
||||
useEffect(() => {
|
||||
if (!address && tokens?.length) {
|
||||
$setTokens(undefined)
|
||||
if (!address && tokens?.length && selectedToken) {
|
||||
$tokens.set(undefined)
|
||||
$selectedToken.set(undefined)
|
||||
}
|
||||
}, [address])
|
||||
|
||||
return tokens && selectedToken ? (
|
||||
return tokens ? (
|
||||
<Select.Root
|
||||
defaultValue={selectedToken?.address}
|
||||
defaultValue={selectedToken?.address || tokens[0].address}
|
||||
onValueChange={(value: `0x${string}`) => handleValueChange(value)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
|
||||
import { WagmiConfig } from 'wagmi'
|
||||
import { wagmiConfig, chains, theme } from '../lib/rainbowkit'
|
||||
import Web3Form from './Form'
|
||||
import { Web3Form } from './Form'
|
||||
|
||||
export function Web3() {
|
||||
return (
|
||||
|
@ -2,8 +2,8 @@ import { useEffect, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import type { GetToken } from '@features/Web3/stores/tokens'
|
||||
import { useNetwork, useAccount } from 'wagmi'
|
||||
import { $setTokens } from '@features/Web3/stores/tokens'
|
||||
import { $setSelectedToken } from '@features/Web3/stores/selectedToken'
|
||||
import { $tokens } from '@features/Web3/stores'
|
||||
import { useStore } from '@nanostores/react'
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
const apiUrl = import.meta.env.PUBLIC_WEB3_API_URL
|
||||
@ -14,6 +14,7 @@ const apiUrl = import.meta.env.PUBLIC_WEB3_API_URL
|
||||
export function useFetchTokens() {
|
||||
const { chain } = useNetwork()
|
||||
const { address } = useAccount()
|
||||
const tokens = useStore($tokens)
|
||||
|
||||
const [url, setUrl] = useState<string | undefined>()
|
||||
|
||||
@ -32,15 +33,11 @@ export function useFetchTokens() {
|
||||
// Sync with $tokens store
|
||||
useEffect(() => {
|
||||
if (!data) return
|
||||
$setTokens(data)
|
||||
}, [data])
|
||||
|
||||
// Set default token data to first item,
|
||||
// which most of time is native token
|
||||
useEffect(() => {
|
||||
if (!data?.[0]?.chainId) return
|
||||
$setSelectedToken(data?.[0])
|
||||
}, [data?.[0]?.chainId])
|
||||
if (data !== tokens) {
|
||||
$tokens.set(data)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
return fetchResults
|
||||
}
|
||||
|
@ -14,7 +14,14 @@ export async function prepare(
|
||||
to: `0x${string}` | null | undefined,
|
||||
chainId: number | undefined
|
||||
) {
|
||||
if (!chainId || !to || !amount || !selectedToken || !selectedToken?.address)
|
||||
if (
|
||||
!chainId ||
|
||||
!to ||
|
||||
!amount ||
|
||||
!selectedToken ||
|
||||
!selectedToken?.address ||
|
||||
!selectedToken?.decimals
|
||||
)
|
||||
return
|
||||
|
||||
const isNative = selectedToken.address === '0x0'
|
||||
@ -24,7 +31,7 @@ export async function prepare(
|
||||
address: selectedToken.address,
|
||||
abi: abiErc20Transfer,
|
||||
functionName: 'transfer',
|
||||
args: [to, parseUnits(amount, selectedToken.decimals || 18)]
|
||||
args: [to, parseUnits(amount, selectedToken.decimals)]
|
||||
}
|
||||
|
||||
const config = isNative
|
||||
|
@ -46,8 +46,6 @@ export function usePrepareSend({
|
||||
'this transaction exceeds the balance of the account.'
|
||||
)
|
||||
) {
|
||||
setError('Insufficient funds')
|
||||
} else {
|
||||
setError(undefined)
|
||||
}
|
||||
} finally {
|
||||
|
@ -27,7 +27,13 @@ export function useSend({
|
||||
$txHash.set(result?.hash)
|
||||
} catch (error: unknown) {
|
||||
console.error((error as Error).message)
|
||||
setError((error as Error).message)
|
||||
|
||||
// only expose useful errors in UI
|
||||
if ((error as Error).message.includes('User rejected the request.')) {
|
||||
setError(undefined)
|
||||
} else {
|
||||
setError((error as Error).message)
|
||||
}
|
||||
setIsError(true)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
|
@ -1,21 +1,23 @@
|
||||
import { action } from 'nanostores'
|
||||
import { persistentAtom } from '@nanostores/persistent'
|
||||
import { atom } from 'nanostores'
|
||||
// import { persistentAtom } from '@nanostores/persistent'
|
||||
import type { GetToken } from './tokens'
|
||||
|
||||
export const $selectedToken = persistentAtom<GetToken | undefined>(
|
||||
'@kremalicious/selectedToken',
|
||||
undefined,
|
||||
{
|
||||
encode: JSON.stringify,
|
||||
decode: JSON.parse
|
||||
}
|
||||
)
|
||||
export const $selectedToken = atom<GetToken | undefined>()
|
||||
|
||||
export const $setSelectedToken = action(
|
||||
$selectedToken,
|
||||
'setSelectedToken',
|
||||
(store, token: GetToken) => {
|
||||
store.set(token)
|
||||
return store.get()
|
||||
}
|
||||
)
|
||||
// export const $selectedToken = persistentAtom<GetToken | undefined>(
|
||||
// '@kremalicious/selectedToken',
|
||||
// undefined,
|
||||
// {
|
||||
// encode: JSON.stringify,
|
||||
// decode: JSON.parse
|
||||
// }
|
||||
// )
|
||||
|
||||
// export const $selectedToken.set = action(
|
||||
// $selectedToken,
|
||||
// 'setSelectedToken',
|
||||
// (store, token: GetToken) => {
|
||||
// store.set(token)
|
||||
// return store.get()
|
||||
// }
|
||||
// )
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { action, atom } from 'nanostores'
|
||||
import { atom } from 'nanostores'
|
||||
import type { GetToken } from './types'
|
||||
|
||||
export const $tokens = atom<GetToken[] | undefined>()
|
||||
|
||||
export const $setTokens = action(
|
||||
$tokens,
|
||||
'setTokens',
|
||||
(store, tokens: GetToken[] | undefined) => {
|
||||
store.set(tokens)
|
||||
return store.get()
|
||||
}
|
||||
)
|
||||
// export const $setTokens = action(
|
||||
// $tokens,
|
||||
// 'setTokens',
|
||||
// (store, tokens: GetToken[] | undefined) => {
|
||||
// store.set(tokens)
|
||||
// return store.get()
|
||||
// }
|
||||
// )
|
||||
|
Loading…
Reference in New Issue
Block a user