diff --git a/src/App.tsx b/src/App.tsx index 21371ee..04360f5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,12 @@ import { CssBaseline } from '@mui/material'; import './App.css'; import { LanguageManager } from './components/LanguageManager'; -import { Settings } from './features/profile/routes/SettingPage'; - function App() { return ( <> - ); } diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx deleted file mode 100644 index 2672a02..0000000 --- a/src/components/Toast.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Alert, Snackbar, type AlertColor } from '@mui/material'; - -import { type PropsWithChildren } from 'react'; - -export interface ToastProps extends PropsWithChildren { - color: AlertColor | undefined; - open: boolean; - onClose: () => void; -} - -export const Toast = ({ color, open, onClose, children }: ToastProps) => { - return ( - - - {children} - - - ); -}; diff --git a/src/features/profile/api/settingsApi.ts b/src/features/profile/api/settingsApi.ts index 0f56237..3e86b44 100644 --- a/src/features/profile/api/settingsApi.ts +++ b/src/features/profile/api/settingsApi.ts @@ -1,22 +1,23 @@ -import axios, { isAxiosError } from 'axios'; import apiClient from '@/lib/apiClient'; import { type GetProfileApiResponse, type SaveProfileApiResponse, - type TokenApiResponse, type PhoneNumberApiResponse, type ConfirmPhoneNumberApiResponse, + type SaveSettingsApiResponse, + type DeleteSessionsApiResponse, + type PasswordApiResponse, + type SendEmailCodeApiResponse, + type ConfirmEmailCodeApiResponse, + type ChangeEmailApiResponse, } from '../types/settingsApiType'; import { type InfoRowData } from '../types/settingsType'; -const storedToken = () => localStorage.getItem('authToken'); - export async function fetchProfile(): Promise<{ data: GetProfileApiResponse }> { const res = await apiClient.post( '/Profile/GetProfile', {}, - { headers: { Authorization: `Bearer ${storedToken()}` } }, ); return { data: res.data }; } @@ -28,56 +29,13 @@ export async function saveProfile(payload?: { if (!payload) { throw new Error('Payload for saving profile is missing.'); } - const res = await apiClient.post( '/Profile/SaveProfilePersonalInforamtion', payload, - { headers: { Authorization: `Bearer ${storedToken()}` } }, ); return { data: res.data }; } -export async function getToken(): Promise<{ - data: { success: boolean; message?: string }; -}> { - const apiUrl = 'https://accounts.business-harmony.com'; - const tokenEndpoint = `${apiUrl}/connect/token`; - - try { - const body = new URLSearchParams(); - body.set('grant_type', 'password'); - body.set('username', 'zareian.1381@gmail.com'); - body.set('password', '123@Qweasd'); - body.set('client_id', 'harmony_identity'); - body.set('scope', 'openid harmony_identity profile offline_access'); - - const response = await axios.post( - tokenEndpoint, - body.toString(), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }, - ); - - if (response.data?.access_token) { - localStorage.setItem('authToken', response.data.access_token); - return { data: { success: true } }; - } else { - throw new Error('No access token in response'); - } - } catch (error: unknown) { - let message = 'Failed to get token'; - if (isAxiosError(error) && error.response) { - message = `Request failed with status ${error.response.status}`; - } else if (error instanceof Error) { - message = error.message; - } - throw new Error(message); - } -} - export async function sendVerificationCode(payload?: { phoneNumber: string; }): Promise<{ data: PhoneNumberApiResponse }> { @@ -87,7 +45,6 @@ export async function sendVerificationCode(payload?: { const res = await apiClient.post( '/Profile/SendVerfiyPhoneNumberCode', payload, - { headers: { Authorization: `Bearer ${storedToken()}` } }, ); return { data: res.data }; } @@ -102,7 +59,6 @@ export async function confirmPhoneNumberCode(payload?: { const res = await apiClient.post( '/Profile/ConfirmPhoneNumberChangeCode', payload, - { headers: { Authorization: `Bearer ${storedToken()}` } }, ); return { data: res.data }; } @@ -116,7 +72,102 @@ export async function changePhoneNumber(payload?: { const res = await apiClient.post( '/Profile/ChangePhoneNumber', payload, - { headers: { Authorization: `Bearer ${storedToken()}` } }, + ); + return { data: res.data }; +} + +export async function saveSettings(payload?: { + theme: number; + calendarType: number; + language: number; +}): Promise<{ data: SaveSettingsApiResponse }> { + if (!payload) { + throw new Error('Payload for saving settings is missing.'); + } + const res = await apiClient.post( + '/Profile/SaveSetting', + payload, + ); + return { data: res.data }; +} + +export async function deleteSessions(payload?: { + keys: string[]; +}): Promise<{ data: DeleteSessionsApiResponse }> { + if (!payload || payload.keys.length === 0) { + throw new Error('Payload with session keys is missing or empty.'); + } + const res = await apiClient.post( + '/Profile/DeleteSessions', + payload, + ); + return { data: res.data }; +} + +export async function addPassword(payload?: { + password: string; +}): Promise<{ data: PasswordApiResponse }> { + if (!payload) { + throw new Error('Payload for adding password is missing.'); + } + const res = await apiClient.post( + '/Profile/AddPassword', + payload, + ); + return { data: res.data }; +} + +export async function changePassword(payload?: { + oldPassword: string; + newPassword: string; +}): Promise<{ data: PasswordApiResponse }> { + if (!payload) { + throw new Error('Payload for changing password is missing.'); + } + const res = await apiClient.post( + '/Profile/ChangePassword', + payload, + ); + return { data: res.data }; +} + +// ✅ NEW FUNCTIONS +export async function sendEmailCode(payload?: { + email: string; +}): Promise<{ data: SendEmailCodeApiResponse }> { + if (!payload) { + throw new Error('Payload for sending email code is missing.'); + } + const res = await apiClient.post( + 'Profile/SendEmailChangeCode', + payload, + ); + return { data: res.data }; +} + +export async function confirmEmailCode(payload?: { + email: string; + verifyCode: string; +}): Promise<{ data: ConfirmEmailCodeApiResponse }> { + if (!payload) { + throw new Error('Payload for confirming email code is missing.'); + } + const res = await apiClient.post( + 'Profile/ConfirmEmailChangeCode', + payload, + ); + return { data: res.data }; +} + +export async function changeEmail(payload?: { + email: string; +}): Promise<{ data: ChangeEmailApiResponse }> { + if (!payload) { + throw new Error('Payload for changing email is missing.'); + } + const res = await apiClient.post( + 'Profile/ChangeEmail', + payload, ); return { data: res.data }; } diff --git a/src/features/profile/components/activeDevices/ActiveDevices.tsx b/src/features/profile/components/activeDevices/ActiveDevices.tsx index 7590ef7..47cce7e 100644 --- a/src/features/profile/components/activeDevices/ActiveDevices.tsx +++ b/src/features/profile/components/activeDevices/ActiveDevices.tsx @@ -8,173 +8,72 @@ import { CircularProgress, } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import type { TFunction } from 'i18next'; import { DeviceMessage, Logout } from 'iconsax-react'; import { CardContainer } from '@/components/CardContainer'; import { PageWrapper } from '../PageWrapper'; import { Icon } from '@rkheftan/harmony-ui'; -import apiClient from '@/lib/apiClient'; -import { toLocaleDigits } from '@/utils/persianDigit'; - -function formatSessionDate( - isoDate: string, - lang: string, - t: TFunction, -): string { - const date = new Date(isoDate); - const now = new Date(); - const diffInMinutes = Math.floor( - (now.getTime() - date.getTime()) / (1000 * 60), - ); - - if (diffInMinutes < 1) { - return t('active.justNow'); - } - if (diffInMinutes < 60) { - const minuteString = t('active.minutesAgo', { count: diffInMinutes }); - return toLocaleDigits(minuteString, lang); - } - - let displayLocale: string; - let options: Intl.DateTimeFormatOptions; - - if (lang.startsWith('fa')) { - displayLocale = 'fa-IR'; - options = { - calendar: 'persian', - numberingSystem: 'arab', - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - hour12: false, - }; - } else { - displayLocale = 'en-US'; - options = { - calendar: 'gregory', - numberingSystem: 'latn', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false, - }; - } - - const formattedDate = new Intl.DateTimeFormat(displayLocale, options).format( - date, - ); - - return toLocaleDigits(formattedDate, lang); -} - -interface Device { - id: string; - timeAndDate: string; - deviceModel: string; - ip: string; - current: boolean; -} - -interface ApiSession { - key: string; - created: string; - deviceOs: string; - deviceName: string; - ipAddress: string; -} - -interface ActiveSessionsData { - sessions: ApiSession[]; - currentKey: string; -} - -interface ProfileApiResponse { - success: boolean; - message?: string; - activeSessions?: ActiveSessionsData; -} +import { fetchProfile, deleteSessions } from '../../api/settingsApi'; +import { type ApiSession } from '../../types/settingsApiType'; +import { useManualApi } from '@/hooks/useApi'; +import { formatDate } from '@/utils/formatSessionDate'; +import { type Device } from '../../types/settingsType'; export function ActiveDevices() { const { t, i18n } = useTranslation('setting'); - const token = localStorage.getItem('authToken'); - const [devices, setDevices] = useState([]); const [loadingDeleteIds, setLoadingDeleteIds] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [fetchError, setFetchError] = useState(null); - const theme = useTheme(); const isXsup = useMediaQuery(theme.breakpoints.up('xs')); + const { + data: profileData, + loading: isLoading, + error: fetchError, + execute: executeFetchProfile, + } = useManualApi(fetchProfile); + + const { + data: terminateData, + loading: isTerminating, + execute: executeTerminateAll, + } = useManualApi(deleteSessions); + useEffect(() => { - const fetchActiveSessions = async () => { - setIsLoading(true); - setFetchError(null); - if (!token) { - setIsLoading(false); - setFetchError(t('active.notLoggedIn')); - return; - } - try { - const res = await apiClient.post( - '/Profile/GetProfile', - {}, - { headers: { Authorization: `Bearer ${token}` } }, - ); + executeFetchProfile(); + }, [i18n.language]); - if (res.data.success && res.data.activeSessions) { - const { sessions, currentKey } = res.data.activeSessions; - const formattedDevices = sessions.map((session: ApiSession) => ({ - id: session.key, - timeAndDate: formatSessionDate(session.created, i18n.language, t), - deviceModel: `${session.deviceOs} ${session.deviceName}`, - ip: session.ipAddress, - current: session.key === currentKey, - })); - setDevices(formattedDevices); - } else { - throw new Error( - res.data.message || t('active.failFetchActiveSessions'), - ); - } - } catch (err: unknown) { - console.error(t('active.failFetchActiveSessions'), err); - let message = t('active.errorFetch'); - if (err instanceof Error) { - message = err.message; - } - setFetchError(message); - } finally { - setIsLoading(false); - } - }; + useEffect(() => { + if (profileData?.success && profileData.activeSessions) { + const { sessions, currentKey } = profileData.activeSessions; + const formattedDevices = sessions.map((session: ApiSession) => ({ + id: session.key, + timeAndDate: formatDate(session.created, i18n.language, t), + deviceModel: `${session.deviceOs} ${session.deviceName}`, + ip: session.ipAddress, + current: session.key === currentKey, + })); + setDevices(formattedDevices); + } + }, [profileData, i18n.language, t]); - fetchActiveSessions(); - }, [token, i18n.language, t]); + useEffect(() => { + if (terminateData?.success) { + setDevices((prev) => prev.filter((d) => d.current)); + } + }, [terminateData]); const handleDeleteDevice = async (id: string) => { if (loadingDeleteIds.includes(id)) return; setLoadingDeleteIds((prev) => [...prev, id]); try { - const res = await apiClient.post( - '/Profile/DeleteSessions', - { - keys: [id], - }, - { headers: { Authorization: `Bearer ${token}` } }, - ); - - if (res.data.success) { + const { data } = await deleteSessions({ keys: [id] }); + if (data.success) { setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id)); } else { - console.error('Delete failed:', res.data.message); + console.error('Delete failed:', data.message); } } catch (error: unknown) { - // console.error('Delete error:', error); + console.error('Delete error:', error); } finally { setLoadingDeleteIds((prev) => prev.filter((loadingId) => loadingId !== id), @@ -182,30 +81,19 @@ export function ActiveDevices() { } }; - const handleTerminateAllOtherSessions = async () => { + const handleTerminateAllOtherSessions = () => { const otherSessionKeys = devices.filter((d) => !d.current).map((d) => d.id); - - if (otherSessionKeys.length === 0) return; - - try { - const res = await apiClient.post( - '/Profile/DeleteSessions', - { - keys: otherSessionKeys, - }, - { headers: { Authorization: `Bearer ${token}` } }, - ); - - if (res.data.success) { - setDevices((prev) => prev.filter((d) => d.current)); - } else { - console.error('Failed to terminate other sessions:', res.data.message); - } - } catch (error: unknown) { - console.error('Error terminating sessions:', error); + if (otherSessionKeys.length > 0) { + executeTerminateAll({ keys: otherSessionKeys }); } }; + const getErrorMessage = (error: unknown): string | null => { + if (!error) return null; + if (error instanceof Error) return error.message; + return String(error); + }; + return ( - {t('active.deleteDevicesButton')} + {isTerminating + ? t('active.deleting') + : t('active.deleteDevicesButton')} } > @@ -242,7 +132,9 @@ export function ActiveDevices() { ) : fetchError ? ( - {fetchError} + + {getErrorMessage(fetchError)} + ) : ( - } + startIcon={} disabled={ device.current || loadingDeleteIds.includes(device.id) } @@ -378,14 +264,16 @@ export function ActiveDevices() { }, }} > - {loadingDeleteIds.includes(device.id) - ? t('active.deleting') - : t('active.deleteDevice')} + {loadingDeleteIds.includes(device.id) ? ( + + ) : ( + t('active.deleteDevice') + )} {isXsup && ( - + )} ))} diff --git a/src/features/profile/components/layout/Header.tsx b/src/features/profile/components/layout/Header.tsx deleted file mode 100644 index cffd51b..0000000 --- a/src/features/profile/components/layout/Header.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Box, Typography } from '@mui/material'; - -export function Header() { - const headers = [{ name: 'محمدحسین برزه گر', phone: '09123456789' }]; - return ( - - {headers.map((h) => ( - - {h.name} - - {h.phone} - - - ))} - - ); -} diff --git a/src/features/profile/components/layout/Layout.tsx b/src/features/profile/components/layout/Layout.tsx deleted file mode 100644 index 5584405..0000000 --- a/src/features/profile/components/layout/Layout.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useState } from 'react'; -import { Outlet, useLocation } from 'react-router-dom'; -import { - Box, - useTheme, - useMediaQuery, - Drawer, - AppBar, - Toolbar, - IconButton, -} from '@mui/material'; -import { SideNav } from '@rkheftan/harmony-ui'; -import { useTranslation } from 'react-i18next'; -import { HambergerMenu } from 'iconsax-react'; - -import { Header } from './Header'; -import { getNavConfig } from './navConfigs'; - -export function Layout() { - const theme = useTheme(); - const isMdUp = useMediaQuery(theme.breakpoints.up('md')); - const isLgUp = useMediaQuery(theme.breakpoints.up('lg')); - const location = useLocation(); - const { t } = useTranslation('sideMap'); - const [drawerOpen, setDrawerOpen] = useState(false); - - const navWidthMd = isLgUp ? 274 : 'min(15vw, 180px)'; - // const sidebarWidth = isLgUp ? 274 : isMdUp ? 'min(15vw, 180px)' : 0; - - const navConfig = getNavConfig(t); - - return ( - - - {!isMdUp && ( - - - setDrawerOpen(true)} - size="medium" - sx={{ backgroundColor: '#2979FF1F' }} - > - - - - - )} - - - {isMdUp && ( - - } - activePath={location.pathname + location.hash} - sideNavVariant="full" - positioning="absolute" - /> - - )} - - - - - - - {!isMdUp && ( - setDrawerOpen(false)} - > - - } - activePath={location.pathname + location.hash} - sideNavVariant="full" - /> - - - )} - - - ); -} diff --git a/src/features/profile/components/layout/Routes.tsx b/src/features/profile/components/layout/Routes.tsx deleted file mode 100644 index 26eb288..0000000 --- a/src/features/profile/components/layout/Routes.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { createBrowserRouter, Navigate } from 'react-router-dom'; -import { Layout } from './Layout'; -import { PersonalInformation } from '../userInformation/PersonalInformation'; -import { PhoneNumber } from '../userInformation/PhoneNumber'; -import { SocialMedia } from '../userInformation/SocialMedia'; -import { PasswordSecurity } from '../security/PasswordSecurity'; -import { RecentLogins } from '../security/RecentLogins'; -import { ActiveDevices } from '../activeDevices/ActiveDevices'; -import { Setting } from '../setting/Setting'; -import { Box } from '@mui/material'; -// tooye route ye component taarif mikonim be esme profile bad mibarim tooye route -export const router = createBrowserRouter([ - { - path: '/', - element: , - children: [ - { path: '/', element: }, - { - path: '/profile', - element: ( - -
- -
-
- -
-
- -
-
- ), - }, - { - path: '/security', - element: ( - -
- -
-
-
- -
-
- ), - }, - { path: '/devices', element: }, - { path: '/setting', element: }, - ], - }, -]); diff --git a/src/features/profile/components/layout/navConfigs.tsx b/src/features/profile/components/layout/navConfigs.tsx deleted file mode 100644 index 47e364d..0000000 --- a/src/features/profile/components/layout/navConfigs.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { - Devices, - LocationTick, - Mobile, - PasswordCheck, - Personalcard, - ProfileCircle, - Setting as SettingIcon, - Shield, - Sms, -} from 'iconsax-react'; -import { type TFunction } from 'i18next'; - -export function getNavConfig(t: TFunction) { - return [ - { - text: t('side.account'), - getIcon: (selected: boolean) => - selected ? ( - - ) : ( - - ), - path: '/profile', - children: [ - { - text: t('side.personalInfo'), - getIcon: (sel: boolean) => - sel ? ( - - ) : ( - - ), - path: '/profile#info', - }, - { - text: t('side.phoneNumber'), - getIcon: (sel: boolean) => - sel ? ( - - ) : ( - - ), - path: '/profile#contact-info', - }, - { - text: t('side.email'), - getIcon: (sel: boolean) => - sel ? ( - - ) : ( - - ), - path: '/profile#email', - }, - ], - }, - { - text: t('side.security'), - getIcon: (sel: boolean) => - sel ? ( - - ) : ( - - ), - path: '/security', - children: [ - { - text: t('side.password'), - getIcon: (sel: boolean) => - sel ? ( - - ) : ( - - ), - path: '/security#password', - }, - { - text: t('side.verifiedAddress'), - getIcon: (sel: boolean) => - sel ? ( - - ) : ( - - ), - path: '/security#locations', - }, - { - text: t('side.recentLogins'), - getIcon: (sel: boolean) => - sel ? ( - - ) : ( - - ), - path: '/security#sessions', - }, - ], - }, - { - text: t('side.activeDevices'), - getIcon: (sel: boolean) => - sel ? ( - - ) : ( - - ), - path: '/devices', - }, - { - text: t('side.settings'), - getIcon: (sel: boolean) => - sel ? ( - - ) : ( - - ), - path: '/setting', - }, - ]; -} diff --git a/src/features/profile/components/security/PasswordDialog.tsx b/src/features/profile/components/security/PasswordDialog.tsx index 8cabc24..9077c94 100644 --- a/src/features/profile/components/security/PasswordDialog.tsx +++ b/src/features/profile/components/security/PasswordDialog.tsx @@ -9,32 +9,11 @@ import { Button, Link, CircularProgress, + Typography, } from '@mui/material'; import { CloseCircle } from 'iconsax-react'; -import { PasswordValidationItem } from './PasswordValidation'; import { Icon } from '@rkheftan/harmony-ui'; - -interface PasswordDialogProps { - open: boolean; - fullScreen: boolean; - handleClose: () => void; - password: string; - setPassword: (val: string) => void; - confirmPassword: string; - setConfirmPassword: (val: string) => void; - currentPassword: string; - setCurrentPassword: (val: string) => void; - showValidation: boolean; - hasNumber: boolean; - hasMinLength: boolean; - hasUpperAndLower: boolean; - hasSpecialChar: boolean; - matchPassword: boolean; - loading: boolean; - handleShowAlert: () => void; - changePassword: boolean; - t: (key: string) => string; -} +import { type PasswordDialogProps } from '../../types/settingsType'; export function PasswordDialog({ open, @@ -47,14 +26,12 @@ export function PasswordDialog({ currentPassword, setCurrentPassword, showValidation, - hasNumber, - hasMinLength, - hasUpperAndLower, - hasSpecialChar, + validPassword, matchPassword, loading, - handleShowAlert, + handleSubmit, changePassword, + apiError, t, }: PasswordDialogProps) { return ( @@ -73,7 +50,9 @@ export function PasswordDialog({ - {t('securityForm.addPassword')} + {changePassword + ? t('securityForm.changePassword') + : t('securityForm.addPassword')}
@@ -86,16 +65,24 @@ export function PasswordDialog({ px: { xs: 2, sm: 3 }, }} > + {/* ✅ 4. API ERROR DISPLAY ADDED */} + {apiError && ( + + {apiError} + + )} + {changePassword && ( <> setCurrentPassword(e.target.value)} sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }} /> - + {t('securityForm.forgetPassword')} @@ -110,27 +97,15 @@ export function PasswordDialog({ sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} /> - {password && showValidation && ( - - - - - - - - + {showValidation && ( + + {validPassword + ? t('securityForm.passwordIsValid') + : t('securityForm.passwordIsInvalid')} + )} - {loading ? : t('securityForm.confirm')} + {loading ? ( + + ) : ( + t('securityForm.confirm') + )} diff --git a/src/features/profile/components/security/PasswordSecurity.tsx b/src/features/profile/components/security/PasswordSecurity.tsx index b628324..a847ee9 100644 --- a/src/features/profile/components/security/PasswordSecurity.tsx +++ b/src/features/profile/components/security/PasswordSecurity.tsx @@ -1,8 +1,9 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Box, Typography, Button, + CircularProgress, useTheme, useMediaQuery, } from '@mui/material'; @@ -10,9 +11,13 @@ import { useTranslation } from 'react-i18next'; import { CardContainer } from '@/components/CardContainer'; import { PageWrapper } from '../PageWrapper'; import { PasswordDialog } from './PasswordDialog'; -import { Toast } from '@/components/Toast'; import { regex } from '@/utils/regex'; -import apiClient from '@/lib/apiClient'; +import { useManualApi } from '@/hooks/useApi'; +import { + addPassword, + changePassword, + fetchProfile, +} from '../../api/settingsApi'; export function PasswordSecurity() { const theme = useTheme(); @@ -25,158 +30,161 @@ export function PasswordSecurity() { const [currentPassword, setCurrentPassword] = useState(''); const [showValidation, setShowValidation] = useState(false); const [showPasswordAlert, setShowPasswordAlert] = useState(false); - const [changePassword, setChangePassword] = useState(false); - const [loading, setLoading] = useState(false); + const [changePasswordUI, setChangePasswordUI] = useState(false); + + const { validPassword } = regex(password); + const matchPassword = password === confirmPassword; const { - hasNumber, - hasMinLength, - hasUpperAndLower, - hasSpecialChar, - validPassword, - } = regex(password); + data: profileData, + loading: isFetching, + error: fetchError, + execute: executeFetchProfile, + } = useManualApi(fetchProfile); - const matchPassword = password === confirmPassword; + const { + data: addData, + loading: isAdding, + error: addError, + execute: executeAddPassword, + } = useManualApi(addPassword); + + const { + data: changeData, + loading: isChanging, + error: changeError, + execute: executeChangePassword, + } = useManualApi(changePassword); + + useEffect(() => { + executeFetchProfile(); + }, []); + + useEffect(() => { + if (profileData?.success) { + setChangePasswordUI(profileData.hasPassword || false); + } + }, [profileData]); useEffect(() => { if (password) { - if (!validPassword) { - setShowValidation(true); - } else { - const timer = setTimeout(() => setShowValidation(false), 1000); - return () => clearTimeout(timer); - } + setShowValidation(!validPassword); } else { setShowValidation(false); } }, [password, validPassword]); + useEffect(() => { + if (addData?.success || changeData?.success) { + setShowPasswordAlert(true); + if (!changePasswordUI) { + setChangePasswordUI(true); + } + setOpen(false); + setPassword(''); + setConfirmPassword(''); + setCurrentPassword(''); + } + }, [addData, changeData, changePasswordUI]); + const handleOpen = () => setOpen(true); const handleClose = () => setOpen(false); - const handleShowAlert = async () => { - try { - setLoading(true); - - const url = changePassword - ? 'Profile/ChangePassword' - : 'Profile/AddPassword'; - - const payload = changePassword - ? { - oldPassword: currentPassword, - newPassword: password, - } - : { - password, - // confirmPassword, - }; - - const res = await apiClient.post(url, payload, { - headers: { - Authorization: `Bearer ${localStorage.getItem('authToken')}`, - 'Content-Type': 'application/json', - }, + const handlePasswordSubmit = () => { + if (changePasswordUI) { + executeChangePassword({ + oldPassword: currentPassword, + newPassword: password, }); - - if (res.data?.success) { - setShowPasswordAlert(true); - if (!changePassword) setChangePassword(true); - handleClose(); - setPassword(''); - setConfirmPassword(''); - setCurrentPassword(''); - } else { - console.error('Password update failed:', res.data?.message); - } - } catch (err) { - console.error('Error updating password:', err); - } finally { - setLoading(false); + } else { + executeAddPassword({ password }); } }; + const getErrorMessage = (error: unknown): string | null => { + if (!error) return null; + if (error instanceof Error) return error.message; + return String(error); + }; + + const apiError = useMemo( + () => addError || changeError, + [addError, changeError], + ); + return ( - {changePassword + } > - - {changePassword ? ( - - - {t('securityForm.activePassword')} - - - {t('securityForm.lastChange')} - - - ) : ( - - {t('securityForm.notDeterminedPassword')} - - )} - - - - setShowPasswordAlert(false)} + {isFetching ? ( + - {t('securityForm.alertSuccess')} - - + +
+ ) : fetchError ? ( + + + {getErrorMessage(fetchError)} + + + ) : ( + + {changePasswordUI ? ( + + + {t('securityForm.activePassword')} + + + {t('securityForm.lastChange')} + + + ) : ( + + {t('securityForm.notDeterminedPassword')} + + )} + + + + )}
); diff --git a/src/features/profile/components/security/PasswordValidation.tsx b/src/features/profile/components/security/PasswordValidation.tsx index 925efc0..29b9d94 100644 --- a/src/features/profile/components/security/PasswordValidation.tsx +++ b/src/features/profile/components/security/PasswordValidation.tsx @@ -1,11 +1,7 @@ import { Box, Typography } from '@mui/material'; import { TickCircle } from 'iconsax-react'; import { Icon } from '@rkheftan/harmony-ui'; - -interface ValidationItemProps { - isValid: boolean; - label: string; -} +import { type ValidationItemProps } from '../../types/settingsType'; export function PasswordValidationItem({ isValid, diff --git a/src/features/profile/components/security/RecentLogins.tsx b/src/features/profile/components/security/RecentLogins.tsx index 44c5edd..c21ce57 100644 --- a/src/features/profile/components/security/RecentLogins.tsx +++ b/src/features/profile/components/security/RecentLogins.tsx @@ -2,116 +2,46 @@ import React, { useState, useEffect } from 'react'; import { Box, Typography, - Button, CircularProgress, useTheme, useMediaQuery, } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import type { TFunction } from 'i18next'; import { CardContainer } from '@/components/CardContainer'; import { PageWrapper } from '../PageWrapper'; -import apiClient from '@/lib/apiClient'; - -function formatLoginDate(isoDate: string, lang: string, t: TFunction): string { - const date = new Date(isoDate); - const now = new Date(); - const diffInMinutes = Math.floor( - (now.getTime() - date.getTime()) / (1000 * 60), - ); - - if (diffInMinutes < 1) return t('active.justNow'); - if (diffInMinutes < 60) - return t('active.minutesAgo', { count: diffInMinutes }); - - let displayLocale: string; - let options: Intl.DateTimeFormatOptions; - - if (lang.startsWith('fa')) { - displayLocale = 'fa-IR'; - options = { - calendar: 'persian', - numberingSystem: 'arab', - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - hour12: false, - }; - } else { - displayLocale = 'en-US'; - options = { - calendar: 'gregory', - numberingSystem: 'latn', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false, - }; - } - - return new Intl.DateTimeFormat(displayLocale, options).format(date); -} - -interface LoginLog { - loginDateTime: string; - ipAddress: string; - deviceName: string; - deviceOs: string; -} - -interface ProfileApiResponse { - success: boolean; - loginLogs?: LoginLog[]; -} +import { fetchProfile } from '../../api/settingsApi'; +import type { LoginLog } from '../../types/settingsApiType'; +import { useManualApi } from '@/hooks/useApi'; +import { formatDate } from '@/utils/formatSessionDate'; // ✅ 1. IMPORT the shared function export function RecentLogins() { const { t, i18n } = useTranslation('setting'); - const token = localStorage.getItem('authToken'); - const [logs, setLogs] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [fetchError, setFetchError] = useState(null); - const theme = useTheme(); const isXsup = useMediaQuery(theme.breakpoints.up('xs')); + const { + data: profileData, + loading: isLoading, + error: fetchError, + execute: executeFetchProfile, + } = useManualApi(fetchProfile); + useEffect(() => { - const fetchLoginLogs = async () => { - setIsLoading(true); - setFetchError(null); + executeFetchProfile(); + }, [i18n.language]); - if (!token) { - setIsLoading(false); - setFetchError(t('active.notLoggedIn')); - return; - } + useEffect(() => { + if (profileData?.success && Array.isArray(profileData.loginLogs)) { + setLogs(profileData.loginLogs); + } + }, [profileData]); - try { - const res = await apiClient.post( - '/Profile/GetProfile', - {}, - { headers: { Authorization: `Bearer ${token}` } }, - ); - - if (res.data.success && Array.isArray(res.data.loginLogs)) { - setLogs(res.data.loginLogs); - } else { - throw new Error(t('active.failFetchActiveSessions')); - } - } catch (err) { - console.error(t('active.failFetchActiveSessions'), err); - setFetchError(t('active.errorFetch')); - } finally { - setIsLoading(false); - } - }; - - fetchLoginLogs(); - }, [token, i18n.language, t]); + const getErrorMessage = (error: unknown): string | null => { + if (!error) return null; + if (error instanceof Error) return error.message; + return String(error); + }; return ( @@ -133,7 +63,9 @@ export function RecentLogins() { ) : fetchError ? ( - {fetchError} + + {getErrorMessage(fetchError) || t('active.errorFetch')} + ) : ( @@ -142,45 +74,26 @@ export function RecentLogins() { - - {formatLoginDate(log.loginDateTime, i18n.language, t)} + + {formatDate(log.loginDateTime, i18n.language, t)} - + {`${log.deviceOs} ${log.deviceName}`} - + {log.ipAddress} - {isXsup && ( - + {isXsup && index < logs.length - 1 && ( + )} ))} diff --git a/src/features/profile/components/setting/Setting.tsx b/src/features/profile/components/setting/Setting.tsx index 2179c57..1037286 100644 --- a/src/features/profile/components/setting/Setting.tsx +++ b/src/features/profile/components/setting/Setting.tsx @@ -14,7 +14,8 @@ import { ThemeToggleButton } from '@/components/ThemToggle'; import { PageWrapper } from '../PageWrapper'; import { Icon } from '@rkheftan/harmony-ui'; import { Sun1, Moon, Calendar1 } from 'iconsax-react'; -import apiClient from '@/lib/apiClient'; +import { useManualApi } from '@/hooks/useApi'; +import { fetchProfile, saveSettings } from '../../api/settingsApi'; type ThemeMode = 'light' | 'dark'; type CalendarType = 'christian' | 'solar' | 'lunar'; @@ -25,23 +26,6 @@ interface SettingsState { theme: ThemeMode; } -interface UserSettingsFromApi { - theme: number; - calendarType: number; - language: number; -} - -interface GetProfileApiResponse { - success: boolean; - message?: string; - userSettings?: UserSettingsFromApi; -} - -interface SaveSettingApiResponse { - success: boolean; - message?: string; -} - const languageOptions = [ { code: 'en', label: 'English', apiValue: 1 }, { code: 'fa', label: 'فارسی', apiValue: 2 }, @@ -58,356 +42,275 @@ const themeApiMap: Record = { light: 1, dark: 2 }; export function Setting() { const { t, i18n } = useTranslation(['setting']); const { mode, setMode } = useColorScheme(); - const token = localStorage.getItem('authToken'); const [savedSettings, setSavedSettings] = useState({ - language: i18n.language || 'en', + language: i18n.language, calendar: 'solar', theme: mode === 'light' || mode === 'dark' ? mode : 'light', }); - const [draftSettings, setDraftSettings] = useState(savedSettings); const [isEditing, setIsEditing] = useState(false); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [isFetching, setIsFetching] = useState(true); - const [fetchError, setFetchError] = useState(null); + const { + data: profileData, + loading: isFetching, + error: fetchError, + execute: executeFetchProfile, + } = useManualApi(fetchProfile); + + const { + data: saveData, + loading: isSaving, + error: saveError, + execute: executeSaveSettings, + } = useManualApi(saveSettings); - const notLoggedIn = t('settings.notLoggedIn'); - const failRetrieve = t('settings.failRetrieve'); - const errorFetch = t('settings.errorFetch'); useEffect(() => { - const fetchUserSettings = async () => { - setIsFetching(true); - setFetchError(null); - if (!token) { - setIsFetching(false); - setFetchError(notLoggedIn); - return; - } - try { - const res = await apiClient.post( - '/Profile/GetProfile', - {}, - { headers: { Authorization: `Bearer ${token}` } }, - ); + executeFetchProfile(); + }, []); - if (res.data.success && res.data.userSettings) { - const { theme, calendarType, language } = res.data.userSettings; - const themeReverseMap: { [key: number]: ThemeMode | undefined } = { - 1: 'light', - 2: 'dark', - }; - const themeMode = themeReverseMap[theme] || 'light'; - const calendarSetting = calendarOptions.find( - (c) => c.apiValue === calendarType, - ); - const calendarKey = calendarSetting ? calendarSetting.key : 'solar'; - const languageSetting = languageOptions.find( - (l) => l.apiValue === language, - ); - const languageCode = languageSetting ? languageSetting.code : 'en'; - const newSettings: SettingsState = { - theme: themeMode, - calendar: calendarKey, - language: languageCode, - }; - setSavedSettings(newSettings); - setDraftSettings(newSettings); - setMode(themeMode); + useEffect(() => { + if (profileData?.success && profileData.userSettings) { + const { theme, calendarType, language } = profileData.userSettings; + const themeReverseMap: { [key: number]: ThemeMode | undefined } = { + 1: 'light', + 2: 'dark', + }; + const newSettings: SettingsState = { + theme: themeReverseMap[theme] || 'light', + calendar: + calendarOptions.find((c) => c.apiValue === calendarType)?.key || + 'solar', + language: + languageOptions.find((l) => l.apiValue === language)?.code || 'en', + }; + setSavedSettings(newSettings); + setDraftSettings(newSettings); + setMode(newSettings.theme); + i18n.changeLanguage(newSettings.language); + } + }, [profileData, setMode, i18n]); - i18n.changeLanguage(languageCode); - } else { - throw new Error(res.data.message || failRetrieve); - } - } catch (e: unknown) { - let message = errorFetch; - if (e instanceof Error) { - message = e.message; - } - setFetchError(message); - } finally { - setIsFetching(false); - } - }; - - fetchUserSettings(); - }, [token, setMode, i18n, notLoggedIn, failRetrieve, errorFetch]); + useEffect(() => { + if (saveData?.success) { + setMode(draftSettings.theme); + setSavedSettings(draftSettings); + i18n.changeLanguage(draftSettings.language); + setIsEditing(false); + } + }, [saveData, draftSettings, setMode, i18n]); useEffect(() => { if (isEditing) { - setDraftSettings({ - ...savedSettings, - theme: mode === 'light' || mode === 'dark' ? mode : 'light', - }); + const resolvedMode = mode === 'light' || mode === 'dark' ? mode : 'light'; + setDraftSettings({ ...savedSettings, theme: resolvedMode }); } }, [isEditing, savedSettings, mode]); const handleCancel = () => { setIsEditing(false); - setError(null); + setDraftSettings(savedSettings); + setMode(savedSettings.theme); }; - const handleSave = async () => { - setError(null); - setLoading(true); - try { + const handleEditToggle = () => { + if (isEditing) { const languageObj = languageOptions.find( (o) => o.code === draftSettings.language, ); const calendarObj = calendarOptions.find( (c) => c.key === draftSettings.calendar, ); - const apiThemeValue = themeApiMap[draftSettings.theme]; - if (!languageObj || !calendarObj) { - setError(t('settings.invalidSelection')); - setLoading(false); - return; - } - - const res = await apiClient.post( - '/Profile/SaveSetting', - { - theme: apiThemeValue, + if (languageObj && calendarObj) { + executeSaveSettings({ + theme: themeApiMap[draftSettings.theme], calendarType: calendarObj.apiValue, language: languageObj.apiValue, - }, - { headers: { Authorization: `Bearer ${token}` } }, - ); - - if (res.data.success) { - setMode(draftSettings.theme); - setSavedSettings(draftSettings); - await i18n.changeLanguage(draftSettings.language); - setIsEditing(false); - } else { - setError(res.data.message || t('settings.saveFailed')); + }); } - } catch (e: unknown) { - setError(t('settings.saveFailed')); - } finally { - setLoading(false); + } else { + setIsEditing(true); } }; - - const handleEditToggle = () => { - isEditing ? handleSave() : setIsEditing(true); + const getErrorMessage = (error: unknown): string | null => { + if (!error) return null; + if (error instanceof Error) return error.message; + return String(error); }; return ( - - - - {isEditing && ( - - )} - - - } + + {isEditing && ( + + )} + + + } + > + {isFetching ? ( + - {isFetching ? ( - - + + + ) : fetchError ? ( + + + {getErrorMessage(fetchError)} + + + ) : ( + + {getErrorMessage(saveError) && ( + + {getErrorMessage(saveError)} + + )} + + + + {t('settings.theme')} + + {isEditing ? ( + { + setDraftSettings((prev) => ({ + ...prev, + theme: newTheme, + })); + }} + /> + ) : ( + + + + {t(`settings.${savedSettings.theme}`)} + + + )} - ) : fetchError ? ( - - {fetchError} - - ) : ( - - {error && ( - - {error} + + + {t('settings.language')} + + {isEditing ? ( + o.label} + value={ + languageOptions.find( + (o) => o.code === draftSettings.language, + ) // ✅ FIX: Removed '|| null' + } + onChange={(_, v) => + v && + setDraftSettings((prev) => ({ + ...prev, + language: v.code, + })) + } + renderInput={(p) => } + size="small" + fullWidth + disableClearable + /> + ) : ( + + { + languageOptions.find( + (o) => o.code === savedSettings.language, + )?.label + } )} - - - {isEditing ? ( - - - {t('settings.theme')} - - { - setDraftSettings((prev) => ({ - ...prev, - theme: newTheme, - })); - }} - /> - - ) : ( - - - {t('settings.theme')} - - - - - {t(`settings.${savedSettings.theme}`)} - - - - )} - - - {isEditing ? ( - o.label} - value={ - languageOptions.find( - (o) => o.code === draftSettings.language, - ) || null - } - onChange={(_, v) => - v && - setDraftSettings((prev) => ({ - ...prev, - language: v.code, - })) - } - renderInput={(p) => ( - - )} - size="medium" - fullWidth - /> - ) : ( - - - {t('settings.language')} - - - { - languageOptions.find( - (o) => o.code === savedSettings.language, - )?.label - } - - - )} - - - - {isEditing ? ( - c.key)} - getOptionLabel={(key) => t(`settings.${key}`)} - value={draftSettings.calendar} - onChange={(_, v) => - v && - setDraftSettings((prev) => ({ ...prev, calendar: v })) - } - renderInput={(params) => ( - - )} - size="medium" - fullWidth - /> - ) : ( - - - {t('settings.calendar')} - - - - - {t(`settings.${savedSettings.calendar}`)} - - - - )} - - )} - - - + + + + {t('settings.calendar')} + + {isEditing ? ( + c.key)} + getOptionLabel={(key) => t(`settings.${key}`)} + value={draftSettings.calendar} + onChange={(_, v) => + v && setDraftSettings((prev) => ({ ...prev, calendar: v })) + } + renderInput={(params) => } + size="small" + fullWidth + disableClearable + /> + ) : ( + + + + {t(`settings.${savedSettings.calendar}`)} + + + )} + + + )} + ); } diff --git a/src/features/profile/components/userInformation/PersonalInformation.tsx b/src/features/profile/components/userInformation/PersonalInformation.tsx index de67fd0..d89b4c1 100644 --- a/src/features/profile/components/userInformation/PersonalInformation.tsx +++ b/src/features/profile/components/userInformation/PersonalInformation.tsx @@ -8,7 +8,7 @@ import { InfoRowEdit } from './personalInformation/InfoRowEdit'; import { PageWrapper } from '../PageWrapper'; import { useManualApi } from '@/hooks/useApi'; import { Gender, type InfoRowData } from '../../types/settingsType'; -import { fetchProfile, saveProfile, getToken } from '../../api/settingsApi'; +import { fetchProfile, saveProfile } from '../../api/settingsApi'; export function PersonalInformation() { const { t } = useTranslation('setting'); @@ -37,13 +37,6 @@ export function PersonalInformation() { execute: executeSaveProfile, } = useManualApi(saveProfile); - const { - data: tokenData, - loading: isGettingToken, - error: tokenError, - execute: executeGetToken, - } = useManualApi(getToken); - useEffect(() => { executeFetchProfile(); }, []); @@ -91,10 +84,6 @@ export function PersonalInformation() { executeSaveProfile({ data, imageUrl: uploadedImageUrl }); }; - const handleGetTokenClick = async () => { - executeGetToken(); - }; - const getErrorMessage = (error: unknown): string | null => { if (!error) return null; if (error instanceof Error) return error.message; @@ -124,16 +113,6 @@ export function PersonalInformation() { justifyContent: 'flex-end', }} > - {isEditing ? ( <> @@ -186,14 +164,6 @@ export function PersonalInformation() { {getErrorMessage(saveProfileError)} )} - {tokenData?.success && ( - - Token fetched and stored successfully! - - )} } > @@ -218,11 +188,6 @@ export function PersonalInformation() { ) : ( <> - {getErrorMessage(tokenError) && ( - - Error: {getErrorMessage(tokenError)} - - )} ( 'enterEmail', ); - const [apiError, setApiError] = useState(null); - const [isLoading, setIsLoading] = useState(false); const [emailList, setEmailList] = useState([]); + const [formError, setFormError] = useState(null); - const [isFetching, setIsFetching] = useState(true); - const [fetchError, setFetchError] = useState(null); + const { + data: profileData, + loading: isFetching, + error: fetchError, + execute: executeFetchProfile, + } = useManualApi(fetchProfile); + + const { + data: sendCodeData, + loading: isSendingCode, + error: sendCodeError, + execute: executeSendCode, + } = useManualApi(sendEmailCode); + + const { + data: confirmData, + loading: isConfirming, + error: confirmError, + execute: executeConfirmCode, + } = useManualApi(confirmEmailCode); + + const { + data: changeEmailData, + loading: isChangingEmail, + error: changeEmailError, + execute: executeChangeEmail, + } = useManualApi(changeEmail); const fullScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'), @@ -67,128 +74,85 @@ export function SocialMedia() { | 'xl'; useEffect(() => { - const fetchInitialEmail = async () => { - setIsFetching(true); - setFetchError(null); - if (!token) { - setIsFetching(false); - setFetchError(t('settingForm.notLoggedIn')); - return; - } - try { - const res = await apiClient.post( - '/Profile/GetProfile', - {}, - { headers: { Authorization: `Bearer ${token}` } }, - ); + executeFetchProfile(); + }, []); - if (res.data.success && res.data.email) { - const userEmail = res.data.email; - const newAccount: EmailAccount = { - email: userEmail, - provider: userEmail.includes('gmail.com') ? 'google' : 'email', - time: '', - }; - setEmailList([newAccount]); - } else if (!res.data.success) { - throw new Error(res.data.message || t('settingForm.failFetchEmail')); - } - } catch (err: unknown) { - let message = t('settingForm.errorFetchEmail'); - if (err instanceof Error) { - message = err.message; - } - setFetchError(message); - } finally { - setIsFetching(false); - } - }; + useEffect(() => { + if (profileData?.success && profileData.email) { + const { email } = profileData; + setEmailList([ + { + email, + provider: email.includes('@gmail.') ? 'google' : 'email', + time: '', + }, + ]); + } + }, [profileData]); - fetchInitialEmail(); - }, [token, t]); + useEffect(() => { + if (sendCodeData?.success) { + setDialogStep('enterCode'); + } + }, [sendCodeData]); + + useEffect(() => { + if (confirmData?.success && confirmData.confirm) { + executeChangeEmail({ email: emailInput }); + } + }, [confirmData, emailInput, executeChangeEmail]); + + useEffect(() => { + if (changeEmailData?.success) { + setEmailList((prev) => [ + ...prev, + { + email: emailInput, + provider: emailInput.includes('@gmail.') ? 'google' : 'email', + time: t('settingForm.justNow'), + }, + ]); + resetDialog(); + } + }, [changeEmailData, emailInput, t]); const resetDialog = () => { setOpenDialog(false); setEmailInput(''); setVerificationCode(''); - setApiError(null); - setIsLoading(false); + setFormError(null); setDialogStep('enterEmail'); }; - const handleSendCode = async () => { + const handleSendCode = () => { if (!/^\S+@\S+\.\S+$/.test(emailInput)) { - setApiError(t('settingForm.emailIsInvalid')); + setFormError(t('settingForm.emailIsInvalid')); return; } - setIsLoading(true); - setApiError(null); - try { - const res = await apiClient.post( - 'Profile/SendEmailChangeCode', - { email: emailInput }, - { headers: { Authorization: `Bearer ${token}` } }, - ); - if (res.data.success) { - setDialogStep('enterCode'); - } else { - setApiError(res.data.message || t('settingForm.sendCodeFailed')); - } - } catch (err: unknown) { - setApiError(t('settingForm.sendCodeFailed')); - } finally { - setIsLoading(false); - } + setFormError(null); + executeSendCode({ email: emailInput }); }; - const handleConfirmAndChangeEmail = async () => { + const handleConfirmAndChangeEmail = () => { if (verificationCode.length < 4) { - setApiError(t('settingForm.verificationCodeRequired')); + setFormError(t('settingForm.verificationCodeRequired')); return; } - setIsLoading(true); - setApiError(null); - try { - const confirmRes = await apiClient.post( - 'Profile/ConfirmEmailChangeCode', - { email: emailInput, verifyCode: verificationCode }, - { headers: { Authorization: `Bearer ${token}` } }, - ); - - if (confirmRes.data.success && confirmRes.data.confirm) { - const changeRes = await apiClient.post( - 'Profile/ChangeEmail', - { email: emailInput }, - { headers: { Authorization: `Bearer ${token}` } }, - ); - - if (changeRes.data.success) { - setEmailList((prevList) => [ - ...prevList, - { - email: emailInput, - provider: 'email', - time: t('settingForm.justNow'), - }, - ]); - resetDialog(); - } else { - setApiError( - changeRes.data.message || t('settingForm.changeEmailFailed'), - ); - } - } else { - setApiError( - confirmRes.data.message || t('settingForm.verifyCodeFailed'), - ); - } - } catch (err: unknown) { - setApiError(t('settingForm.anErrorOccurred')); - } finally { - setIsLoading(false); - } + setFormError(null); + executeConfirmCode({ email: emailInput, verifyCode: verificationCode }); }; + const getErrorMessage = (error: unknown): string | null => { + if (!error) return null; + if (error instanceof Error) return error.message; + return String(error); + }; + + const apiError = useMemo( + () => getErrorMessage(sendCodeError || confirmError || changeEmailError), + [sendCodeError, confirmError, changeEmailError], + ); + return ( ) : fetchError ? ( - {fetchError} + + {getErrorMessage(fetchError)} + ) : ( @@ -225,8 +191,8 @@ export function SocialMedia() { setEmailInput={setEmailInput} verificationCode={verificationCode} setVerificationCode={setVerificationCode} - apiError={apiError} - isLoading={isLoading} + apiError={formError || apiError} + isLoading={isSendingCode || isConfirming || isChangingEmail} dialogStep={dialogStep} onSendCode={handleSendCode} onConfirmEmail={handleConfirmAndChangeEmail} diff --git a/src/features/profile/components/userInformation/phoneNumber/PhoneEditForm.tsx b/src/features/profile/components/userInformation/phoneNumber/PhoneEditForm.tsx index b27d0b4..4477b82 100644 --- a/src/features/profile/components/userInformation/phoneNumber/PhoneEditForm.tsx +++ b/src/features/profile/components/userInformation/phoneNumber/PhoneEditForm.tsx @@ -9,7 +9,6 @@ import { } from '@mui/material'; import { Edit2, TickCircle } from 'iconsax-react'; import { CountDownTimer } from '@/components/CountDownTimer'; -import { Toast } from '@/components/Toast'; import { CountryCodeSelector } from '../../CountryCodeSelector'; import { Icon } from '@rkheftan/harmony-ui'; @@ -217,14 +216,6 @@ export default function PhoneEditForm({ )} - - setShowToast(false)} - > - {t('settingForm.successfulChangePhone')} - ); } diff --git a/src/features/profile/routes/SettingPage.tsx b/src/features/profile/routes/SettingPage.tsx deleted file mode 100644 index 631c267..0000000 --- a/src/features/profile/routes/SettingPage.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { RouterProvider } from 'react-router-dom'; -import { router } from '../components/layout/Routes'; - -export function Settings() { - return ; -} diff --git a/src/features/profile/types/settingsApiType.ts b/src/features/profile/types/settingsApiType.ts index 02ac5da..4edc7de 100644 --- a/src/features/profile/types/settingsApiType.ts +++ b/src/features/profile/types/settingsApiType.ts @@ -1,5 +1,29 @@ import { Gender } from './settingsType'; +/* General API Types */ +export interface UserSettingsFromApi { + theme: number; + calendarType: number; + language: number; +} +export interface ApiSession { + key: string; + created: string; + deviceOs: string; + deviceName: string; + ipAddress: string; +} +export interface ActiveSessionsData { + sessions: ApiSession[]; + currentKey: string; +} +export interface LoginLog { + loginDateTime: string; + ipAddress: string; + deviceName: string; + deviceOs: string; +} + /* Profile API Types */ export interface GetProfileApiResponse { success: boolean; @@ -10,9 +34,13 @@ export interface GetProfileApiResponse { gender?: Gender; countryCode?: string; profileImageUrl?: string; - phoneNumber?: string; // ✅ ADDED + phoneNumber?: string; + userSettings?: UserSettingsFromApi; + activeSessions?: ActiveSessionsData; + loginLogs?: LoginLog[]; + email?: string; + hasPassword?: boolean; } - export interface SaveProfileApiResponse { success: boolean; message?: string; @@ -20,8 +48,37 @@ export interface SaveProfileApiResponse { email?: string; } -export interface TokenApiResponse { - access_token: string; +/* Settings API Types */ +export interface SaveSettingsApiResponse { + success: boolean; + message?: string; +} + +/* Session API Types */ +export interface DeleteSessionsApiResponse { + success: boolean; + message?: string; +} + +/* Password API Types */ +export interface PasswordApiResponse { + success: boolean; + message?: string; +} + +/* Email API Types */ +export interface SendEmailCodeApiResponse { + success: boolean; + message?: string; +} +export interface ConfirmEmailCodeApiResponse { + success: boolean; + confirm?: boolean; + message?: string; +} +export interface ChangeEmailApiResponse { + success: boolean; + message?: string; } /* Phone Number API Types */ @@ -29,7 +86,6 @@ export interface PhoneNumberApiResponse { success: boolean; message?: string; } - export interface ConfirmPhoneNumberApiResponse extends PhoneNumberApiResponse { confirm?: boolean; } diff --git a/src/features/profile/types/settingsType.ts b/src/features/profile/types/settingsType.ts index 96c9964..41d83f4 100644 --- a/src/features/profile/types/settingsType.ts +++ b/src/features/profile/types/settingsType.ts @@ -11,3 +11,36 @@ export interface InfoRowData { country: string; gender: Gender; } + +export interface Device { + id: string; + timeAndDate: string; + deviceModel: string; + ip: string; + current: boolean; +} + +export interface PasswordDialogProps { + open: boolean; + fullScreen: boolean; + handleClose: () => void; + password: string; + setPassword: (val: string) => void; + confirmPassword: string; + setConfirmPassword: (val: string) => void; + currentPassword: string; + setCurrentPassword: (val: string) => void; + showValidation: boolean; + validPassword: boolean; + matchPassword: boolean; + loading: boolean; + handleSubmit: () => void; + changePassword: boolean; + apiError: string | null; + t: (key: string) => string; +} + +export interface ValidationItemProps { + isValid: boolean; + label: string; +} diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts index 4570202..b2743a9 100644 --- a/src/lib/apiClient.ts +++ b/src/lib/apiClient.ts @@ -12,7 +12,8 @@ const apiClient = axios.create({ // Set default headers headers: { - Accept: 'application/json', + // Accept: 'application/json', + Authorization: 'Bearer ' + getToken(), }, }); diff --git a/src/types/apiResponse.ts b/src/types/apiResponse.ts new file mode 100644 index 0000000..abc0599 --- /dev/null +++ b/src/types/apiResponse.ts @@ -0,0 +1,13 @@ +export interface ApiResponse { + success: boolean; + errorCode: number; + message: string; + validations: ApiResponseValidation[]; +} + +export interface ApiResponseValidation { + message: string; + code: number; + property: string; + severity: number; +} diff --git a/src/utils/formatSessionDate.tsx b/src/utils/formatSessionDate.tsx new file mode 100644 index 0000000..eb49453 --- /dev/null +++ b/src/utils/formatSessionDate.tsx @@ -0,0 +1,38 @@ +import { type TFunction } from 'i18next'; +import { toLocaleDigits } from './persianDigit'; + +export function formatDate( + isoDate: string, + lang: string, + t: TFunction, +): string { + const date = new Date(isoDate); + const now = new Date(); + const diffInMinutes = Math.floor( + (now.getTime() - date.getTime()) / (1000 * 60), + ); + + if (diffInMinutes < 1) return t('active.justNow'); + if (diffInMinutes < 60) { + return toLocaleDigits( + t('active.minutesAgo', { count: diffInMinutes }), + lang, + ); + } + + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + calendar: lang.startsWith('fa') ? 'persian' : 'gregory', + numberingSystem: lang.startsWith('fa') ? 'arab' : 'latn', + }; + const displayLocale = lang.startsWith('fa') ? 'fa-IR' : 'en-US'; + const formattedDate = new Intl.DateTimeFormat(displayLocale, options).format( + date, + ); + return toLocaleDigits(formattedDate, lang); +}