F
This commit is contained in:
153
dist/assets/index-BSmqh004.js
vendored
153
dist/assets/index-BSmqh004.js
vendored
File diff suppressed because one or more lines are too long
153
dist/assets/index-DhHOsmUC.js
vendored
Normal file
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
2
dist/index.html
vendored
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
12
src/features/auth/hooks/useUpdatePhone.ts
Normal file
12
src/features/auth/hooks/useUpdatePhone.ts
Normal 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'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user