mirror of
https://github.com/kremalicious/blog.git
synced 2024-11-22 09:56:51 +01:00
new token selection, reorg
This commit is contained in:
parent
d809f982f0
commit
0ca6558def
@ -2,7 +2,7 @@ import Web3Donation from './Web3Donation'
|
|||||||
import config from '@config/blog.config'
|
import config from '@config/blog.config'
|
||||||
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
|
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
|
||||||
import { WagmiConfig } from 'wagmi'
|
import { WagmiConfig } from 'wagmi'
|
||||||
import { wagmiConfig, chains, theme } from '@lib/rainbowkit'
|
import { wagmiConfig, chains, theme } from './Web3Donation/lib/rainbowkit'
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
|
|
||||||
export default function Web3(): ReactElement {
|
export default function Web3(): ReactElement {
|
||||||
|
@ -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} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
9
src/components/Sponsor/Web3Donation/api/getBalance.ts
Normal file
9
src/components/Sponsor/Web3Donation/api/getBalance.ts
Normal 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
|
||||||
|
}
|
18
src/components/Sponsor/Web3Donation/api/getFiat.ts
Normal file
18
src/components/Sponsor/Web3Donation/api/getFiat.ts
Normal 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 }
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
import { describe, it } from 'vitest'
|
import { describe, it } from 'vitest'
|
||||||
import Conversion from './Conversion'
|
import { Conversion } from './Conversion'
|
||||||
|
|
||||||
describe('Conversion', () => {
|
describe('Conversion', () => {
|
||||||
it('renders without crashing', async () => {
|
it('renders without crashing', async () => {
|
@ -1,26 +1,8 @@
|
|||||||
import { type ReactElement, useEffect, useState } from 'react'
|
import { type ReactElement, useEffect, useState } from 'react'
|
||||||
import styles from './Conversion.module.css'
|
import styles from './Conversion.module.css'
|
||||||
|
import { getFiat } from '../../api/getFiat'
|
||||||
|
|
||||||
export async function getFiat({
|
export function Conversion({
|
||||||
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({
|
|
||||||
amount,
|
amount,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: {
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Conversion'
|
@ -3,48 +3,28 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
animation: fadeIn 0.8s ease-out backwards;
|
animation: fadeIn 0.8s ease-out backwards;
|
||||||
margin-top: var(--spacer);
|
margin-top: var(--spacer);
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 40rem) {
|
/* @media (min-width: 40rem) {
|
||||||
.inputGroup {
|
.inputGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
|
|
||||||
.inputGroup button {
|
/* .currency {
|
||||||
width: 100%;
|
} */
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-color: var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 40rem) {
|
:global([data-theme='dark']) .currency {
|
||||||
.inputGroup button {
|
border-right-color: #000;
|
||||||
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%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputInput {
|
.inputInput {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 3)
|
padding: calc(var(--spacer) / 4);
|
||||||
calc(var(--spacer) / 3) calc(var(--spacer) * 1.7);
|
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
@ -68,27 +48,25 @@
|
|||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.currency {
|
.submit {
|
||||||
position: absolute;
|
width: 100%;
|
||||||
top: 1px;
|
border-top-left-radius: 0;
|
||||||
bottom: 1px;
|
border-top-right-radius: 0;
|
||||||
left: 1px;
|
border-color: var(--border-color);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global([data-theme='dark']) .currency {
|
@media (min-width: 40rem) {
|
||||||
border-right-color: #000;
|
.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 {
|
.message {
|
||||||
composes: message from './index.module.css';
|
composes: message from '../../index.module.css';
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
@ -1,6 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { describe, it, expect, vi } from 'vitest'
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
import InputGroup from './InputGroup'
|
import { InputGroup } from '.'
|
||||||
|
|
||||||
const setAmount = vi.fn()
|
const setAmount = vi.fn()
|
||||||
|
|
@ -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} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './InputGroup'
|
@ -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;
|
||||||
|
}
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Select'
|
@ -9,8 +9,8 @@ import {
|
|||||||
} from 'wagmi'
|
} from 'wagmi'
|
||||||
import { ConnectButton } from '@rainbow-me/rainbowkit'
|
import { ConnectButton } from '@rainbow-me/rainbowkit'
|
||||||
|
|
||||||
import Alert, { getTransactionMessage } from './Alert'
|
import Alert, { getTransactionMessage } from './components/Alert/Alert'
|
||||||
import InputGroup from './InputGroup'
|
import { InputGroup } from './components/Input'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
|
|
||||||
export default function Web3Donation({
|
export default function Web3Donation({
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { type Theme, getDefaultWallets } from '@rainbow-me/rainbowkit'
|
import { type Theme, getDefaultWallets } from '@rainbow-me/rainbowkit'
|
||||||
import { configureChains, createConfig } from 'wagmi'
|
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 { infuraProvider } from 'wagmi/providers/infura'
|
||||||
import { publicProvider } from 'wagmi/providers/public'
|
import { publicProvider } from 'wagmi/providers/public'
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ if (isProduction && (!PUBLIC_INFURA_ID || !PUBLIC_WALLETCONNECT_ID)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const { chains, publicClient } = configureChains(
|
export const { chains, publicClient } = configureChains(
|
||||||
[mainnet, polygon],
|
[mainnet, polygon, base, bsc],
|
||||||
[infuraProvider({ apiKey: PUBLIC_INFURA_ID }), publicProvider()]
|
[infuraProvider({ apiKey: PUBLIC_INFURA_ID }), publicProvider()]
|
||||||
)
|
)
|
||||||
|
|
@ -33,7 +33,7 @@ const coins = Object.entries(config.author).filter(
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<LayoutBase title="Thanks" pageTitle="Say Thanks">
|
<LayoutBase title="Say Thanks">
|
||||||
<BackButton />
|
<BackButton />
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
Loading…
Reference in New Issue
Block a user