17.05.2026 funny
This commit is contained in:
1
dist/assets/index-BbQ5Ok1h.css
vendored
1
dist/assets/index-BbQ5Ok1h.css
vendored
File diff suppressed because one or more lines are too long
60
dist/assets/index-CbGm-SmX.js
vendored
60
dist/assets/index-CbGm-SmX.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-ChMX4U7G.css
vendored
Normal file
1
dist/assets/index-ChMX4U7G.css
vendored
Normal file
File diff suppressed because one or more lines are too long
60
dist/assets/index-DOZHm_HX.js
vendored
Normal file
60
dist/assets/index-DOZHm_HX.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ЭКСА — Ваш мост в мир цифровых активов</title>
|
<title>ЭКСА — Ваш мост в мир цифровых активов</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CbGm-SmX.js"></script>
|
<script type="module" crossorigin src="/assets/index-DOZHm_HX.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BbQ5Ok1h.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-ChMX4U7G.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function RouterProvider() {
|
|||||||
<Route element={<ProtectedRoute />}>
|
<Route element={<ProtectedRoute />}>
|
||||||
<Route path={ROUTES.CONVERTER} element={<ConverterPage />} />
|
<Route path={ROUTES.CONVERTER} element={<ConverterPage />} />
|
||||||
<Route path={ROUTES.WALLET} element={<WalletPage />} />
|
<Route path={ROUTES.WALLET} element={<WalletPage />} />
|
||||||
|
<Route path={ROUTES.WALLET_CHAIN} element={<WalletPage />} />
|
||||||
<Route path={ROUTES.SWAP} element={<SwapPage />} />
|
<Route path={ROUTES.SWAP} element={<SwapPage />} />
|
||||||
<Route path={ROUTES.BRIDGE} element={<BridgePage />} />
|
<Route path={ROUTES.BRIDGE} element={<BridgePage />} />
|
||||||
<Route path={ROUTES.PROFILE} element={<ProfilePage />} />
|
<Route path={ROUTES.PROFILE} element={<ProfilePage />} />
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Navigate, useNavigate } from 'react-router-dom'
|
import { Navigate, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { useMe } from '@features/auth'
|
import { useMe } from '@features/auth'
|
||||||
import { ROUTES } from '@shared/config/routes'
|
import { ROUTES } from '@shared/config/routes'
|
||||||
import { usePortfolio, useCreateWallet } from '@features/wallet'
|
import { usePortfolio, useCreateWallet, CHAINS, type Chain } from '@features/wallet'
|
||||||
import { BalanceCard } from '@widgets/balance-card'
|
import { BalanceCard } from '@widgets/balance-card'
|
||||||
import { TokenTable } from '@widgets/token-table'
|
import { TokenTable } from '@widgets/token-table'
|
||||||
import { WalletHeader } from '@widgets/wallet-header'
|
import { WalletHeader } from '@widgets/wallet-header'
|
||||||
|
import { WalletChainTabs } from '@widgets/wallet-chain-tabs'
|
||||||
import { Button } from '@shared/ui'
|
import { Button } from '@shared/ui'
|
||||||
import styles from './WalletPage.module.css'
|
import styles from './WalletPage.module.css'
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ export function WalletPage() {
|
|||||||
const { error: portfolioError } = usePortfolio()
|
const { error: portfolioError } = usePortfolio()
|
||||||
const { mutate: createWallet, isPending } = useCreateWallet()
|
const { mutate: createWallet, isPending } = useCreateWallet()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { chain: chainParam } = useParams<{ chain?: string }>()
|
||||||
|
|
||||||
const noWallet = (portfolioError as any)?.error?.includes('No wallets')
|
const noWallet = (portfolioError as any)?.error?.includes('No wallets')
|
||||||
|
|
||||||
@@ -20,6 +22,11 @@ export function WalletPage() {
|
|||||||
if (isError) return <div className={styles.error}>Произошла ошибка. Попробуйте обновить страницу.</div>
|
if (isError) return <div className={styles.error}>Произошла ошибка. Попробуйте обновить страницу.</div>
|
||||||
if (data && !data.kyc_verified) return <Navigate to={ROUTES.KYC} replace />
|
if (data && !data.kyc_verified) return <Navigate to={ROUTES.KYC} replace />
|
||||||
|
|
||||||
|
const upper = chainParam?.toUpperCase() as Chain | undefined
|
||||||
|
const chain: Chain | undefined = upper && CHAINS.includes(upper) ? upper : undefined
|
||||||
|
|
||||||
|
if (!noWallet && !chain) return <Navigate to="/wallet/btc" replace />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<WalletHeader />
|
<WalletHeader />
|
||||||
@@ -39,7 +46,8 @@ export function WalletPage() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<BalanceCard />
|
<BalanceCard />
|
||||||
<TokenTable />
|
<WalletChainTabs />
|
||||||
|
<TokenTable chain={chain!} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const ROUTES = {
|
export const ROUTES = {
|
||||||
HOME: '/',
|
HOME: '/',
|
||||||
WALLET: '/wallet',
|
WALLET: '/wallet',
|
||||||
|
WALLET_CHAIN: '/wallet/:chain',
|
||||||
SWAP: "/swap",
|
SWAP: "/swap",
|
||||||
BRIDGE: "/bridge",
|
BRIDGE: "/bridge",
|
||||||
LOGIN: '/login',
|
LOGIN: '/login',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Button } from '@shared/ui'
|
import { Button } from '@shared/ui'
|
||||||
import { useMe, useUploadAvatar } from '@features/auth'
|
import { useMe, useUploadAvatar } from '@features/auth'
|
||||||
import styles from './ProfileAvatar.module.css'
|
import styles from './ProfileAvatar.module.css'
|
||||||
@@ -21,8 +21,14 @@ export function ProfileAvatar() {
|
|||||||
const { mutateAsync: upload, isPending } = useUploadAvatar()
|
const { mutateAsync: upload, isPending } = useUploadAvatar()
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [imgFailed, setImgFailed] = useState(false)
|
||||||
|
|
||||||
const avatarLink = data?.avatar_link ?? null
|
const avatarLink = data?.avatar_link ?? null
|
||||||
|
const showImg = avatarLink && !imgFailed
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setImgFailed(false)
|
||||||
|
}, [avatarLink])
|
||||||
|
|
||||||
const openPicker = () => {
|
const openPicker = () => {
|
||||||
if (isPending) return
|
if (isPending) return
|
||||||
@@ -46,8 +52,17 @@ export function ProfileAvatar() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.col}>
|
<div className={styles.col}>
|
||||||
<div className={styles.avatar} onClick={openPicker}>
|
<div className={styles.avatar} onClick={openPicker}>
|
||||||
{avatarLink ? (
|
{showImg ? (
|
||||||
<img src={avatarLink} alt="avatar" className={styles.avatarImg} />
|
<img
|
||||||
|
src={avatarLink}
|
||||||
|
alt="avatar"
|
||||||
|
className={styles.avatarImg}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
onError={() => {
|
||||||
|
console.warn('[avatar] failed to load', avatarLink)
|
||||||
|
setImgFailed(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<circle cx="12" cy="8" r="4" />
|
<circle cx="12" cy="8" r="4" />
|
||||||
|
|||||||
76
src/widgets/token-table/model/useChainTokenRows.ts
Normal file
76
src/widgets/token-table/model/useChainTokenRows.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { useWalletBalance } from '@features/wallet'
|
||||||
|
import type { Chain } from '@features/wallet'
|
||||||
|
import { TOKENS, type Token } from './tokens'
|
||||||
|
|
||||||
|
const NATIVE_TICKER: Record<Chain, string> = {
|
||||||
|
BTC: 'BTC',
|
||||||
|
ETH: 'ETH',
|
||||||
|
SOL: 'SOL',
|
||||||
|
TRX: 'TRX',
|
||||||
|
BSC: 'BNB',
|
||||||
|
}
|
||||||
|
|
||||||
|
const NATIVE_NAME: Record<Chain, string> = {
|
||||||
|
BTC: 'Bitcoin',
|
||||||
|
ETH: 'Ethereum',
|
||||||
|
SOL: 'Solana',
|
||||||
|
TRX: 'Tron',
|
||||||
|
BSC: 'BNB',
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TOKEN_COLOR = '#2A2D3A'
|
||||||
|
|
||||||
|
function formatUsd(value: number | null | undefined): string {
|
||||||
|
if (value == null) return '$—'
|
||||||
|
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPrice(value: number | null | undefined): string {
|
||||||
|
if (value == null) return '$—'
|
||||||
|
if (value >= 1) {
|
||||||
|
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||||
|
}
|
||||||
|
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 6 })}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function lookupStatic(ticker: string): Token | undefined {
|
||||||
|
return TOKENS.find((t) => t.ticker === ticker)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChainTokenRows(chain: Chain) {
|
||||||
|
const { data, isLoading } = useWalletBalance(chain)
|
||||||
|
|
||||||
|
if (!data) return { rows: [] as Token[], isLoading }
|
||||||
|
|
||||||
|
const nativeTicker = NATIVE_TICKER[chain]
|
||||||
|
const nativeStatic = lookupStatic(nativeTicker)
|
||||||
|
|
||||||
|
const nativeRow: Token = {
|
||||||
|
ticker: nativeTicker,
|
||||||
|
name: NATIVE_NAME[chain],
|
||||||
|
logo: nativeStatic?.logo,
|
||||||
|
color: nativeStatic?.color ?? DEFAULT_TOKEN_COLOR,
|
||||||
|
price: formatPrice(data.native.usdPrice),
|
||||||
|
change: 0,
|
||||||
|
bal: data.native.formatted,
|
||||||
|
usd: formatUsd(data.native.usdValue),
|
||||||
|
fav: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenRows: Token[] = Object.entries(data.tokens).map(([symbol, amount]) => {
|
||||||
|
const staticToken = lookupStatic(symbol)
|
||||||
|
return {
|
||||||
|
ticker: symbol,
|
||||||
|
name: staticToken?.name ?? symbol,
|
||||||
|
logo: staticToken?.logo,
|
||||||
|
color: staticToken?.color ?? DEFAULT_TOKEN_COLOR,
|
||||||
|
price: formatPrice(amount.usdPrice),
|
||||||
|
change: 0,
|
||||||
|
bal: amount.formatted,
|
||||||
|
usd: formatUsd(amount.usdValue),
|
||||||
|
fav: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { rows: [nativeRow, ...tokenRows], isLoading }
|
||||||
|
}
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { useAllWalletBalances, usePrices } from '@features/wallet'
|
|
||||||
import { CHAINS } from '@features/wallet'
|
|
||||||
import { TOKENS } from './tokens'
|
|
||||||
import type { WalletBalanceData } from '@features/wallet'
|
|
||||||
|
|
||||||
const PRICE_SYMBOLS = ['BTC', 'ETH', 'SOL', 'TRX', 'BSC']
|
|
||||||
|
|
||||||
type ChainSource =
|
|
||||||
| { chain: 'BTC' | 'ETH' | 'SOL' | 'TRX' | 'BSC'; type: 'native' }
|
|
||||||
| { chain: 'ETH'; type: 'token'; symbol: string }
|
|
||||||
|
|
||||||
const CHAIN_MAP: Record<string, ChainSource> = {
|
|
||||||
BTC: { chain: 'BTC', type: 'native' },
|
|
||||||
ETH: { chain: 'ETH', type: 'native' },
|
|
||||||
SOL: { chain: 'SOL', type: 'native' },
|
|
||||||
TRX: { chain: 'TRX', type: 'native' },
|
|
||||||
BSC: { chain: 'BSC', type: 'native' },
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUsd(value: number | null | undefined): string {
|
|
||||||
if (value == null) return '$—'
|
|
||||||
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPrice(value: number | null | undefined): string {
|
|
||||||
if (value == null) return '$—'
|
|
||||||
if (value >= 1) {
|
|
||||||
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
|
||||||
}
|
|
||||||
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 6 })}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTokenRows() {
|
|
||||||
const balanceResults = useAllWalletBalances()
|
|
||||||
const pricesQuery = usePrices(PRICE_SYMBOLS)
|
|
||||||
|
|
||||||
const isLoading = balanceResults.some(r => r.isLoading) || pricesQuery.isLoading
|
|
||||||
|
|
||||||
const balanceByChain: Record<string, WalletBalanceData> = {}
|
|
||||||
CHAINS.forEach((chain, i) => {
|
|
||||||
const data = balanceResults[i].data
|
|
||||||
if (data) balanceByChain[chain] = data
|
|
||||||
})
|
|
||||||
|
|
||||||
const rows = TOKENS.map(t => {
|
|
||||||
const src = CHAIN_MAP[t.ticker]
|
|
||||||
const chainData = src ? balanceByChain[src.chain] : undefined
|
|
||||||
|
|
||||||
let bal = t.bal
|
|
||||||
let usd = t.usd
|
|
||||||
|
|
||||||
if (chainData) {
|
|
||||||
const amount =
|
|
||||||
src.type === 'native'
|
|
||||||
? chainData.native
|
|
||||||
: chainData.tokens[src.type === 'token' ? src.symbol : t.ticker]
|
|
||||||
|
|
||||||
if (amount) {
|
|
||||||
bal = amount.formatted ?? t.bal
|
|
||||||
usd = formatUsd(amount.usdValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const priceEntry = pricesQuery.data?.[t.ticker]
|
|
||||||
const price = priceEntry ? formatPrice(priceEntry.usd) : t.price
|
|
||||||
|
|
||||||
return { ...t, price, bal, usd }
|
|
||||||
})
|
|
||||||
|
|
||||||
return { rows, isLoading }
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTokenRows } from '../model/useTokenRows'
|
import { useChainTokenRows } from '../model/useChainTokenRows'
|
||||||
import { SendModal, TICKER_TO_CHAIN, type Chain } from '@widgets/send-modal'
|
import { SendModal, TICKER_TO_CHAIN, type Chain } from '@widgets/send-modal'
|
||||||
import { ReceiveModal } from '@widgets/receive-modal'
|
import { ReceiveModal } from '@widgets/receive-modal'
|
||||||
import styles from './TokenTable.module.css'
|
import styles from './TokenTable.module.css'
|
||||||
|
|
||||||
export function TokenTable() {
|
interface Props {
|
||||||
const { rows, isLoading } = useTokenRows()
|
chain: Chain
|
||||||
const [favs, setFavs] = useState<boolean[]>(() => rows.map((t) => t.fav))
|
}
|
||||||
|
|
||||||
|
export function TokenTable({ chain }: Props) {
|
||||||
|
const { rows, isLoading } = useChainTokenRows(chain)
|
||||||
|
const [favs, setFavs] = useState<Record<string, boolean>>({})
|
||||||
const [sendModal, setSendModal] = useState<{ open: boolean; network: Chain }>({
|
const [sendModal, setSendModal] = useState<{ open: boolean; network: Chain }>({
|
||||||
open: false,
|
open: false,
|
||||||
network: 'ETH',
|
network: 'ETH',
|
||||||
@@ -17,7 +21,7 @@ export function TokenTable() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function openSend(ticker: string) {
|
function openSend(ticker: string) {
|
||||||
const network = TICKER_TO_CHAIN[ticker] ?? 'ETH'
|
const network = TICKER_TO_CHAIN[ticker] ?? chain
|
||||||
setSendModal({ open: true, network })
|
setSendModal({ open: true, network })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,16 +30,16 @@ export function TokenTable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openReceive(ticker: string) {
|
function openReceive(ticker: string) {
|
||||||
const chain = TICKER_TO_CHAIN[ticker] ?? 'ETH'
|
const target = TICKER_TO_CHAIN[ticker] ?? chain
|
||||||
setReceiveModal({ open: true, chain })
|
setReceiveModal({ open: true, chain: target })
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeReceive() {
|
function closeReceive() {
|
||||||
setReceiveModal((s) => ({ ...s, open: false }))
|
setReceiveModal((s) => ({ ...s, open: false }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFav(i: number) {
|
function toggleFav(ticker: string) {
|
||||||
setFavs((prev) => prev.map((v, idx) => (idx === i ? !v : v)))
|
setFavs((prev) => ({ ...prev, [ticker]: !prev[ticker] }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendIcon = (
|
const sendIcon = (
|
||||||
@@ -64,14 +68,14 @@ export function TokenTable() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((t, i) => (
|
{rows.map((t) => (
|
||||||
<tr key={t.ticker}>
|
<tr key={t.ticker}>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
className={`${styles.star} ${favs[i] ? styles.starOn : ''}`}
|
className={`${styles.star} ${favs[t.ticker] ? styles.starOn : ''}`}
|
||||||
onClick={() => toggleFav(i)}
|
onClick={() => toggleFav(t.ticker)}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={favs[i] ? 'Убрать из избранного' : 'В избранное'}
|
aria-label={favs[t.ticker] ? 'Убрать из избранного' : 'В избранное'}
|
||||||
>
|
>
|
||||||
★
|
★
|
||||||
</button>
|
</button>
|
||||||
@@ -79,7 +83,7 @@ export function TokenTable() {
|
|||||||
<td>
|
<td>
|
||||||
<div className={styles.tokId}>
|
<div className={styles.tokId}>
|
||||||
<div className={styles.tokLogo} style={{ background: t.color }}>
|
<div className={styles.tokLogo} style={{ background: t.color }}>
|
||||||
{t.logo && <img src={t.logo} alt={t.ticker} className={''} />}
|
{t.logo ? <img src={t.logo} alt={t.ticker} className={''} /> : t.ticker[0]}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.balCol}>
|
<div className={styles.balCol}>
|
||||||
<b className={styles.cardTicker}>{t.ticker}</b>
|
<b className={styles.cardTicker}>{t.ticker}</b>
|
||||||
@@ -123,13 +127,13 @@ export function TokenTable() {
|
|||||||
|
|
||||||
{/* Mobile card list */}
|
{/* Mobile card list */}
|
||||||
<div className={styles.mobileList}>
|
<div className={styles.mobileList}>
|
||||||
{rows.map((t, i) => (
|
{rows.map((t) => (
|
||||||
<div key={t.ticker} className={styles.card}>
|
<div key={t.ticker} className={styles.card}>
|
||||||
<button
|
<button
|
||||||
className={`${styles.star} ${favs[i] ? styles.starOn : ''}`}
|
className={`${styles.star} ${favs[t.ticker] ? styles.starOn : ''}`}
|
||||||
onClick={() => toggleFav(i)}
|
onClick={() => toggleFav(t.ticker)}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={favs[i] ? 'Убрать из избранного' : 'В избранное'}
|
aria-label={favs[t.ticker] ? 'Убрать из избранного' : 'В избранное'}
|
||||||
>
|
>
|
||||||
★
|
★
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
1
src/widgets/wallet-chain-tabs/index.ts
Normal file
1
src/widgets/wallet-chain-tabs/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { WalletChainTabs } from './ui/WalletChainTabs'
|
||||||
53
src/widgets/wallet-chain-tabs/ui/WalletChainTabs.module.css
Normal file
53
src/widgets/wallet-chain-tabs/ui/WalletChainTabs.module.css
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 24px 0 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
|
||||||
|
color: var(--text-secondary, rgba(255, 255, 255, 0.65));
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
color: var(--text-primary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background: rgba(74, 109, 255, 0.18);
|
||||||
|
border-color: rgba(74, 109, 255, 0.55);
|
||||||
|
color: var(--text-primary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.tab {
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/widgets/wallet-chain-tabs/ui/WalletChainTabs.tsx
Normal file
39
src/widgets/wallet-chain-tabs/ui/WalletChainTabs.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { NavLink } from 'react-router-dom'
|
||||||
|
import type { Chain } from '@features/wallet'
|
||||||
|
import btc from '@shared/assets/btc.svg'
|
||||||
|
import eth from '@shared/assets/eth.svg'
|
||||||
|
import sol from '@shared/assets/sol.svg'
|
||||||
|
import trx from '@shared/assets/trx.svg'
|
||||||
|
import bnb from '@shared/assets/bnb.svg'
|
||||||
|
import styles from './WalletChainTabs.module.css'
|
||||||
|
|
||||||
|
interface TabItem {
|
||||||
|
chain: Chain
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const TABS: TabItem[] = [
|
||||||
|
{ chain: 'BTC', label: 'BTC', icon: btc },
|
||||||
|
{ chain: 'ETH', label: 'ETH', icon: eth },
|
||||||
|
{ chain: 'SOL', label: 'SOL', icon: sol },
|
||||||
|
{ chain: 'TRX', label: 'TRX', icon: trx },
|
||||||
|
{ chain: 'BSC', label: 'BSC', icon: bnb },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function WalletChainTabs() {
|
||||||
|
return (
|
||||||
|
<div className={styles.tabs}>
|
||||||
|
{TABS.map((t) => (
|
||||||
|
<NavLink
|
||||||
|
key={t.chain}
|
||||||
|
to={`/wallet/${t.chain.toLowerCase()}`}
|
||||||
|
className={({ isActive }) => `${styles.tab} ${isActive ? styles.active : ''}`}
|
||||||
|
>
|
||||||
|
<img src={t.icon} alt={t.label} className={styles.icon} />
|
||||||
|
<span>{t.label}</span>
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user