1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-11-22 18:00:06 +01:00
This commit is contained in:
Matthias Kretschmann 2023-11-03 20:56:08 +00:00
parent bad987d68d
commit a6f01ed2aa
Signed by: m
GPG Key ID: 606EEEF3C479A91F
20 changed files with 188 additions and 155 deletions

12
package-lock.json generated
View File

@ -35,7 +35,6 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"swr": "^2.2.4", "swr": "^2.2.4",
"use-debounce": "^9.0.4",
"viem": "^1.18.2", "viem": "^1.18.2",
"wagmi": "^1.4.5" "wagmi": "^1.4.5"
}, },
@ -18987,17 +18986,6 @@
} }
} }
}, },
"node_modules/use-debounce": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz",
"integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==",
"engines": {
"node": ">= 10.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/use-sidecar": { "node_modules/use-sidecar": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",

View File

@ -74,7 +74,6 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"swr": "^2.2.4", "swr": "^2.2.4",
"use-debounce": "^9.0.4",
"viem": "^1.18.2", "viem": "^1.18.2",
"wagmi": "^1.4.5" "wagmi": "^1.4.5"
}, },

View File

@ -2,9 +2,11 @@ import { useEffect, type ReactElement, useState } from 'react'
import styles from './Conversion.module.css' import styles from './Conversion.module.css'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $selectedToken } from '@features/Web3/stores/selectedToken' import { $selectedToken } from '@features/Web3/stores/selectedToken'
import { $amount } from '@features/Web3/stores'
export function Conversion({ amount }: { amount: string }): ReactElement { export function Conversion(): ReactElement {
const selectedToken = useStore($selectedToken) const selectedToken = useStore($selectedToken)
const amount = useStore($amount)
const [dollar, setDollar] = useState('0.00') const [dollar, setDollar] = useState('0.00')
const [euro, setEuro] = useState('0.00') const [euro, setEuro] = useState('0.00')

View File

@ -1,11 +1,10 @@
import { type ReactElement, useState, useEffect } from 'react' import { type ReactElement, useEffect } from 'react'
import { useDebounce } from 'use-debounce'
import { useAccount } from 'wagmi' import { useAccount } from 'wagmi'
import { ConnectButton } from '@rainbow-me/rainbowkit' import { ConnectButton } from '@rainbow-me/rainbowkit'
import { InputGroup } from '../Input' import { InputGroup } from '../Input'
import styles from './index.module.css' import styles from './index.module.css'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $selectedToken, $isInitSend } from '@features/Web3/stores' import { $selectedToken, $isInitSend, $amount } from '@features/Web3/stores'
import siteConfig from '@config/blog.config' import siteConfig from '@config/blog.config'
import { Send } from '../Send' import { Send } from '../Send'
@ -13,42 +12,34 @@ export default function Web3Form(): ReactElement {
const { address: account } = useAccount() const { address: account } = useAccount()
const selectedToken = useStore($selectedToken) const selectedToken = useStore($selectedToken)
const isInitSend = useStore($isInitSend) const isInitSend = useStore($isInitSend)
const amount = useStore($amount)
const [amount, setAmount] = useState('')
const [debouncedAmount] = useDebounce(amount, 500)
const isDisabled = !account const isDisabled = !account
// reset amount whenever token changes // reset amount whenever token changes
useEffect(() => { useEffect(() => {
if (!selectedToken) return if (!selectedToken) return
setAmount('') $amount.set('')
}, [selectedToken]) }, [selectedToken])
return isInitSend ? ( return isInitSend ? (
<Send amount={debouncedAmount} /> <Send />
) : ( ) : (
<form <form
className={styles.web3} className={styles.web3}
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault()
if (debouncedAmount === '' || debouncedAmount === '0') return if (amount === '' || amount === '0') return
$isInitSend.set(true) $isInitSend.set(true)
}} }}
> >
<>
<div className={styles.rainbowkit}> <div className={styles.rainbowkit}>
<ConnectButton chainStatus="full" showBalance={false} /> <ConnectButton chainStatus="full" showBalance={false} />
</div> </div>
<InputGroup <InputGroup isDisabled={isDisabled} />
amount={amount}
setAmount={setAmount}
isDisabled={isDisabled}
/>
<div className={styles.disclaimer}> <div className={styles.disclaimer}>
Sends tokens to my account <code>{siteConfig.author.ether.ens}</code> Sends tokens to my account <code>{siteConfig.author.ether.ens}</code>
</div> </div>
</>
</form> </form>
) )
} }

View File

@ -3,17 +3,20 @@ import Input from '@components/Input'
import { Conversion } from '../Conversion' import { Conversion } from '../Conversion'
import styles from './InputGroup.module.css' import styles from './InputGroup.module.css'
import { TokenSelect } from '../TokenSelect' import { TokenSelect } from '../TokenSelect'
import { $isInitSend } from '@features/Web3/stores' import { $amount, $isInitSend } from '@features/Web3/stores'
import { useStore } from '@nanostores/react'
export function InputGroup({ export function InputGroup({
amount, isDisabled
isDisabled,
setAmount
}: { }: {
amount: string
isDisabled: boolean isDisabled: boolean
setAmount: React.Dispatch<React.SetStateAction<string>>
}): ReactElement { }): ReactElement {
const amount = useStore($amount)
function handleChange(newAmount: string) {
$amount.set(newAmount)
}
return ( return (
<> <>
<div className={styles.inputGroup}> <div className={styles.inputGroup}>
@ -27,7 +30,7 @@ export function InputGroup({
pattern="[0-9.]*" pattern="[0-9.]*"
value={amount} value={amount}
placeholder="0.00" placeholder="0.00"
onChange={(e) => setAmount(e.target.value)} onChange={(e) => handleChange(e.target.value)}
className={styles.inputInput} className={styles.inputInput}
/> />
</div> </div>
@ -40,7 +43,7 @@ export function InputGroup({
Preview Preview
</button> </button>
</div> </div>
<Conversion amount={amount} /> <Conversion />
</> </>
) )
} }

View File

@ -1,9 +1,3 @@
.amount,
.network,
.to {
/* font-weight: var(--font-weight-bold); */
}
.to, .to,
.from { .from {
display: block; display: block;
@ -23,7 +17,7 @@
margin-left: -14px; margin-left: -14px;
} }
.from .table { .table {
/* max-width: 386px; */ /* max-width: 386px; */
margin-bottom: calc(var(--spacer) / 1.5); margin-bottom: calc(var(--spacer) / 1.5);
} }

View File

@ -4,11 +4,11 @@ import type {
SendTransactionArgs, SendTransactionArgs,
WriteContractPreparedArgs WriteContractPreparedArgs
} from 'wagmi/actions' } from 'wagmi/actions'
import styles from './SendTable.module.css' import styles from './Data.module.css'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $selectedToken } from '@features/Web3/stores' import { $selectedToken } from '@features/Web3/stores'
export function SendTable({ export function Data({
to, to,
ensResolved, ensResolved,
txConfig, txConfig,

View File

@ -0,0 +1,21 @@
.actions {
display: flex;
justify-content: center;
}
.actions button {
line-height: 1;
}
.actions button:first-child {
margin-right: var(--spacer);
width: 115px;
height: 50px;
padding-top: 0;
padding-bottom: 0;
}
.alert {
font-size: var(--font-size-small);
display: inline-block;
}

View File

@ -0,0 +1,67 @@
import { Loader } from '@components/Loader'
import { usePrepareSend } from '@features/Web3/hooks/usePrepareSend'
import { useSend } from '@features/Web3/hooks/useSend'
import { $isInitSend } from '@features/Web3/stores'
import { useEnsAddress, useEnsName } from 'wagmi'
import { Data } from './Data'
import siteConfig from '@config/blog.config'
import styles from './Preview.module.css'
export function Preview() {
// Always resolve to address from ENS name and vice versa
// so nobody has to trust my config values.
const { ens } = siteConfig.author.ether
const { data: to } = useEnsAddress({ name: ens, chainId: 1 })
const { data: ensResolved } = useEnsName({
address: to as `0x${string}` | undefined,
chainId: 1
})
const {
data: txConfig,
error: prepareError,
isError: isPrepareError
} = usePrepareSend({ to })
const { handleSend, isLoading, error } = useSend({ txConfig })
// TODO: Cancel flow if chain changes in preview as this can mess with token selection
// useEffect(() => {
// if (!chain?.id || $isInitSend.get() === false) return
// $isInitSend.set(false)
// }, [chain?.id])
return (
<>
<Data
to={to}
ensResolved={ensResolved}
txConfig={txConfig}
isDisabled={isLoading}
/>
{error || prepareError ? (
<div className={styles.alert}>{error || prepareError}</div>
) : null}
<footer className={styles.actions}>
<button
onClick={async (e) => {
e?.preventDefault()
await handleSend()
}}
className="btn btn-primary"
disabled={isLoading || !txConfig || isPrepareError}
>
{isLoading ? <Loader /> : 'Make it rain'}
</button>
<button
onClick={() => $isInitSend.set(false)}
className="link"
disabled={isLoading}
>
Cancel
</button>
</footer>
</>
)
}

View File

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

View File

@ -1,29 +1,3 @@
.send { .send {
margin-top: calc(var(--spacer) / 2); margin-top: calc(var(--spacer) / 2);
} }
.title {
text-align: center;
}
.actions {
display: flex;
justify-content: center;
}
.actions button {
line-height: 1;
}
.actions button:first-child {
margin-right: var(--spacer);
width: 115px;
height: 50px;
padding-top: 0;
padding-bottom: 0;
}
.alert {
font-size: var(--font-size-small);
display: inline-block;
}

View File

@ -1,71 +1,11 @@
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { useEnsAddress, useEnsName } from 'wagmi' import { $txHash } from '@features/Web3/stores'
import { $isInitSend, $txHash } from '@features/Web3/stores'
import siteConfig from '@config/blog.config'
import styles from './Send.module.css' import styles from './Send.module.css'
import { SendTable } from './SendTable' import { Success } from '../Success'
import { Loader } from '@components/Loader' import { Preview } from '../Preview'
import { usePrepareSend } from '@features/Web3/hooks/usePrepareSend'
import { useSend } from '@features/Web3/hooks/useSend'
export function Send({ amount }: { amount: string }) { export function Send() {
const txHash = useStore($txHash) const txHash = useStore($txHash)
// Always resolve to address from ENS name and vice versa return <div className={styles.send}>{txHash ? <Success /> : <Preview />}</div>
// so nobody has to trust my config values.
const { ens } = siteConfig.author.ether
const { data: to } = useEnsAddress({ name: ens, chainId: 1 })
const { data: ensResolved } = useEnsName({
address: to as `0x${string}` | undefined,
chainId: 1
})
const {
data: txConfig,
error: prepareError,
isError: isPrepareError
} = usePrepareSend({ amount, to })
const { handleSend, isLoading, error } = useSend({ txConfig })
// Cancel send flow if chain changes as this can mess with token selection
// useEffect(() => {
// if (!chain?.id || $isInitSend.get() === false) return
// $isInitSend.set(false)
// }, [chain?.id])
console.log(txHash)
return (
<>
<div className={styles.send}>
{/* <h5 className={styles.title}>You are sending</h5> */}
<SendTable
to={to}
ensResolved={ensResolved}
txConfig={txConfig}
isDisabled={isLoading}
/>
<div className={styles.alert}>{error || prepareError}</div>
<footer className={styles.actions}>
<button
onClick={(e) => handleSend(e)}
className="btn btn-primary"
disabled={isLoading || !txConfig || isPrepareError}
>
{isLoading ? <Loader /> : 'Make it rain'}
</button>
<button
onClick={() => $isInitSend.set(false)}
className="link"
disabled={isLoading}
>
Cancel
</button>
</footer>
</div>
</>
)
} }

View File

@ -0,0 +1,8 @@
.success {
text-align: center;
margin-top: var(--spacer);
}
.title {
font-size: var(--font-size-h3);
}

View File

@ -0,0 +1,37 @@
import { $txHash, $isInitSend } from '@features/Web3/stores'
import { useStore } from '@nanostores/react'
import styles from './Success.module.css'
export function Success() {
const txHash = useStore($txHash)
return (
<div className={styles.success}>
<h5 className={styles.title}>You're amazing!</h5>
<p>
Your transaction has been sent. You can check the status on{' '}
<a
href={`https://etherscan.io/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
Etherscan
</a>
.
</p>
<p>
<code>0xxxx{txHash}</code>
</p>
<footer className={styles.actions}>
<button
onClick={() => $isInitSend.set(false)}
className="btn btn-primary"
>
Reset
</button>
</footer>
</div>
)
}

View File

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

View File

@ -6,14 +6,17 @@ import { Icon as ChevronsDown } from '@images/components/react/ChevronsDown'
import { Icon as ChevronsUp } from '@images/components/react/ChevronsUp' import { Icon as ChevronsUp } from '@images/components/react/ChevronsUp'
import { useFetchTokens } from '@features/Web3/hooks/useFetchTokens' import { useFetchTokens } from '@features/Web3/hooks/useFetchTokens'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $tokens } from '@features/Web3/stores/tokens' import { $setTokens, $tokens } from '@features/Web3/stores'
import { import {
$selectedToken, $selectedToken,
$setSelectedToken $setSelectedToken
} from '@features/Web3/stores/selectedToken' } from '@features/Web3/stores/selectedToken'
import { Loader } from '@components/Loader' import { Loader } from '@components/Loader'
import { useAccount } from 'wagmi'
import { useEffect } from 'react'
export function TokenSelect() { export function TokenSelect() {
const { address } = useAccount()
const { isLoading } = useFetchTokens() const { isLoading } = useFetchTokens()
const tokens = useStore($tokens) const tokens = useStore($tokens)
const selectedToken = useStore($selectedToken) const selectedToken = useStore($selectedToken)
@ -28,6 +31,13 @@ export function TokenSelect() {
$setSelectedToken(token) $setSelectedToken(token)
} }
// reset when no account connected
useEffect(() => {
if (!address && tokens?.length) {
$setTokens(undefined)
}
}, [address])
return tokens && selectedToken ? ( return tokens && selectedToken ? (
<Select.Root <Select.Root
defaultValue={selectedToken?.address} defaultValue={selectedToken?.address}
@ -38,7 +48,6 @@ export function TokenSelect() {
className="SelectTrigger" className="SelectTrigger"
disabled={isLoading} disabled={isLoading}
aria-label="Token" aria-label="Token"
placeholder="..."
> >
<Select.Value /> <Select.Value />
<Select.Icon> <Select.Icon>

View File

@ -5,18 +5,17 @@ import type {
SendTransactionArgs, SendTransactionArgs,
WriteContractPreparedArgs WriteContractPreparedArgs
} from 'wagmi/actions' } from 'wagmi/actions'
import { $selectedToken } from '@features/Web3/stores' import { $amount, $selectedToken } from '@features/Web3/stores'
import { prepare } from './prepare' import { prepare } from './prepare'
export function usePrepareSend({ export function usePrepareSend({
amount,
to to
}: { }: {
amount: string
to: `0x${string}` | null | undefined to: `0x${string}` | null | undefined
}) { }) {
const { chain } = useNetwork()
const selectedToken = useStore($selectedToken) const selectedToken = useStore($selectedToken)
const amount = useStore($amount)
const { chain } = useNetwork()
const [txConfig, setTxConfig] = useState< const [txConfig, setTxConfig] = useState<
SendTransactionArgs | WriteContractPreparedArgs SendTransactionArgs | WriteContractPreparedArgs

View File

@ -1,6 +1,6 @@
import { $txHash, $selectedToken } from '@features/Web3/stores' import { $txHash, $selectedToken } from '@features/Web3/stores'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { useState, type FormEvent } from 'react' import { useState } from 'react'
import type { import type {
SendTransactionArgs, SendTransactionArgs,
WriteContractPreparedArgs WriteContractPreparedArgs
@ -18,9 +18,7 @@ export function useSend({
const [isError, setIsError] = useState(false) const [isError, setIsError] = useState(false)
const [error, setError] = useState<string | undefined>() const [error, setError] = useState<string | undefined>()
async function handleSend(event: FormEvent<HTMLButtonElement>) { async function handleSend() {
event?.preventDefault()
try { try {
setIsError(false) setIsError(false)
setError(undefined) setError(undefined)

View File

@ -1,4 +1,5 @@
import { atom } from 'nanostores' import { atom } from 'nanostores'
export const $isInitSend = atom<boolean>(false) export const $isInitSend = atom<boolean>(false)
export const $amount = atom<string>('')
export const $txHash = atom<string | undefined>() export const $txHash = atom<string | undefined>()

View File

@ -6,7 +6,7 @@ export const $tokens = atom<GetToken[] | undefined>()
export const $setTokens = action( export const $setTokens = action(
$tokens, $tokens,
'setTokens', 'setTokens',
(store, tokens: GetToken[]) => { (store, tokens: GetToken[] | undefined) => {
store.set(tokens) store.set(tokens)
return store.get() return store.get()
} }