From bd3d747edef2c845d221ed99a6cbf820ff348cd8 Mon Sep 17 00:00:00 2001 From: rassadin11 Date: Sun, 17 May 2026 13:06:18 +0300 Subject: [PATCH] 17.05.2026 funny --- src/features/auth/api/profileApi.ts | 43 ++++++++++-- src/features/auth/hooks/useUploadAvatar.ts | 13 ++++ src/features/auth/index.ts | 5 +- .../ui/ConverterSection.module.css | 2 +- .../converter-page/ui/ConverterSection.tsx | 9 --- .../ui/Converter.module.css | 23 ++++++- .../currency-converter/ui/Converter.tsx | 18 ++--- .../profile/ui/ProfileAvatar.module.css | 13 ++++ src/widgets/profile/ui/ProfileAvatar.tsx | 68 +++++++++++++++++-- tsconfig.tsbuildinfo | 2 +- 10 files changed, 157 insertions(+), 39 deletions(-) create mode 100644 src/features/auth/hooks/useUploadAvatar.ts diff --git a/src/features/auth/api/profileApi.ts b/src/features/auth/api/profileApi.ts index 23e7177..703e9c8 100644 --- a/src/features/auth/api/profileApi.ts +++ b/src/features/auth/api/profileApi.ts @@ -1,4 +1,5 @@ import { getCsrfToken } from '@shared/api/csrf' +import { tokenStore } from '@shared/api/tokenStore' const USERS_API_URL = 'https://app.users.elcsa.ru' @@ -9,26 +10,58 @@ export interface MeResponse { middle_name: string last_name: string birth_date: string - crypto_wallet: string | null + encrypted_mnemonic: string | null phone: string passport_data: string | null inn: string | null erc20: string | null + avatar_link: string | null kyc_verified: boolean is_deleted: boolean created_at: string updated_at: string kyc_verified_at: string | null + webp_size_bytes: number +} + +export interface UploadAvatarPayload { + photo_base64: string + decoded_bytes: string +} + +async function authedHeaders(): Promise { + const csrf = await getCsrfToken() + const bearer = tokenStore.get() + return { + 'X-CSRF-Token': csrf, + ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}), + } } export async function getMe(): Promise { - const csrf = await getCsrfToken() + const headers = await authedHeaders() const res = await fetch(`${USERS_API_URL}/me/`, { credentials: 'include', - headers: { - 'X-CSRF-Token': csrf, - }, + headers, + }) + + const data = await res.json() + if (!res.ok) throw data + return data +} + +export async function uploadAvatar(payload: UploadAvatarPayload): Promise { + const headers = await authedHeaders() + + const res = await fetch(`${USERS_API_URL}/me/settings/avatar`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + body: JSON.stringify(payload), }) const data = await res.json() diff --git a/src/features/auth/hooks/useUploadAvatar.ts b/src/features/auth/hooks/useUploadAvatar.ts new file mode 100644 index 0000000..99ae1de --- /dev/null +++ b/src/features/auth/hooks/useUploadAvatar.ts @@ -0,0 +1,13 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { uploadAvatar } from '../api/profileApi' +import type { MeResponse, UploadAvatarPayload } from '../api/profileApi' + +export function useUploadAvatar() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: uploadAvatar, + onSuccess: (data) => { + queryClient.setQueryData(['me'], data) + }, + }) +} diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts index 4f9ffa5..16c597c 100644 --- a/src/features/auth/index.ts +++ b/src/features/auth/index.ts @@ -1,7 +1,8 @@ export { registrationStart, registrationComplete, loginStart, loginComplete } from './api/registrationApi' -export { getMe } from './api/profileApi' -export type { MeResponse } from './api/profileApi' +export { getMe, uploadAvatar } from './api/profileApi' +export type { MeResponse, UploadAvatarPayload } from './api/profileApi' export { useMe } from './hooks/useMe' +export { useUploadAvatar } from './hooks/useUploadAvatar' export type { RegistrationStartPayload, RegistrationCompletePayload, LoginStartPayload, LoginCompletePayload, AuthResponse } from './api/registrationApi' export { useIsAuthenticated } from './hooks/useIsAuthenticated' export { useAuth, AUTH_QUERY_KEY } from './hooks/useAuth' diff --git a/src/widgets/converter-page/ui/ConverterSection.module.css b/src/widgets/converter-page/ui/ConverterSection.module.css index 641de5e..9bf3c83 100644 --- a/src/widgets/converter-page/ui/ConverterSection.module.css +++ b/src/widgets/converter-page/ui/ConverterSection.module.css @@ -221,7 +221,7 @@ } .tab { - flex: 0 0 50%; + flex: 1 1 0; } .field { diff --git a/src/widgets/converter-page/ui/ConverterSection.tsx b/src/widgets/converter-page/ui/ConverterSection.tsx index 778fe2e..7f9984c 100644 --- a/src/widgets/converter-page/ui/ConverterSection.tsx +++ b/src/widgets/converter-page/ui/ConverterSection.tsx @@ -61,15 +61,6 @@ export function ConverterSection() { > КУПИТЬ - {/* */}
diff --git a/src/widgets/currency-converter/ui/Converter.module.css b/src/widgets/currency-converter/ui/Converter.module.css index 2497332..59f89c4 100644 --- a/src/widgets/currency-converter/ui/Converter.module.css +++ b/src/widgets/currency-converter/ui/Converter.module.css @@ -197,6 +197,27 @@ border-top: 1px solid var(--glass-border); } +.payBtn { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + margin-top: 32px; + padding: 18px; + border-radius: 12px; + background: var(--grad-center); + color: var(--text-primary); + font-size: 16px; + font-weight: 600; + letter-spacing: 1px; + text-decoration: none; + transition: opacity 0.2s; +} + +.payBtn:hover { + opacity: 0.9; +} + @media (max-width: 1024px) { .body { grid-template-columns: 1fr; @@ -217,7 +238,7 @@ } .tab { - flex: 0 0 50%; + flex: 1 1 0; } .field { diff --git a/src/widgets/currency-converter/ui/Converter.tsx b/src/widgets/currency-converter/ui/Converter.tsx index a6d1399..da96604 100644 --- a/src/widgets/currency-converter/ui/Converter.tsx +++ b/src/widgets/currency-converter/ui/Converter.tsx @@ -3,10 +3,11 @@ import { useConverter } from '../model/useConverter' import { progressPercent } from '../model/tiers' import { usePaymentConfig, usePaymentQuote } from '@features/payment' import { useDebounce } from '@shared/lib/hooks/useDebounce' -import { AgreementCheckbox } from './AgreementCheckbox' import { CommissionTable } from './CommissionTable' import styles from './Converter.module.css' import { Title } from '@shared/ui/Title/Title' +import { Link } from 'react-router-dom' +import { ROUTES } from '@shared/config/routes' export function Converter() { const { data: config } = usePaymentConfig() @@ -54,15 +55,6 @@ export function Converter() { > КУПИТЬ -
@@ -116,9 +108,9 @@ export function Converter() { />
-
- c.setAgreed(!c.agreed)} /> -
+ + Перейти к оплате + ) diff --git a/src/widgets/profile/ui/ProfileAvatar.module.css b/src/widgets/profile/ui/ProfileAvatar.module.css index ee62bdb..8a6ae1f 100644 --- a/src/widgets/profile/ui/ProfileAvatar.module.css +++ b/src/widgets/profile/ui/ProfileAvatar.module.css @@ -27,6 +27,19 @@ height: 54px; } +.avatarImg { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.error { + color: var(--danger, #ff5252); + font-size: 12px; + text-align: center; +} + .overlay { position: absolute; inset: 0; diff --git a/src/widgets/profile/ui/ProfileAvatar.tsx b/src/widgets/profile/ui/ProfileAvatar.tsx index 0359d2b..4d37175 100644 --- a/src/widgets/profile/ui/ProfileAvatar.tsx +++ b/src/widgets/profile/ui/ProfileAvatar.tsx @@ -1,14 +1,59 @@ +import { useRef, useState } from 'react' import { Button } from '@shared/ui' +import { useMe, useUploadAvatar } from '@features/auth' import styles from './ProfileAvatar.module.css' +function fileToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + const result = reader.result as string + const comma = result.indexOf(',') + resolve(comma >= 0 ? result.slice(comma + 1) : result) + } + reader.onerror = () => reject(reader.error) + reader.readAsDataURL(file) + }) +} + export function ProfileAvatar() { + const { data } = useMe() + const { mutateAsync: upload, isPending } = useUploadAvatar() + const inputRef = useRef(null) + const [error, setError] = useState(null) + + const avatarLink = data?.avatar_link ?? null + + const openPicker = () => { + if (isPending) return + inputRef.current?.click() + } + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + e.target.value = '' + if (!file) return + + setError(null) + try { + const photo_base64 = await fileToBase64(file) + await upload({ photo_base64, decoded_bytes: String(file.size) }) + } catch { + setError('Не удалось загрузить фото') + } + } + return (
-
- - - - +
+ {avatarLink ? ( + avatar + ) : ( + + + + + )}
@@ -16,10 +61,19 @@ export function ProfileAvatar() {
+
- +
- + {error && {error}}
) } diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 8659e20..37690e1 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/app.tsx","./src/app/providers/guestroute.tsx","./src/app/providers/protectedroute.tsx","./src/app/providers/queryprovider.tsx","./src/app/providers/routerprovider.tsx","./src/app/providers/scrolltotop.tsx","./src/app/providers/index.ts","./src/features/auth/index.ts","./src/features/auth/api/profileapi.ts","./src/features/auth/api/registrationapi.ts","./src/features/auth/hooks/useauth.ts","./src/features/auth/hooks/useisauthenticated.ts","./src/features/auth/hooks/useme.ts","./src/features/kyc/api/kycapi.ts","./src/features/payment/index.ts","./src/features/payment/api/paymentapi.ts","./src/features/payment/hooks/usecreateorder.ts","./src/features/payment/hooks/usepaymentconfig.ts","./src/features/payment/hooks/usepaymentquote.ts","./src/features/wallet/index.ts","./src/features/wallet/api/walletapi.ts","./src/features/wallet/model/usewalletdata.ts","./src/pages/bridge/index.ts","./src/pages/bridge/ui/bridgepage.tsx","./src/pages/converter/index.ts","./src/pages/converter/ui/converterpage.tsx","./src/pages/home/index.ts","./src/pages/home/ui/homepage.tsx","./src/pages/kyc/index.ts","./src/pages/kyc/ui/kycpage.tsx","./src/pages/login/index.ts","./src/pages/login/ui/loginpage.tsx","./src/pages/profile/index.ts","./src/pages/profile/ui/profilepage.tsx","./src/pages/register/index.ts","./src/pages/register/ui/registerpage.tsx","./src/pages/seed-phrase/index.ts","./src/pages/seed-phrase/ui/seedphrasepage.tsx","./src/pages/swap/index.ts","./src/pages/swap/ui/swappage.tsx","./src/pages/wallet/index.ts","./src/pages/wallet/ui/walletpage.tsx","./src/shared/api/base.ts","./src/shared/api/csrf.ts","./src/shared/api/tokenstore.ts","./src/shared/api/types.ts","./src/shared/config/constants.ts","./src/shared/config/env.ts","./src/shared/config/routes.ts","./src/shared/lib/hooks/usedebounce.ts","./src/shared/lib/hooks/uselocalstorage.ts","./src/shared/lib/utils/cn.ts","./src/shared/types/index.ts","./src/shared/ui/index.ts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/formfield/formfield.tsx","./src/shared/ui/formfield/index.ts","./src/shared/ui/notification/notification.tsx","./src/shared/ui/notification/index.ts","./src/shared/ui/pill/pill.tsx","./src/shared/ui/pill/index.ts","./src/shared/ui/primarybutton/primarybutton.tsx","./src/shared/ui/primarybutton/index.ts","./src/shared/ui/title/title.tsx","./src/shared/ui/tokenicon/tokenicon.tsx","./src/shared/ui/tokenicon/index.ts","./src/widgets/about/index.ts","./src/widgets/about/ui/about.tsx","./src/widgets/balance-card/index.ts","./src/widgets/balance-card/ui/balancecard.tsx","./src/widgets/bridge-form/index.ts","./src/widgets/bridge-form/ui/bridgeform.tsx","./src/widgets/bridge-form/ui/networkselect.tsx","./src/widgets/converter-page/index.ts","./src/widgets/converter-page/ui/agreementcheck.tsx","./src/widgets/converter-page/ui/commissionpanel.tsx","./src/widgets/converter-page/ui/convertersection.tsx","./src/widgets/currency-converter/index.ts","./src/widgets/currency-converter/model/tiers.ts","./src/widgets/currency-converter/model/useconverter.ts","./src/widgets/currency-converter/ui/agreementcheckbox.tsx","./src/widgets/currency-converter/ui/commissiontable.tsx","./src/widgets/currency-converter/ui/converter.tsx","./src/widgets/currency-converter/ui/tiers.tsx","./src/widgets/footer/index.ts","./src/widgets/footer/ui/footer.tsx","./src/widgets/header/index.ts","./src/widgets/header/ui/header.tsx","./src/widgets/hero/index.ts","./src/widgets/hero/lib/usecountdown.ts","./src/widgets/hero/ui/conversionflow.tsx","./src/widgets/hero/ui/countdown.tsx","./src/widgets/hero/ui/exchangecard.tsx","./src/widgets/hero/ui/hero.tsx","./src/widgets/kyc-verification/index.ts","./src/widgets/kyc-verification/model/usekyc.ts","./src/widgets/kyc-verification/ui/kycmodal.tsx","./src/widgets/kyc-verification/ui/kycwidget.tsx","./src/widgets/login-form/index.ts","./src/widgets/login-form/model/useloginform.ts","./src/widgets/login-form/ui/loginform.tsx","./src/widgets/networks-table/index.ts","./src/widgets/networks-table/model/networks.ts","./src/widgets/networks-table/ui/networkstable.tsx","./src/widgets/profile/index.ts","./src/widgets/profile/ui/profileavatar.tsx","./src/widgets/profile/ui/profilesection.tsx","./src/widgets/receive-modal/index.ts","./src/widgets/receive-modal/ui/receivemodal.tsx","./src/widgets/register-form/index.ts","./src/widgets/register-form/model/useregisterform.ts","./src/widgets/register-form/ui/registerform.tsx","./src/widgets/seed-phrase/index.ts","./src/widgets/seed-phrase/model/useseedphrase.ts","./src/widgets/seed-phrase/ui/seedphrasewidget.tsx","./src/widgets/send-modal/index.ts","./src/widgets/send-modal/model/sendtypes.ts","./src/widgets/send-modal/ui/sendmodal.tsx","./src/widgets/swap-form/index.ts","./src/widgets/swap-form/model/useswapform.ts","./src/widgets/swap-form/ui/raterow.tsx","./src/widgets/swap-form/ui/swapcard.tsx","./src/widgets/swap-form/ui/swapconfirmmodal.tsx","./src/widgets/swap-form/ui/swapdirectionbutton.tsx","./src/widgets/swap-form/ui/swapform.tsx","./src/widgets/swap-form/ui/swapinfopanel.tsx","./src/widgets/swap-form/ui/tokenselect.tsx","./src/widgets/swap-form/ui/trxconfirmmodal.tsx","./src/widgets/token-table/index.ts","./src/widgets/token-table/model/tokens.ts","./src/widgets/token-table/model/usetokenrows.ts","./src/widgets/token-table/ui/tokentable.tsx","./src/widgets/wallet-header/index.ts","./src/widgets/wallet-header/ui/walletheader.tsx"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/app.tsx","./src/app/providers/guestroute.tsx","./src/app/providers/protectedroute.tsx","./src/app/providers/queryprovider.tsx","./src/app/providers/routerprovider.tsx","./src/app/providers/scrolltotop.tsx","./src/app/providers/index.ts","./src/features/auth/index.ts","./src/features/auth/api/profileapi.ts","./src/features/auth/api/registrationapi.ts","./src/features/auth/hooks/useauth.ts","./src/features/auth/hooks/useisauthenticated.ts","./src/features/auth/hooks/useme.ts","./src/features/auth/hooks/useuploadavatar.ts","./src/features/kyc/api/kycapi.ts","./src/features/payment/index.ts","./src/features/payment/api/paymentapi.ts","./src/features/payment/hooks/usecreateorder.ts","./src/features/payment/hooks/usepaymentconfig.ts","./src/features/payment/hooks/usepaymentquote.ts","./src/features/wallet/index.ts","./src/features/wallet/api/walletapi.ts","./src/features/wallet/model/usewalletdata.ts","./src/pages/bridge/index.ts","./src/pages/bridge/ui/bridgepage.tsx","./src/pages/converter/index.ts","./src/pages/converter/ui/converterpage.tsx","./src/pages/home/index.ts","./src/pages/home/ui/homepage.tsx","./src/pages/kyc/index.ts","./src/pages/kyc/ui/kycpage.tsx","./src/pages/login/index.ts","./src/pages/login/ui/loginpage.tsx","./src/pages/profile/index.ts","./src/pages/profile/ui/profilepage.tsx","./src/pages/register/index.ts","./src/pages/register/ui/registerpage.tsx","./src/pages/seed-phrase/index.ts","./src/pages/seed-phrase/ui/seedphrasepage.tsx","./src/pages/swap/index.ts","./src/pages/swap/ui/swappage.tsx","./src/pages/wallet/index.ts","./src/pages/wallet/ui/walletpage.tsx","./src/shared/api/base.ts","./src/shared/api/csrf.ts","./src/shared/api/tokenstore.ts","./src/shared/api/types.ts","./src/shared/config/constants.ts","./src/shared/config/env.ts","./src/shared/config/routes.ts","./src/shared/lib/hooks/usedebounce.ts","./src/shared/lib/hooks/uselocalstorage.ts","./src/shared/lib/utils/cn.ts","./src/shared/types/index.ts","./src/shared/ui/index.ts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/formfield/formfield.tsx","./src/shared/ui/formfield/index.ts","./src/shared/ui/notification/notification.tsx","./src/shared/ui/notification/index.ts","./src/shared/ui/pill/pill.tsx","./src/shared/ui/pill/index.ts","./src/shared/ui/primarybutton/primarybutton.tsx","./src/shared/ui/primarybutton/index.ts","./src/shared/ui/title/title.tsx","./src/shared/ui/tokenicon/tokenicon.tsx","./src/shared/ui/tokenicon/index.ts","./src/widgets/about/index.ts","./src/widgets/about/ui/about.tsx","./src/widgets/balance-card/index.ts","./src/widgets/balance-card/ui/balancecard.tsx","./src/widgets/bridge-form/index.ts","./src/widgets/bridge-form/ui/bridgeform.tsx","./src/widgets/bridge-form/ui/networkselect.tsx","./src/widgets/converter-page/index.ts","./src/widgets/converter-page/ui/agreementcheck.tsx","./src/widgets/converter-page/ui/commissionpanel.tsx","./src/widgets/converter-page/ui/convertersection.tsx","./src/widgets/currency-converter/index.ts","./src/widgets/currency-converter/model/tiers.ts","./src/widgets/currency-converter/model/useconverter.ts","./src/widgets/currency-converter/ui/agreementcheckbox.tsx","./src/widgets/currency-converter/ui/commissiontable.tsx","./src/widgets/currency-converter/ui/converter.tsx","./src/widgets/currency-converter/ui/tiers.tsx","./src/widgets/footer/index.ts","./src/widgets/footer/ui/footer.tsx","./src/widgets/header/index.ts","./src/widgets/header/ui/header.tsx","./src/widgets/hero/index.ts","./src/widgets/hero/lib/usecountdown.ts","./src/widgets/hero/ui/conversionflow.tsx","./src/widgets/hero/ui/countdown.tsx","./src/widgets/hero/ui/exchangecard.tsx","./src/widgets/hero/ui/hero.tsx","./src/widgets/kyc-verification/index.ts","./src/widgets/kyc-verification/model/usekyc.ts","./src/widgets/kyc-verification/ui/kycmodal.tsx","./src/widgets/kyc-verification/ui/kycwidget.tsx","./src/widgets/login-form/index.ts","./src/widgets/login-form/model/useloginform.ts","./src/widgets/login-form/ui/loginform.tsx","./src/widgets/networks-table/index.ts","./src/widgets/networks-table/model/networks.ts","./src/widgets/networks-table/ui/networkstable.tsx","./src/widgets/profile/index.ts","./src/widgets/profile/ui/profileavatar.tsx","./src/widgets/profile/ui/profilesection.tsx","./src/widgets/receive-modal/index.ts","./src/widgets/receive-modal/ui/receivemodal.tsx","./src/widgets/register-form/index.ts","./src/widgets/register-form/model/useregisterform.ts","./src/widgets/register-form/ui/registerform.tsx","./src/widgets/seed-phrase/index.ts","./src/widgets/seed-phrase/model/useseedphrase.ts","./src/widgets/seed-phrase/ui/seedphrasewidget.tsx","./src/widgets/send-modal/index.ts","./src/widgets/send-modal/model/sendtypes.ts","./src/widgets/send-modal/ui/sendmodal.tsx","./src/widgets/swap-form/index.ts","./src/widgets/swap-form/model/useswapform.ts","./src/widgets/swap-form/ui/raterow.tsx","./src/widgets/swap-form/ui/swapcard.tsx","./src/widgets/swap-form/ui/swapconfirmmodal.tsx","./src/widgets/swap-form/ui/swapdirectionbutton.tsx","./src/widgets/swap-form/ui/swapform.tsx","./src/widgets/swap-form/ui/swapinfopanel.tsx","./src/widgets/swap-form/ui/tokenselect.tsx","./src/widgets/swap-form/ui/trxconfirmmodal.tsx","./src/widgets/token-table/index.ts","./src/widgets/token-table/model/tokens.ts","./src/widgets/token-table/model/usetokenrows.ts","./src/widgets/token-table/ui/tokentable.tsx","./src/widgets/wallet-header/index.ts","./src/widgets/wallet-header/ui/walletheader.tsx"],"version":"5.6.3"} \ No newline at end of file