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);
+}