Compare commits
2 Commits
08a735bda5
...
7e3493005b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e3493005b | |||
| 564f21552d |
1
dist/assets/index-BP0dDjYZ.css
vendored
1
dist/assets/index-BP0dDjYZ.css
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-CzU7MdbY.css
vendored
Normal file
1
dist/assets/index-CzU7MdbY.css
vendored
Normal file
File diff suppressed because one or more lines are too long
161
dist/assets/index-DFLADCGY.js
vendored
161
dist/assets/index-DFLADCGY.js
vendored
File diff suppressed because one or more lines are too long
161
dist/assets/index-NvuTFZg0.js
vendored
Normal file
161
dist/assets/index-NvuTFZg0.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-DFLADCGY.js"></script>
|
<script type="module" crossorigin src="/assets/index-NvuTFZg0.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BP0dDjYZ.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CzU7MdbY.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
51
openspec/config.yaml
Normal file
51
openspec/config.yaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
|
||||||
|
# Project context (optional)
|
||||||
|
# This is shown to AI when creating artifacts.
|
||||||
|
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
||||||
|
context: |
|
||||||
|
Project: elcsa frontend — a crypto/finance web app (currency converter,
|
||||||
|
wallet, networks, auth) for the ЭКСА platform.
|
||||||
|
|
||||||
|
Tech stack:
|
||||||
|
- React 19 + Vite, TypeScript (strict, tsc -b before build)
|
||||||
|
- TanStack Query (React Query) for server state
|
||||||
|
- React Router for routing
|
||||||
|
- Plain CSS Modules (.module.css) — NO Tailwind, NO CSS-in-JS
|
||||||
|
- Design tokens live in src/app/styles/variables.css
|
||||||
|
|
||||||
|
Architecture — Feature-Sliced Design (FSD). Layers are strictly ordered;
|
||||||
|
a lower layer must never import from a higher one:
|
||||||
|
app → pages → widgets → features → entities → shared
|
||||||
|
Path aliases map to layers: @app/* @pages/* @widgets/* @features/*
|
||||||
|
@entities/* @shared/*. Each widget/feature owns its model/, ui/ and a
|
||||||
|
barrel index.ts.
|
||||||
|
|
||||||
|
Key conventions:
|
||||||
|
- Auth is two-step email+code for both register and login. Access token is
|
||||||
|
kept in an in-memory tokenStore (NOT localStorage); refreshed via an
|
||||||
|
HttpOnly cookie against the auth service. Every request attaches a cached
|
||||||
|
CSRF token and retries once on 401 after a silent refresh.
|
||||||
|
- Reusable UI lives in shared/ui (Button, PrimaryButton, FormField, Pill,
|
||||||
|
Title, TokenIcon, Notification), each with its own CSS Module.
|
||||||
|
- Routes are centralized in shared/config/routes.ts (ROUTES const).
|
||||||
|
ProtectedRoute / GuestRoute guard auth-only and guest-only pages.
|
||||||
|
- VITE_API_URL must be set (src/shared/config/env.ts).
|
||||||
|
- No test runner is configured.
|
||||||
|
|
||||||
|
# Per-artifact rules (optional)
|
||||||
|
# Add custom rules for specific artifacts.
|
||||||
|
rules:
|
||||||
|
proposal:
|
||||||
|
- Keep it concise and focused on user-facing behavior and the "why".
|
||||||
|
- State which FSD layer(s) the change touches and any cross-layer impact.
|
||||||
|
specs:
|
||||||
|
- Write requirements as observable behavior, not implementation details.
|
||||||
|
- Cover the error/edge paths (network failure, 401 refresh, validation).
|
||||||
|
design:
|
||||||
|
- Place new code in the correct FSD layer and justify the placement.
|
||||||
|
- Reuse existing shared/ui components and shared/api helpers before adding new ones.
|
||||||
|
- Note any new env vars, routes (ROUTES const), or React Query keys.
|
||||||
|
tasks:
|
||||||
|
- Break work into small, verifiable steps ordered by FSD dependency (shared → up).
|
||||||
|
- Include a "type-check passes (npm run build)" and "npm run lint" step; there are no automated tests.
|
||||||
@@ -154,6 +154,7 @@ export function BridgeForm() {
|
|||||||
const { data: relayQuote } = useRelayQuote(relayQuotePayload)
|
const { data: relayQuote } = useRelayQuote(relayQuotePayload)
|
||||||
|
|
||||||
const serviceFee = relayQuote?.appCommission?.usd?.toString()
|
const serviceFee = relayQuote?.appCommission?.usd?.toString()
|
||||||
|
const serviceFeeToken = relayQuote?.appCommission?.inToken
|
||||||
|
|
||||||
const displayToAmount = relayQuote
|
const displayToAmount = relayQuote
|
||||||
? relayQuote.details.currencyOut.amountFormatted
|
? relayQuote.details.currencyOut.amountFormatted
|
||||||
@@ -245,7 +246,7 @@ export function BridgeForm() {
|
|||||||
hideNetworkSelect
|
hideNetworkSelect
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BridgeInfoPanel serviceFee={serviceFee} />
|
<BridgeInfoPanel serviceFee={serviceFee} serviceFeeToken={serviceFeeToken} />
|
||||||
|
|
||||||
<PrimaryButton label={isPending ? 'Загрузка...' : 'Подтвердить бридж'} onClick={handleConfirm} disabled={!quotePayload || isPending} />
|
<PrimaryButton label={isPending ? 'Загрузка...' : 'Подтвердить бридж'} onClick={handleConfirm} disabled={!quotePayload || isPending} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
|
import { truncateDecimals } from '@shared/lib/utils/truncateDecimals'
|
||||||
import styles from './BridgeInfoPanel.module.css'
|
import styles from './BridgeInfoPanel.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serviceFee?: string
|
serviceFee?: string
|
||||||
|
serviceFeeToken?: { symbol: string; amount: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BridgeInfoPanel({ serviceFee }: Props) {
|
export function BridgeInfoPanel({ serviceFee, serviceFeeToken }: Props) {
|
||||||
const rows = [
|
const rows = [
|
||||||
{ label: 'СЕРВИСНЫЙ СБОР', value: serviceFee ? `$${serviceFee}` : '—' },
|
{ label: 'СЕРВИСНЫЙ СБОР', value: serviceFee ? `$${serviceFee}` : '—' },
|
||||||
|
{
|
||||||
|
label: serviceFeeToken ? `СЕРВИСНЫЙ СБОР (${serviceFeeToken.symbol})` : 'СЕРВИСНЫЙ СБОР В ТОКЕНАХ',
|
||||||
|
value: serviceFeeToken ? truncateDecimals(serviceFeeToken.amount, 8) : '—',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ export function useSwapForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setPercent(p: number) {
|
function setPercent(p: number) {
|
||||||
setFromAmountRaw((fromToken.balance * p / 100).toFixed(4))
|
// На «МАКС»/100% оставляем запас на комиссию сети — выставляем 99% баланса (молча, без уведомления юзера).
|
||||||
|
const effective = p >= 100 ? 99 : p
|
||||||
|
setFromAmountRaw((fromToken.balance * effective / 100).toFixed(4))
|
||||||
}
|
}
|
||||||
|
|
||||||
function swapTokens() {
|
function swapTokens() {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export function SwapForm() {
|
|||||||
: quoteData?.fees.gas.amountUsd
|
: quoteData?.fees.gas.amountUsd
|
||||||
|
|
||||||
const serviceFee = quoteData?.appCommission?.usd?.toString()
|
const serviceFee = quoteData?.appCommission?.usd?.toString()
|
||||||
|
const serviceFeeToken = quoteData?.appCommission?.inToken
|
||||||
|
|
||||||
const isButtonDisabled = isTrxNetwork
|
const isButtonDisabled = isTrxNetwork
|
||||||
? parsedAmount <= 0 || isFetchingTrxQuote
|
? parsedAmount <= 0 || isFetchingTrxQuote
|
||||||
@@ -155,7 +156,7 @@ export function SwapForm() {
|
|||||||
onTokenChange={setToToken}
|
onTokenChange={setToToken}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SwapInfoPanel gasFee={gasFee} serviceFee={serviceFee} />
|
<SwapInfoPanel gasFee={gasFee} serviceFee={serviceFee} serviceFeeToken={serviceFeeToken} />
|
||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label={isSwapping || isFetchingTrxQuote ? 'Загрузка...' : undefined}
|
label={isSwapping || isFetchingTrxQuote ? 'Загрузка...' : undefined}
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
|
import { truncateDecimals } from '@shared/lib/utils/truncateDecimals'
|
||||||
import styles from './SwapInfoPanel.module.css'
|
import styles from './SwapInfoPanel.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
gasFee?: string
|
gasFee?: string
|
||||||
serviceFee?: string
|
serviceFee?: string
|
||||||
|
serviceFeeToken?: { symbol: string; amount: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SwapInfoPanel({ gasFee, serviceFee }: Props) {
|
export function SwapInfoPanel({ gasFee, serviceFee, serviceFeeToken }: Props) {
|
||||||
const rows = [
|
const rows = [
|
||||||
{ label: 'ПРОВАЙДЕР', value: 'ЛУЧШИЙ', link: false },
|
{ label: 'ПРОВАЙДЕР', value: 'ЛУЧШИЙ', link: false },
|
||||||
{ label: 'СКОЛЬЖЕНИЕ', value: 'АВТО (0.5%)', link: true },
|
{ label: 'СКОЛЬЖЕНИЕ', value: 'АВТО (0.5%)', link: true },
|
||||||
{ label: 'СЕТЕВОЙ СБОР', value: gasFee ? `$${gasFee}` : '—', link: false },
|
{ label: 'СЕТЕВОЙ СБОР', value: gasFee ? `$${gasFee}` : '—', link: false },
|
||||||
{ label: 'СЕРВИСНЫЙ СБОР', value: serviceFee ? `$${serviceFee}` : '—', link: false },
|
{ label: 'СЕРВИСНЫЙ СБОР', value: serviceFee ? `$${serviceFee}` : '—', link: false },
|
||||||
|
{
|
||||||
|
label: serviceFeeToken ? `СЕРВИСНЫЙ СБОР (${serviceFeeToken.symbol})` : 'СЕРВИСНЫЙ СБОР В ТОКЕНАХ',
|
||||||
|
value: serviceFeeToken ? truncateDecimals(serviceFeeToken.amount, 8) : '—',
|
||||||
|
link: false,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user