This commit is contained in:
2026-05-22 23:11:09 +03:00
parent aa25c6dec5
commit 96ea3788d5
9 changed files with 229 additions and 161 deletions

File diff suppressed because one or more lines are too long

153
dist/assets/index-DhHOsmUC.js vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/index.html vendored
View File

@@ -5,7 +5,7 @@
<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-BSmqh004.js"></script> <script type="module" crossorigin src="/assets/index-DhHOsmUC.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CbqAOC8U.css"> <link rel="stylesheet" crossorigin href="/assets/index-CbqAOC8U.css">
</head> </head>
<body> <body>

View File

@@ -90,6 +90,25 @@ export async function passwordResetStart(payload: PasswordResetStartPayload): Pr
} }
} }
export async function updatePhone(phone: string): Promise<void> {
const headers = await authedHeaders()
const res = await fetch(`${USERS_API_URL}/me/settings/phone`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify({ phone }),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw data
}
}
export interface PasswordResetCompletePayload { export interface PasswordResetCompletePayload {
email: string email: string
code: string code: string

View File

@@ -0,0 +1,12 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { updatePhone } from '../api/profileApi'
export function useUpdatePhone() {
const queryClient = useQueryClient()
return useMutation<void, unknown, string>({
mutationFn: updatePhone,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['me'] })
},
})
}

View File

@@ -1,8 +1,9 @@
export { registrationStart, registrationComplete, loginStart, loginComplete } from './api/registrationApi' export { registrationStart, registrationComplete, loginStart, loginComplete } from './api/registrationApi'
export { getMe, uploadAvatar } from './api/profileApi' export { getMe, uploadAvatar, updatePhone } from './api/profileApi'
export type { MeResponse, UploadAvatarPayload } from './api/profileApi' export type { MeResponse, UploadAvatarPayload } from './api/profileApi'
export { useMe } from './hooks/useMe' export { useMe } from './hooks/useMe'
export { useUploadAvatar } from './hooks/useUploadAvatar' export { useUploadAvatar } from './hooks/useUploadAvatar'
export { useUpdatePhone } from './hooks/useUpdatePhone'
export type { RegistrationStartPayload, RegistrationCompletePayload, LoginStartPayload, LoginCompletePayload, AuthResponse } from './api/registrationApi' export type { RegistrationStartPayload, RegistrationCompletePayload, LoginStartPayload, LoginCompletePayload, AuthResponse } from './api/registrationApi'
export { useIsAuthenticated } from './hooks/useIsAuthenticated' export { useIsAuthenticated } from './hooks/useIsAuthenticated'
export { useAuth, AUTH_QUERY_KEY } from './hooks/useAuth' export { useAuth, AUTH_QUERY_KEY } from './hooks/useAuth'

View File

@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useMe } from '@features/auth' import { useMe, useUpdatePhone } from '@features/auth'
import { usePortfolio, useWalletAddresses } from '@features/wallet' import { usePortfolio, useWalletAddresses } from '@features/wallet'
import { ROUTES } from '@shared/config/routes' import { ROUTES } from '@shared/config/routes'
import { Button, FormField } from '@shared/ui' import { Button, FormField, Notification } from '@shared/ui'
import { WalletHeader } from '@widgets/wallet-header' import { WalletHeader } from '@widgets/wallet-header'
import { ProfileAvatar, ProfileSection } from '@widgets/profile' import { ProfileAvatar, ProfileSection } from '@widgets/profile'
import styles from './ProfilePage.module.css' import styles from './ProfilePage.module.css'
@@ -11,8 +12,34 @@ export function ProfilePage() {
const { data } = useMe() const { data } = useMe()
const { data: portfolio, isLoading: isPortfolioLoading } = usePortfolio() const { data: portfolio, isLoading: isPortfolioLoading } = usePortfolio()
const { data: walletAddresses } = useWalletAddresses() const { data: walletAddresses } = useWalletAddresses()
const updatePhone = useUpdatePhone()
const navigate = useNavigate() const navigate = useNavigate()
const [phone, setPhone] = useState('')
const [savedPhone, setSavedPhone] = useState('')
const [notification, setNotification] = useState<{ message: string; status: 'success' | 'error' } | null>(null)
useEffect(() => {
if (data?.phone != null) {
setPhone(data.phone)
setSavedPhone(data.phone)
}
}, [data?.phone])
function handlePhoneBlur() {
const next = phone.trim()
if (next === savedPhone || updatePhone.isPending) return
updatePhone.mutate(next, {
onSuccess: () => {
setSavedPhone(next)
setNotification({ status: 'success', message: 'Номер телефона обновлён' })
},
onError: () => {
setNotification({ status: 'error', message: 'Не удалось обновить номер телефона' })
},
})
}
const capitalize = (s: string) => (s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : '') const capitalize = (s: string) => (s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : '')
const fullName = data const fullName = data
? [data.last_name, data.first_name, data.middle_name].filter(Boolean).map(capitalize).join(' ') ? [data.last_name, data.first_name, data.middle_name].filter(Boolean).map(capitalize).join(' ')
@@ -44,10 +71,10 @@ export function ProfilePage() {
<div className={styles.sections}> <div className={styles.sections}>
<ProfileSection title="Личные данные"> <ProfileSection title="Личные данные">
<div className={styles.grid2}> <div className={styles.grid2}>
<FormField label="Полное ФИО" value={fullName} placeholder="Например: Иванов Иван Иванович" /> <FormField label="Полное ФИО" value={fullName} placeholder="Например: Иванов Иван Иванович" readOnly />
<FormField label="Адрес электронной почты" value={data?.email ?? ''} type="email" icon="check" placeholder="example@mail.ru" readOnly /> <FormField label="Адрес электронной почты" value={data?.email ?? ''} type="email" icon="check" placeholder="example@mail.ru" readOnly />
<FormField label="Серия и номер паспорта" value={data?.passport_data ?? ''} placeholder="0000 000000" readOnly /> <FormField label="Серия и номер паспорта" value={data?.passport_data ?? ''} placeholder="0000 000000" readOnly />
<FormField label="Номер телефона" value={data?.phone ?? ''} type="tel" icon="check" placeholder="+7 (999) 000-00-00" readOnly /> <FormField label="Номер телефона" value={phone} onChange={setPhone} onBlur={handlePhoneBlur} type="tel" placeholder="+7 (999) 000-00-00" />
</div> </div>
</ProfileSection> </ProfileSection>
@@ -92,6 +119,13 @@ export function ProfilePage() {
</ProfileSection> </ProfileSection>
</div> </div>
</main> </main>
{notification && (
<Notification
status={notification.status}
message={notification.message}
onClose={() => setNotification(null)}
/>
)}
</div> </div>
) )
} }

View File

@@ -7,12 +7,13 @@ interface Props {
placeholder?: string placeholder?: string
type?: 'text' | 'email' | 'tel' | 'password' type?: 'text' | 'email' | 'tel' | 'password'
onChange?: (value: string) => void onChange?: (value: string) => void
onBlur?: () => void
readOnly?: boolean readOnly?: boolean
required?: boolean required?: boolean
icon?: 'check' | 'lock' icon?: 'check' | 'lock'
} }
export function FormField({ label, value, placeholder, type = 'text', onChange, readOnly, required, icon }: Props) { export function FormField({ label, value, placeholder, type = 'text', onChange, onBlur, readOnly, required, icon }: Props) {
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [isVisible, setIsVisible] = useState(false) const [isVisible, setIsVisible] = useState(false)
@@ -41,6 +42,7 @@ export function FormField({ label, value, placeholder, type = 'text', onChange,
placeholder={placeholder} placeholder={placeholder}
readOnly={readOnly} readOnly={readOnly}
required={required} required={required}
onBlur={onBlur}
/> />
{isPassword && ( {isPassword && (
<button <button

File diff suppressed because one or more lines are too long