Compare commits

...

2 Commits

Author SHA1 Message Date
79f1ee371b 14.05.2026 rip 2026-05-14 15:50:22 +03:00
0f88a68c59 feat: похуй 2.0 2026-05-14 00:31:06 +03:00
12 changed files with 149 additions and 17 deletions

View File

@@ -1,11 +1,13 @@
import { Navigate, Outlet } from 'react-router-dom' import { Navigate, Outlet, useLocation } from 'react-router-dom'
import { useIsAuthenticated } from '@features/auth' import { useIsAuthenticated } from '@features/auth'
import { ROUTES } from '@shared/config/routes' import { ROUTES } from '@shared/config/routes'
export function GuestRoute() { export function GuestRoute() {
const { isAuthenticated, isLoading } = useIsAuthenticated() const { isAuthenticated, isLoading } = useIsAuthenticated()
const location = useLocation()
const from = (location.state as { from?: { pathname: string } })?.from?.pathname ?? ROUTES.WALLET
if (isLoading) return null if (isLoading) return null
if (isAuthenticated) return <Navigate to={ROUTES.WALLET} replace /> if (isAuthenticated) return <Navigate to={from} replace />
return <Outlet /> return <Outlet />
} }

View File

@@ -1,7 +1,9 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
const queryClient = new QueryClient() const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
export function QueryProvider({ children }: { children: ReactNode }) { export function QueryProvider({ children }: { children: ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>

View File

@@ -26,9 +26,9 @@ export function RouterProvider() {
</Route> </Route>
<Route path={ROUTES.CONVERTER} element={<ConverterPage />} /> <Route path={ROUTES.CONVERTER} element={<ConverterPage />} />
<Route path={ROUTES.WALLET} element={<WalletPage />} />
<Route element={<ProtectedRoute />}> <Route element={<ProtectedRoute />}>
<Route path={ROUTES.WALLET} element={<WalletPage />} />
<Route path={ROUTES.SWAP} element={<SwapPage />} /> <Route path={ROUTES.SWAP} element={<SwapPage />} />
<Route path={ROUTES.PROFILE} element={<ProfilePage />} /> <Route path={ROUTES.PROFILE} element={<ProfilePage />} />
<Route path={ROUTES.SEED_PHRASE} element={<SeedPhrasePage />} /> <Route path={ROUTES.SEED_PHRASE} element={<SeedPhrasePage />} />

View File

@@ -0,0 +1,36 @@
import { api } from '@shared/api/base'
export type Chain = 'ETH' | 'BSC' | 'BTC' | 'TRX' | 'SOL'
export interface FormattedAmount {
raw: string
formatted: string
decimals: number
usdPrice: number
usdValue: number
}
export interface WalletBalanceData {
chain: Chain
address: string
native: FormattedAmount
tokens: Record<string, FormattedAmount>
}
export interface PriceEntry {
usd: number
}
export const CHAINS: Chain[] = ['ETH', 'BSC', 'BTC', 'TRX', 'SOL']
export async function getWalletBalance(chain: Chain): Promise<WalletBalanceData> {
const res = await api.get<{ success: boolean; data: WalletBalanceData }>(`/api/wallets/${chain}/balance`)
return res.data
}
export async function getPrices(symbols: string[]): Promise<Record<string, PriceEntry>> {
const res = await api.get<{ success: boolean; data: Record<string, PriceEntry> }>(
`/api/prices?symbols=${symbols.join(',')}`
)
return res.data
}

View File

@@ -0,0 +1,3 @@
export { useAllWalletBalances, usePrices } from './model/useWalletData'
export type { Chain, FormattedAmount, WalletBalanceData, PriceEntry } from './api/walletApi'
export { CHAINS } from './api/walletApi'

View File

@@ -0,0 +1,20 @@
import { useQuery, useQueries } from '@tanstack/react-query'
import { getWalletBalance, getPrices, CHAINS } from '../api/walletApi'
export function useAllWalletBalances() {
return useQueries({
queries: CHAINS.map(chain => ({
queryKey: ['wallet', 'balance', chain],
queryFn: () => getWalletBalance(chain),
staleTime: 30_000,
})),
})
}
export function usePrices(symbols: string[]) {
return useQuery({
queryKey: ['wallet', 'prices', symbols.join(',')],
queryFn: () => getPrices(symbols),
staleTime: 5 * 60 * 1000,
})
}

View File

@@ -68,7 +68,6 @@ export function Converter() {
<button <button
type="button" type="button"
className={styles.swapBtn} className={styles.swapBtn}
onClick={c.toggleMode}
aria-label="Поменять направление" aria-label="Поменять направление"
> >
<svg width={16} height={16} viewBox="0 0 16 16" fill="none"> <svg width={16} height={16} viewBox="0 0 16 16" fill="none">

View File

@@ -1,10 +1,8 @@
import { useState } from 'react' import { useState } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query' import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useNavigate, useLocation } from 'react-router-dom'
import { loginStart, loginComplete, AUTH_QUERY_KEY } from '@features/auth' import { loginStart, loginComplete, AUTH_QUERY_KEY } from '@features/auth'
import { tokenStore } from '@shared/api/tokenStore' import { tokenStore } from '@shared/api/tokenStore'
import { clearCsrfCache } from '@shared/api/csrf' import { clearCsrfCache } from '@shared/api/csrf'
import { ROUTES } from '@shared/config/routes'
import type { ApiErrorResponse } from '@shared/api/types' import type { ApiErrorResponse } from '@shared/api/types'
function extractErrorMessage(error: unknown): string { function extractErrorMessage(error: unknown): string {
@@ -17,10 +15,7 @@ export function useLoginForm() {
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [verificationCode, setVerificationCode] = useState('') const [verificationCode, setVerificationCode] = useState('')
const [codeSent, setCodeSent] = useState(false) const [codeSent, setCodeSent] = useState(false)
const navigate = useNavigate()
const location = useLocation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const from = (location.state as { from?: { pathname: string } })?.from?.pathname ?? ROUTES.WALLET
const startMutation = useMutation({ const startMutation = useMutation({
mutationFn: loginStart, mutationFn: loginStart,
@@ -33,7 +28,6 @@ export function useLoginForm() {
clearCsrfCache() clearCsrfCache()
tokenStore.set(access_token) tokenStore.set(access_token)
queryClient.setQueryData(AUTH_QUERY_KEY, access_token) queryClient.setQueryData(AUTH_QUERY_KEY, access_token)
navigate(from, { replace: true })
}, },
}) })

View File

@@ -0,0 +1,69 @@
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', 'ARB']
type ChainSource =
| { chain: 'BTC' | 'ETH' | 'SOL' | 'TRX'; 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' },
ARB: { chain: 'ETH', type: 'token', symbol: 'ARB' },
}
function formatUsd(value: number): string {
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
function formatPrice(value: number): string {
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
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 }
}

View File

@@ -3,6 +3,12 @@
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
border-radius: 20px; border-radius: 20px;
overflow: hidden; overflow: hidden;
transition: opacity 0.2s;
}
.loading {
opacity: 0.6;
pointer-events: none;
} }
.table { .table {

View File

@@ -1,9 +1,10 @@
import { useState } from 'react' import { useState } from 'react'
import { TOKENS } from '../model/tokens' import { useTokenRows } from '../model/useTokenRows'
import styles from './TokenTable.module.css' import styles from './TokenTable.module.css'
export function TokenTable() { export function TokenTable() {
const [favs, setFavs] = useState<boolean[]>(TOKENS.map((t) => t.fav)) const { rows, isLoading } = useTokenRows()
const [favs, setFavs] = useState<boolean[]>(() => rows.map((t) => t.fav))
function toggleFav(i: number) { function toggleFav(i: number) {
setFavs((prev) => prev.map((v, idx) => (idx === i ? !v : v))) setFavs((prev) => prev.map((v, idx) => (idx === i ? !v : v)))
@@ -17,7 +18,7 @@ export function TokenTable() {
return ( return (
<> <>
<div className={styles.wrap}> <div className={`${styles.wrap} ${isLoading ? styles.loading : ''}`}>
{/* Desktop table */} {/* Desktop table */}
<table className={styles.table}> <table className={styles.table}>
<thead> <thead>
@@ -31,7 +32,7 @@ export function TokenTable() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{TOKENS.map((t, i) => ( {rows.map((t, i) => (
<tr key={t.ticker}> <tr key={t.ticker}>
<td> <td>
<button <button
@@ -81,7 +82,7 @@ export function TokenTable() {
{/* Mobile card list */} {/* Mobile card list */}
<div className={styles.mobileList}> <div className={styles.mobileList}>
{TOKENS.map((t, i) => ( {rows.map((t, i) => (
<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[i] ? styles.starOn : ''}`}

File diff suppressed because one or more lines are too long