chore: move api to api folder and seperate the types into another file
This commit is contained in:
@@ -1,15 +1,12 @@
|
|||||||
import { CssBaseline } from '@mui/material';
|
import { CssBaseline } from '@mui/material';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { LanguageManager } from './components/LanguageManager';
|
import { LanguageManager } from './components/LanguageManager';
|
||||||
import { Settings } from './features/profile/routes/SettingPage';
|
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<LanguageManager />
|
<LanguageManager />
|
||||||
<Settings />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<Snackbar sx={{ minWidth: '396px' }} open={open} onClose={onClose}>
|
|
||||||
<Alert
|
|
||||||
onClose={onClose}
|
|
||||||
severity={color}
|
|
||||||
variant="filled"
|
|
||||||
sx={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
import axios, { isAxiosError } from 'axios';
|
|
||||||
import apiClient from '@/lib/apiClient';
|
import apiClient from '@/lib/apiClient';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type GetProfileApiResponse,
|
type GetProfileApiResponse,
|
||||||
type SaveProfileApiResponse,
|
type SaveProfileApiResponse,
|
||||||
type TokenApiResponse,
|
|
||||||
type PhoneNumberApiResponse,
|
type PhoneNumberApiResponse,
|
||||||
type ConfirmPhoneNumberApiResponse,
|
type ConfirmPhoneNumberApiResponse,
|
||||||
|
type SaveSettingsApiResponse,
|
||||||
|
type DeleteSessionsApiResponse,
|
||||||
|
type PasswordApiResponse,
|
||||||
|
type SendEmailCodeApiResponse,
|
||||||
|
type ConfirmEmailCodeApiResponse,
|
||||||
|
type ChangeEmailApiResponse,
|
||||||
} from '../types/settingsApiType';
|
} from '../types/settingsApiType';
|
||||||
import { type InfoRowData } from '../types/settingsType';
|
import { type InfoRowData } from '../types/settingsType';
|
||||||
|
|
||||||
const storedToken = () => localStorage.getItem('authToken');
|
|
||||||
|
|
||||||
export async function fetchProfile(): Promise<{ data: GetProfileApiResponse }> {
|
export async function fetchProfile(): Promise<{ data: GetProfileApiResponse }> {
|
||||||
const res = await apiClient.post<GetProfileApiResponse>(
|
const res = await apiClient.post<GetProfileApiResponse>(
|
||||||
'/Profile/GetProfile',
|
'/Profile/GetProfile',
|
||||||
{},
|
{},
|
||||||
{ headers: { Authorization: `Bearer ${storedToken()}` } },
|
|
||||||
);
|
);
|
||||||
return { data: res.data };
|
return { data: res.data };
|
||||||
}
|
}
|
||||||
@@ -28,56 +29,13 @@ export async function saveProfile(payload?: {
|
|||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw new Error('Payload for saving profile is missing.');
|
throw new Error('Payload for saving profile is missing.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await apiClient.post<SaveProfileApiResponse>(
|
const res = await apiClient.post<SaveProfileApiResponse>(
|
||||||
'/Profile/SaveProfilePersonalInforamtion',
|
'/Profile/SaveProfilePersonalInforamtion',
|
||||||
payload,
|
payload,
|
||||||
{ headers: { Authorization: `Bearer ${storedToken()}` } },
|
|
||||||
);
|
);
|
||||||
return { data: res.data };
|
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<TokenApiResponse>(
|
|
||||||
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?: {
|
export async function sendVerificationCode(payload?: {
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
}): Promise<{ data: PhoneNumberApiResponse }> {
|
}): Promise<{ data: PhoneNumberApiResponse }> {
|
||||||
@@ -87,7 +45,6 @@ export async function sendVerificationCode(payload?: {
|
|||||||
const res = await apiClient.post<PhoneNumberApiResponse>(
|
const res = await apiClient.post<PhoneNumberApiResponse>(
|
||||||
'/Profile/SendVerfiyPhoneNumberCode',
|
'/Profile/SendVerfiyPhoneNumberCode',
|
||||||
payload,
|
payload,
|
||||||
{ headers: { Authorization: `Bearer ${storedToken()}` } },
|
|
||||||
);
|
);
|
||||||
return { data: res.data };
|
return { data: res.data };
|
||||||
}
|
}
|
||||||
@@ -102,7 +59,6 @@ export async function confirmPhoneNumberCode(payload?: {
|
|||||||
const res = await apiClient.post<ConfirmPhoneNumberApiResponse>(
|
const res = await apiClient.post<ConfirmPhoneNumberApiResponse>(
|
||||||
'/Profile/ConfirmPhoneNumberChangeCode',
|
'/Profile/ConfirmPhoneNumberChangeCode',
|
||||||
payload,
|
payload,
|
||||||
{ headers: { Authorization: `Bearer ${storedToken()}` } },
|
|
||||||
);
|
);
|
||||||
return { data: res.data };
|
return { data: res.data };
|
||||||
}
|
}
|
||||||
@@ -116,7 +72,102 @@ export async function changePhoneNumber(payload?: {
|
|||||||
const res = await apiClient.post<PhoneNumberApiResponse>(
|
const res = await apiClient.post<PhoneNumberApiResponse>(
|
||||||
'/Profile/ChangePhoneNumber',
|
'/Profile/ChangePhoneNumber',
|
||||||
payload,
|
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<SaveSettingsApiResponse>(
|
||||||
|
'/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<DeleteSessionsApiResponse>(
|
||||||
|
'/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<PasswordApiResponse>(
|
||||||
|
'/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<PasswordApiResponse>(
|
||||||
|
'/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<SendEmailCodeApiResponse>(
|
||||||
|
'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<ConfirmEmailCodeApiResponse>(
|
||||||
|
'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<ChangeEmailApiResponse>(
|
||||||
|
'Profile/ChangeEmail',
|
||||||
|
payload,
|
||||||
);
|
);
|
||||||
return { data: res.data };
|
return { data: res.data };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,173 +8,72 @@ import {
|
|||||||
CircularProgress,
|
CircularProgress,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { TFunction } from 'i18next';
|
|
||||||
import { DeviceMessage, Logout } from 'iconsax-react';
|
import { DeviceMessage, Logout } from 'iconsax-react';
|
||||||
import { CardContainer } from '@/components/CardContainer';
|
import { CardContainer } from '@/components/CardContainer';
|
||||||
import { PageWrapper } from '../PageWrapper';
|
import { PageWrapper } from '../PageWrapper';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
import apiClient from '@/lib/apiClient';
|
import { fetchProfile, deleteSessions } from '../../api/settingsApi';
|
||||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
import { type ApiSession } from '../../types/settingsApiType';
|
||||||
|
import { useManualApi } from '@/hooks/useApi';
|
||||||
function formatSessionDate(
|
import { formatDate } from '@/utils/formatSessionDate';
|
||||||
isoDate: string,
|
import { type Device } from '../../types/settingsType';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActiveDevices() {
|
export function ActiveDevices() {
|
||||||
const { t, i18n } = useTranslation('setting');
|
const { t, i18n } = useTranslation('setting');
|
||||||
const token = localStorage.getItem('authToken');
|
|
||||||
|
|
||||||
const [devices, setDevices] = useState<Device[]>([]);
|
const [devices, setDevices] = useState<Device[]>([]);
|
||||||
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
|
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
const fetchActiveSessions = async () => {
|
data: profileData,
|
||||||
setIsLoading(true);
|
loading: isLoading,
|
||||||
setFetchError(null);
|
error: fetchError,
|
||||||
if (!token) {
|
execute: executeFetchProfile,
|
||||||
setIsLoading(false);
|
} = useManualApi(fetchProfile);
|
||||||
setFetchError(t('active.notLoggedIn'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await apiClient.post<ProfileApiResponse>(
|
|
||||||
'/Profile/GetProfile',
|
|
||||||
{},
|
|
||||||
{ headers: { Authorization: `Bearer ${token}` } },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.data.success && res.data.activeSessions) {
|
const {
|
||||||
const { sessions, currentKey } = res.data.activeSessions;
|
data: terminateData,
|
||||||
|
loading: isTerminating,
|
||||||
|
execute: executeTerminateAll,
|
||||||
|
} = useManualApi(deleteSessions);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
executeFetchProfile();
|
||||||
|
}, [i18n.language]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profileData?.success && profileData.activeSessions) {
|
||||||
|
const { sessions, currentKey } = profileData.activeSessions;
|
||||||
const formattedDevices = sessions.map((session: ApiSession) => ({
|
const formattedDevices = sessions.map((session: ApiSession) => ({
|
||||||
id: session.key,
|
id: session.key,
|
||||||
timeAndDate: formatSessionDate(session.created, i18n.language, t),
|
timeAndDate: formatDate(session.created, i18n.language, t),
|
||||||
deviceModel: `${session.deviceOs} ${session.deviceName}`,
|
deviceModel: `${session.deviceOs} ${session.deviceName}`,
|
||||||
ip: session.ipAddress,
|
ip: session.ipAddress,
|
||||||
current: session.key === currentKey,
|
current: session.key === currentKey,
|
||||||
}));
|
}));
|
||||||
setDevices(formattedDevices);
|
setDevices(formattedDevices);
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
res.data.message || t('active.failFetchActiveSessions'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
}, [profileData, i18n.language, t]);
|
||||||
console.error(t('active.failFetchActiveSessions'), err);
|
|
||||||
let message = t('active.errorFetch');
|
|
||||||
if (err instanceof Error) {
|
|
||||||
message = err.message;
|
|
||||||
}
|
|
||||||
setFetchError(message);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchActiveSessions();
|
useEffect(() => {
|
||||||
}, [token, i18n.language, t]);
|
if (terminateData?.success) {
|
||||||
|
setDevices((prev) => prev.filter((d) => d.current));
|
||||||
|
}
|
||||||
|
}, [terminateData]);
|
||||||
|
|
||||||
const handleDeleteDevice = async (id: string) => {
|
const handleDeleteDevice = async (id: string) => {
|
||||||
if (loadingDeleteIds.includes(id)) return;
|
if (loadingDeleteIds.includes(id)) return;
|
||||||
setLoadingDeleteIds((prev) => [...prev, id]);
|
setLoadingDeleteIds((prev) => [...prev, id]);
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.post(
|
const { data } = await deleteSessions({ keys: [id] });
|
||||||
'/Profile/DeleteSessions',
|
if (data.success) {
|
||||||
{
|
|
||||||
keys: [id],
|
|
||||||
},
|
|
||||||
{ headers: { Authorization: `Bearer ${token}` } },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.data.success) {
|
|
||||||
setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id));
|
setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id));
|
||||||
} else {
|
} else {
|
||||||
console.error('Delete failed:', res.data.message);
|
console.error('Delete failed:', data.message);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// console.error('Delete error:', error);
|
console.error('Delete error:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingDeleteIds((prev) =>
|
setLoadingDeleteIds((prev) =>
|
||||||
prev.filter((loadingId) => loadingId !== id),
|
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);
|
const otherSessionKeys = devices.filter((d) => !d.current).map((d) => d.id);
|
||||||
|
if (otherSessionKeys.length > 0) {
|
||||||
if (otherSessionKeys.length === 0) return;
|
executeTerminateAll({ keys: otherSessionKeys });
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown): string | null => {
|
||||||
|
if (!error) return null;
|
||||||
|
if (error instanceof Error) return error.message;
|
||||||
|
return String(error);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<CardContainer
|
<CardContainer
|
||||||
@@ -222,9 +110,11 @@ export function ActiveDevices() {
|
|||||||
color: 'error.main',
|
color: 'error.main',
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading || isTerminating}
|
||||||
>
|
>
|
||||||
{t('active.deleteDevicesButton')}
|
{isTerminating
|
||||||
|
? t('active.deleting')
|
||||||
|
: t('active.deleteDevicesButton')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -242,7 +132,9 @@ export function ActiveDevices() {
|
|||||||
</Box>
|
</Box>
|
||||||
) : fetchError ? (
|
) : fetchError ? (
|
||||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||||
<Typography color="error.main">{fetchError}</Typography>
|
<Typography color="error.main">
|
||||||
|
{getErrorMessage(fetchError)}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<Box
|
||||||
@@ -352,13 +244,7 @@ export function ActiveDevices() {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={
|
startIcon={<Icon Component={Logout} size="small" />}
|
||||||
<Icon
|
|
||||||
Component={Logout}
|
|
||||||
size="small"
|
|
||||||
color="error.main"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
disabled={
|
disabled={
|
||||||
device.current || loadingDeleteIds.includes(device.id)
|
device.current || loadingDeleteIds.includes(device.id)
|
||||||
}
|
}
|
||||||
@@ -378,14 +264,16 @@ export function ActiveDevices() {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loadingDeleteIds.includes(device.id)
|
{loadingDeleteIds.includes(device.id) ? (
|
||||||
? t('active.deleting')
|
<CircularProgress size={20} color="error" />
|
||||||
: t('active.deleteDevice')}
|
) : (
|
||||||
|
t('active.deleteDevice')
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{isXsup && (
|
{isXsup && (
|
||||||
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }} />
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Box, Typography } from '@mui/material';
|
|
||||||
|
|
||||||
export function Header() {
|
|
||||||
const headers = [{ name: 'محمدحسین برزه گر', phone: '09123456789' }];
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
height: 84,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
px: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{headers.map((h) => (
|
|
||||||
<Box key={h.phone}>
|
|
||||||
<Typography variant="body2">{h.name}</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{h.phone}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
minHeight: '100vh',
|
|
||||||
width: '100vw',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
px: { xs: 0, sm: 0, md: 'max(1vw, 8px)', lg: '60px' },
|
|
||||||
py: { xs: 0, sm: 0, md: '40px', lg: '80px' },
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
backgroundColor: 'background.default',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: {
|
|
||||||
xs: '100vw',
|
|
||||||
md: 'min(90vw, 900px)',
|
|
||||||
lg: '1100px',
|
|
||||||
},
|
|
||||||
height: '100vh',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
overflow: 'hidden',
|
|
||||||
borderRadius: { xs: 0, md: 2 },
|
|
||||||
boxShadow: { xs: 0, md: 3 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!isMdUp && (
|
|
||||||
<AppBar position="static" elevation={0}>
|
|
||||||
<Toolbar sx={{ backgroundColor: 'background.paper', gap: 2 }}>
|
|
||||||
<IconButton
|
|
||||||
edge="start"
|
|
||||||
onClick={() => setDrawerOpen(true)}
|
|
||||||
size="medium"
|
|
||||||
sx={{ backgroundColor: '#2979FF1F' }}
|
|
||||||
>
|
|
||||||
<HambergerMenu size={24} color="#1976d2" />
|
|
||||||
</IconButton>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1, display: 'flex', overflow: 'hidden' }}>
|
|
||||||
{isMdUp && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: navWidthMd,
|
|
||||||
flexShrink: 0,
|
|
||||||
transition: 'width 0.3s ease',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SideNav
|
|
||||||
navConfig={navConfig}
|
|
||||||
header={<Header />}
|
|
||||||
activePath={location.pathname + location.hash}
|
|
||||||
sideNavVariant="full"
|
|
||||||
positioning="absolute"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
overflowY: 'auto',
|
|
||||||
px: { xs: 2, sm: 3, md: 'max(1.5vw, 12px)' },
|
|
||||||
py: { xs: 2, sm: 3 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Outlet />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{!isMdUp && (
|
|
||||||
<Drawer
|
|
||||||
anchor="left"
|
|
||||||
open={drawerOpen}
|
|
||||||
onClose={() => setDrawerOpen(false)}
|
|
||||||
>
|
|
||||||
<Box width={navWidthMd}>
|
|
||||||
<SideNav
|
|
||||||
navConfig={navConfig}
|
|
||||||
header={<Header />}
|
|
||||||
activePath={location.pathname + location.hash}
|
|
||||||
sideNavVariant="full"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Drawer>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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: <Layout />,
|
|
||||||
children: [
|
|
||||||
{ path: '/', element: <Navigate to="/profile" replace /> },
|
|
||||||
{
|
|
||||||
path: '/profile',
|
|
||||||
element: (
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
||||||
<div id="info">
|
|
||||||
<PersonalInformation />
|
|
||||||
</div>
|
|
||||||
<div id="contact-info">
|
|
||||||
<PhoneNumber />
|
|
||||||
</div>
|
|
||||||
<div id="email">
|
|
||||||
<SocialMedia />
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/security',
|
|
||||||
element: (
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
||||||
<div id="password">
|
|
||||||
<PasswordSecurity />
|
|
||||||
</div>
|
|
||||||
<div id="locations"></div>
|
|
||||||
<div id="sessions">
|
|
||||||
<RecentLogins />
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ path: '/devices', element: <ActiveDevices /> },
|
|
||||||
{ path: '/setting', element: <Setting /> },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
@@ -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 ? (
|
|
||||||
<ProfileCircle size={24} color="#1976d2" variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<ProfileCircle size={24} color="#82B1FF" />
|
|
||||||
),
|
|
||||||
path: '/profile',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
text: t('side.personalInfo'),
|
|
||||||
getIcon: (sel: boolean) =>
|
|
||||||
sel ? (
|
|
||||||
<Personalcard size={24} color="#1976d2" variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<Personalcard size={24} color="#82B1FF" />
|
|
||||||
),
|
|
||||||
path: '/profile#info',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('side.phoneNumber'),
|
|
||||||
getIcon: (sel: boolean) =>
|
|
||||||
sel ? (
|
|
||||||
<Mobile size={24} color="#1976d2" variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<Mobile size={24} color="#82B1FF" />
|
|
||||||
),
|
|
||||||
path: '/profile#contact-info',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('side.email'),
|
|
||||||
getIcon: (sel: boolean) =>
|
|
||||||
sel ? (
|
|
||||||
<Sms size={24} color="#1976d2" variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<Sms size={24} color="#82B1FF" />
|
|
||||||
),
|
|
||||||
path: '/profile#email',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('side.security'),
|
|
||||||
getIcon: (sel: boolean) =>
|
|
||||||
sel ? (
|
|
||||||
<Shield size={24} color="#1976d2" variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<Shield size={24} color="#82B1FF" />
|
|
||||||
),
|
|
||||||
path: '/security',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
text: t('side.password'),
|
|
||||||
getIcon: (sel: boolean) =>
|
|
||||||
sel ? (
|
|
||||||
<PasswordCheck size={24} color="#1976d2" variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<PasswordCheck size={24} color="#82B1FF" />
|
|
||||||
),
|
|
||||||
path: '/security#password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('side.verifiedAddress'),
|
|
||||||
getIcon: (sel: boolean) =>
|
|
||||||
sel ? (
|
|
||||||
<LocationTick size={24} color="#1976d2" variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<LocationTick size={24} color="#82B1FF" />
|
|
||||||
),
|
|
||||||
path: '/security#locations',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('side.recentLogins'),
|
|
||||||
getIcon: (sel: boolean) =>
|
|
||||||
sel ? (
|
|
||||||
<Devices size={24} color="#1976d2" variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<Devices size={24} color="#82B1FF" />
|
|
||||||
),
|
|
||||||
path: '/security#sessions',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('side.activeDevices'),
|
|
||||||
getIcon: (sel: boolean) =>
|
|
||||||
sel ? (
|
|
||||||
<Devices size={24} color="#1976d2" variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<Devices size={24} color="#82B1FF" />
|
|
||||||
),
|
|
||||||
path: '/devices',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t('side.settings'),
|
|
||||||
getIcon: (sel: boolean) =>
|
|
||||||
sel ? (
|
|
||||||
<SettingIcon size={24} color="#1976d2" variant="Bold" />
|
|
||||||
) : (
|
|
||||||
<SettingIcon size={24} color="#82B1FF" />
|
|
||||||
),
|
|
||||||
path: '/setting',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -9,32 +9,11 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Link,
|
Link,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { CloseCircle } from 'iconsax-react';
|
import { CloseCircle } from 'iconsax-react';
|
||||||
import { PasswordValidationItem } from './PasswordValidation';
|
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
|
import { type PasswordDialogProps } from '../../types/settingsType';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PasswordDialog({
|
export function PasswordDialog({
|
||||||
open,
|
open,
|
||||||
@@ -47,14 +26,12 @@ export function PasswordDialog({
|
|||||||
currentPassword,
|
currentPassword,
|
||||||
setCurrentPassword,
|
setCurrentPassword,
|
||||||
showValidation,
|
showValidation,
|
||||||
hasNumber,
|
validPassword,
|
||||||
hasMinLength,
|
|
||||||
hasUpperAndLower,
|
|
||||||
hasSpecialChar,
|
|
||||||
matchPassword,
|
matchPassword,
|
||||||
loading,
|
loading,
|
||||||
handleShowAlert,
|
handleSubmit,
|
||||||
changePassword,
|
changePassword,
|
||||||
|
apiError,
|
||||||
t,
|
t,
|
||||||
}: PasswordDialogProps) {
|
}: PasswordDialogProps) {
|
||||||
return (
|
return (
|
||||||
@@ -73,7 +50,9 @@ export function PasswordDialog({
|
|||||||
<Icon Component={CloseCircle} size="large" color="primary.main" />
|
<Icon Component={CloseCircle} size="large" color="primary.main" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Box component="span" fontWeight="bold" fontSize={18}>
|
<Box component="span" fontWeight="bold" fontSize={18}>
|
||||||
{t('securityForm.addPassword')}
|
{changePassword
|
||||||
|
? t('securityForm.changePassword')
|
||||||
|
: t('securityForm.addPassword')}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
@@ -86,16 +65,24 @@ export function PasswordDialog({
|
|||||||
px: { xs: 2, sm: 3 },
|
px: { xs: 2, sm: 3 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* ✅ 4. API ERROR DISPLAY ADDED */}
|
||||||
|
{apiError && (
|
||||||
|
<Typography color="error" variant="body2" textAlign="center">
|
||||||
|
{apiError}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
{changePassword && (
|
{changePassword && (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
label={t('securityForm.currentPassword')}
|
label={t('securityForm.currentPassword')}
|
||||||
|
type="password"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={currentPassword}
|
value={currentPassword}
|
||||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||||
/>
|
/>
|
||||||
<Link sx={{ fontSize: '15px' }}>
|
<Link sx={{ fontSize: '15px', cursor: 'pointer' }}>
|
||||||
{t('securityForm.forgetPassword')}
|
{t('securityForm.forgetPassword')}
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
@@ -110,27 +97,15 @@ export function PasswordDialog({
|
|||||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{password && showValidation && (
|
{showValidation && (
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
<Typography
|
||||||
<Box sx={{ maxWidth: '364px', width: '100%' }}>
|
variant="body2"
|
||||||
<PasswordValidationItem
|
color={validPassword ? 'success.main' : 'error.main'}
|
||||||
isValid={hasNumber}
|
>
|
||||||
label={t('securityForm.hasNumber')}
|
{validPassword
|
||||||
/>
|
? t('securityForm.passwordIsValid')
|
||||||
<PasswordValidationItem
|
: t('securityForm.passwordIsInvalid')}
|
||||||
isValid={hasMinLength}
|
</Typography>
|
||||||
label={t('securityForm.hasMinLength')}
|
|
||||||
/>
|
|
||||||
<PasswordValidationItem
|
|
||||||
isValid={hasUpperAndLower}
|
|
||||||
label={t('securityForm.hasUpperAndLower')}
|
|
||||||
/>
|
|
||||||
<PasswordValidationItem
|
|
||||||
isValid={hasSpecialChar}
|
|
||||||
label={t('securityForm.hasSpecialChar')}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -154,10 +129,14 @@ export function PasswordDialog({
|
|||||||
fullWidth
|
fullWidth
|
||||||
sx={{ height: 48, textTransform: 'none' }}
|
sx={{ height: 48, textTransform: 'none' }}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleShowAlert}
|
onClick={handleSubmit}
|
||||||
disabled={!password || password !== confirmPassword || loading}
|
disabled={!validPassword || !matchPassword || loading}
|
||||||
>
|
>
|
||||||
{loading ? <CircularProgress size={20} /> : t('securityForm.confirm')}
|
{loading ? (
|
||||||
|
<CircularProgress size={24} color="inherit" />
|
||||||
|
) : (
|
||||||
|
t('securityForm.confirm')
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
|
CircularProgress,
|
||||||
useTheme,
|
useTheme,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
@@ -10,9 +11,13 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { CardContainer } from '@/components/CardContainer';
|
import { CardContainer } from '@/components/CardContainer';
|
||||||
import { PageWrapper } from '../PageWrapper';
|
import { PageWrapper } from '../PageWrapper';
|
||||||
import { PasswordDialog } from './PasswordDialog';
|
import { PasswordDialog } from './PasswordDialog';
|
||||||
import { Toast } from '@/components/Toast';
|
|
||||||
import { regex } from '@/utils/regex';
|
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() {
|
export function PasswordSecurity() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -25,111 +30,123 @@ export function PasswordSecurity() {
|
|||||||
const [currentPassword, setCurrentPassword] = useState('');
|
const [currentPassword, setCurrentPassword] = useState('');
|
||||||
const [showValidation, setShowValidation] = useState(false);
|
const [showValidation, setShowValidation] = useState(false);
|
||||||
const [showPasswordAlert, setShowPasswordAlert] = useState(false);
|
const [showPasswordAlert, setShowPasswordAlert] = useState(false);
|
||||||
const [changePassword, setChangePassword] = useState(false);
|
const [changePasswordUI, setChangePasswordUI] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
const { validPassword } = regex(password);
|
||||||
|
const matchPassword = password === confirmPassword;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasNumber,
|
data: profileData,
|
||||||
hasMinLength,
|
loading: isFetching,
|
||||||
hasUpperAndLower,
|
error: fetchError,
|
||||||
hasSpecialChar,
|
execute: executeFetchProfile,
|
||||||
validPassword,
|
} = useManualApi(fetchProfile);
|
||||||
} = regex(password);
|
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (password) {
|
if (password) {
|
||||||
if (!validPassword) {
|
setShowValidation(!validPassword);
|
||||||
setShowValidation(true);
|
|
||||||
} else {
|
|
||||||
const timer = setTimeout(() => setShowValidation(false), 1000);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setShowValidation(false);
|
setShowValidation(false);
|
||||||
}
|
}
|
||||||
}, [password, validPassword]);
|
}, [password, validPassword]);
|
||||||
|
|
||||||
const handleOpen = () => setOpen(true);
|
useEffect(() => {
|
||||||
const handleClose = () => setOpen(false);
|
if (addData?.success || changeData?.success) {
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.data?.success) {
|
|
||||||
setShowPasswordAlert(true);
|
setShowPasswordAlert(true);
|
||||||
if (!changePassword) setChangePassword(true);
|
if (!changePasswordUI) {
|
||||||
handleClose();
|
setChangePasswordUI(true);
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
setPassword('');
|
setPassword('');
|
||||||
setConfirmPassword('');
|
setConfirmPassword('');
|
||||||
setCurrentPassword('');
|
setCurrentPassword('');
|
||||||
} else {
|
|
||||||
console.error('Password update failed:', res.data?.message);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
}, [addData, changeData, changePasswordUI]);
|
||||||
console.error('Error updating password:', err);
|
|
||||||
} finally {
|
const handleOpen = () => setOpen(true);
|
||||||
setLoading(false);
|
const handleClose = () => setOpen(false);
|
||||||
|
|
||||||
|
const handlePasswordSubmit = () => {
|
||||||
|
if (changePasswordUI) {
|
||||||
|
executeChangePassword({
|
||||||
|
oldPassword: currentPassword,
|
||||||
|
newPassword: password,
|
||||||
|
});
|
||||||
|
} 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 (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<CardContainer
|
<CardContainer
|
||||||
title={t('securityForm.password')}
|
title={t('securityForm.password')}
|
||||||
subtitle={t('securityForm.determinePassword')}
|
subtitle={t('securityForm.determinePassword')}
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button variant="contained" onClick={handleOpen}>
|
||||||
variant="outlined"
|
{changePasswordUI
|
||||||
onClick={handleOpen}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: 'primary.main',
|
|
||||||
color: 'background.paper',
|
|
||||||
textTransform: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{changePassword
|
|
||||||
? t('securityForm.changePassword')
|
? t('securityForm.changePassword')
|
||||||
: t('securityForm.addPassword')}
|
: t('securityForm.addPassword')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{isFetching ? (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
px: { xs: 2, sm: 3, md: 4 },
|
|
||||||
py: 2,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
justifyContent: 'center',
|
||||||
gap: 2,
|
alignItems: 'center',
|
||||||
bgcolor: 'background.paper',
|
p: 4,
|
||||||
flex: 1,
|
minHeight: '120px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{changePassword ? (
|
<CircularProgress />
|
||||||
<Box sx={{ flexDirection: 'column', px: 2, py: 2 }}>
|
</Box>
|
||||||
|
) : fetchError ? (
|
||||||
|
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||||
|
<Typography color="error.main">
|
||||||
|
{getErrorMessage(fetchError)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ px: 4, py: 2 }}>
|
||||||
|
{changePasswordUI ? (
|
||||||
|
<Box>
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
{t('securityForm.activePassword')}
|
{t('securityForm.activePassword')}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -141,7 +158,7 @@ export function PasswordSecurity() {
|
|||||||
<Typography
|
<Typography
|
||||||
variant="body1"
|
variant="body1"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ textAlign: 'center', py: { xs: 4, sm: '43.5px' } }}
|
sx={{ textAlign: 'center', py: 4 }}
|
||||||
>
|
>
|
||||||
{t('securityForm.notDeterminedPassword')}
|
{t('securityForm.notDeterminedPassword')}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -158,25 +175,16 @@ export function PasswordSecurity() {
|
|||||||
currentPassword={currentPassword}
|
currentPassword={currentPassword}
|
||||||
setCurrentPassword={setCurrentPassword}
|
setCurrentPassword={setCurrentPassword}
|
||||||
showValidation={showValidation}
|
showValidation={showValidation}
|
||||||
hasNumber={hasNumber}
|
validPassword={validPassword}
|
||||||
hasMinLength={hasMinLength}
|
|
||||||
hasUpperAndLower={hasUpperAndLower}
|
|
||||||
hasSpecialChar={hasSpecialChar}
|
|
||||||
matchPassword={matchPassword}
|
matchPassword={matchPassword}
|
||||||
loading={loading}
|
loading={isAdding || isChanging}
|
||||||
handleShowAlert={handleShowAlert}
|
handleSubmit={handlePasswordSubmit}
|
||||||
changePassword={changePassword}
|
changePassword={changePasswordUI}
|
||||||
|
apiError={getErrorMessage(apiError)}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Toast
|
|
||||||
color="success"
|
|
||||||
open={showPasswordAlert}
|
|
||||||
onClose={() => setShowPasswordAlert(false)}
|
|
||||||
>
|
|
||||||
{t('securityForm.alertSuccess')}
|
|
||||||
</Toast>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
</CardContainer>
|
</CardContainer>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import { TickCircle } from 'iconsax-react';
|
import { TickCircle } from 'iconsax-react';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
|
import { type ValidationItemProps } from '../../types/settingsType';
|
||||||
interface ValidationItemProps {
|
|
||||||
isValid: boolean;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PasswordValidationItem({
|
export function PasswordValidationItem({
|
||||||
isValid,
|
isValid,
|
||||||
|
|||||||
@@ -2,117 +2,47 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
useTheme,
|
useTheme,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { TFunction } from 'i18next';
|
|
||||||
import { CardContainer } from '@/components/CardContainer';
|
import { CardContainer } from '@/components/CardContainer';
|
||||||
import { PageWrapper } from '../PageWrapper';
|
import { PageWrapper } from '../PageWrapper';
|
||||||
import apiClient from '@/lib/apiClient';
|
import { fetchProfile } from '../../api/settingsApi';
|
||||||
|
import type { LoginLog } from '../../types/settingsApiType';
|
||||||
function formatLoginDate(isoDate: string, lang: string, t: TFunction): string {
|
import { useManualApi } from '@/hooks/useApi';
|
||||||
const date = new Date(isoDate);
|
import { formatDate } from '@/utils/formatSessionDate'; // ✅ 1. IMPORT the shared function
|
||||||
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[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RecentLogins() {
|
export function RecentLogins() {
|
||||||
const { t, i18n } = useTranslation('setting');
|
const { t, i18n } = useTranslation('setting');
|
||||||
const token = localStorage.getItem('authToken');
|
|
||||||
|
|
||||||
const [logs, setLogs] = useState<LoginLog[]>([]);
|
const [logs, setLogs] = useState<LoginLog[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: profileData,
|
||||||
|
loading: isLoading,
|
||||||
|
error: fetchError,
|
||||||
|
execute: executeFetchProfile,
|
||||||
|
} = useManualApi(fetchProfile);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLoginLogs = async () => {
|
executeFetchProfile();
|
||||||
setIsLoading(true);
|
}, [i18n.language]);
|
||||||
setFetchError(null);
|
|
||||||
|
|
||||||
if (!token) {
|
useEffect(() => {
|
||||||
setIsLoading(false);
|
if (profileData?.success && Array.isArray(profileData.loginLogs)) {
|
||||||
setFetchError(t('active.notLoggedIn'));
|
setLogs(profileData.loginLogs);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}, [profileData]);
|
||||||
|
|
||||||
try {
|
const getErrorMessage = (error: unknown): string | null => {
|
||||||
const res = await apiClient.post<ProfileApiResponse>(
|
if (!error) return null;
|
||||||
'/Profile/GetProfile',
|
if (error instanceof Error) return error.message;
|
||||||
{},
|
return String(error);
|
||||||
{ 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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<CardContainer
|
<CardContainer
|
||||||
@@ -133,7 +63,9 @@ export function RecentLogins() {
|
|||||||
</Box>
|
</Box>
|
||||||
) : fetchError ? (
|
) : fetchError ? (
|
||||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||||
<Typography color="error.main">{fetchError}</Typography>
|
<Typography color="error.main">
|
||||||
|
{getErrorMessage(fetchError) || t('active.errorFetch')}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box sx={{ px: 4 }}>
|
<Box sx={{ px: 4 }}>
|
||||||
@@ -142,45 +74,26 @@ export function RecentLogins() {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: { xs: 'column', sm: 'row' },
|
flexDirection: 'row',
|
||||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
alignItems: 'center',
|
||||||
minHeight: 50,
|
minHeight: 50,
|
||||||
py: 1.5,
|
py: 1.5,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography sx={{ flex: 1, minWidth: 160 }}>
|
||||||
variant="body2"
|
{formatDate(log.loginDateTime, i18n.language, t)}
|
||||||
sx={{
|
|
||||||
flexBasis: { xs: '100%', sm: 'auto' },
|
|
||||||
mb: { xs: 1, sm: 0 },
|
|
||||||
minWidth: { sm: '172.5px' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formatLoginDate(log.loginDateTime, i18n.language, t)}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography sx={{ flex: 1, minWidth: 160 }}>
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
flexBasis: { xs: '100%', sm: 'auto' },
|
|
||||||
mb: { xs: 1, sm: 0 },
|
|
||||||
minWidth: { sm: '172.5px' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`${log.deviceOs} ${log.deviceName}`}
|
{`${log.deviceOs} ${log.deviceName}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography sx={{ flex: 1, minWidth: 120 }}>
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
flexBasis: { xs: '100%', sm: 'auto' },
|
|
||||||
mb: { xs: 1, sm: 0 },
|
|
||||||
minWidth: { sm: '172.5px' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{log.ipAddress}
|
{log.ipAddress}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{isXsup && (
|
{isXsup && index < logs.length - 1 && (
|
||||||
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }} />
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { ThemeToggleButton } from '@/components/ThemToggle';
|
|||||||
import { PageWrapper } from '../PageWrapper';
|
import { PageWrapper } from '../PageWrapper';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
import { Sun1, Moon, Calendar1 } from 'iconsax-react';
|
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 ThemeMode = 'light' | 'dark';
|
||||||
type CalendarType = 'christian' | 'solar' | 'lunar';
|
type CalendarType = 'christian' | 'solar' | 'lunar';
|
||||||
@@ -25,23 +26,6 @@ interface SettingsState {
|
|||||||
theme: ThemeMode;
|
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 = [
|
const languageOptions = [
|
||||||
{ code: 'en', label: 'English', apiValue: 1 },
|
{ code: 'en', label: 'English', apiValue: 1 },
|
||||||
{ code: 'fa', label: 'فارسی', apiValue: 2 },
|
{ code: 'fa', label: 'فارسی', apiValue: 2 },
|
||||||
@@ -58,149 +42,106 @@ const themeApiMap: Record<ThemeMode, number> = { light: 1, dark: 2 };
|
|||||||
export function Setting() {
|
export function Setting() {
|
||||||
const { t, i18n } = useTranslation(['setting']);
|
const { t, i18n } = useTranslation(['setting']);
|
||||||
const { mode, setMode } = useColorScheme();
|
const { mode, setMode } = useColorScheme();
|
||||||
const token = localStorage.getItem('authToken');
|
|
||||||
|
|
||||||
const [savedSettings, setSavedSettings] = useState<SettingsState>({
|
const [savedSettings, setSavedSettings] = useState<SettingsState>({
|
||||||
language: i18n.language || 'en',
|
language: i18n.language,
|
||||||
calendar: 'solar',
|
calendar: 'solar',
|
||||||
theme: mode === 'light' || mode === 'dark' ? mode : 'light',
|
theme: mode === 'light' || mode === 'dark' ? mode : 'light',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [draftSettings, setDraftSettings] =
|
const [draftSettings, setDraftSettings] =
|
||||||
useState<SettingsState>(savedSettings);
|
useState<SettingsState>(savedSettings);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [isFetching, setIsFetching] = useState(true);
|
const {
|
||||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchUserSettings = async () => {
|
executeFetchProfile();
|
||||||
setIsFetching(true);
|
}, []);
|
||||||
setFetchError(null);
|
|
||||||
if (!token) {
|
|
||||||
setIsFetching(false);
|
|
||||||
setFetchError(notLoggedIn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await apiClient.post<GetProfileApiResponse>(
|
|
||||||
'/Profile/GetProfile',
|
|
||||||
{},
|
|
||||||
{ headers: { Authorization: `Bearer ${token}` } },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.data.success && res.data.userSettings) {
|
useEffect(() => {
|
||||||
const { theme, calendarType, language } = res.data.userSettings;
|
if (profileData?.success && profileData.userSettings) {
|
||||||
|
const { theme, calendarType, language } = profileData.userSettings;
|
||||||
const themeReverseMap: { [key: number]: ThemeMode | undefined } = {
|
const themeReverseMap: { [key: number]: ThemeMode | undefined } = {
|
||||||
1: 'light',
|
1: 'light',
|
||||||
2: 'dark',
|
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 = {
|
const newSettings: SettingsState = {
|
||||||
theme: themeMode,
|
theme: themeReverseMap[theme] || 'light',
|
||||||
calendar: calendarKey,
|
calendar:
|
||||||
language: languageCode,
|
calendarOptions.find((c) => c.apiValue === calendarType)?.key ||
|
||||||
|
'solar',
|
||||||
|
language:
|
||||||
|
languageOptions.find((l) => l.apiValue === language)?.code || 'en',
|
||||||
};
|
};
|
||||||
setSavedSettings(newSettings);
|
setSavedSettings(newSettings);
|
||||||
setDraftSettings(newSettings);
|
setDraftSettings(newSettings);
|
||||||
setMode(themeMode);
|
setMode(newSettings.theme);
|
||||||
|
i18n.changeLanguage(newSettings.language);
|
||||||
|
}
|
||||||
|
}, [profileData, setMode, i18n]);
|
||||||
|
|
||||||
i18n.changeLanguage(languageCode);
|
useEffect(() => {
|
||||||
} else {
|
if (saveData?.success) {
|
||||||
throw new Error(res.data.message || failRetrieve);
|
setMode(draftSettings.theme);
|
||||||
|
setSavedSettings(draftSettings);
|
||||||
|
i18n.changeLanguage(draftSettings.language);
|
||||||
|
setIsEditing(false);
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
}, [saveData, draftSettings, setMode, i18n]);
|
||||||
let message = errorFetch;
|
|
||||||
if (e instanceof Error) {
|
|
||||||
message = e.message;
|
|
||||||
}
|
|
||||||
setFetchError(message);
|
|
||||||
} finally {
|
|
||||||
setIsFetching(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUserSettings();
|
|
||||||
}, [token, setMode, i18n, notLoggedIn, failRetrieve, errorFetch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
setDraftSettings({
|
const resolvedMode = mode === 'light' || mode === 'dark' ? mode : 'light';
|
||||||
...savedSettings,
|
setDraftSettings({ ...savedSettings, theme: resolvedMode });
|
||||||
theme: mode === 'light' || mode === 'dark' ? mode : 'light',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [isEditing, savedSettings, mode]);
|
}, [isEditing, savedSettings, mode]);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setError(null);
|
setDraftSettings(savedSettings);
|
||||||
|
setMode(savedSettings.theme);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleEditToggle = () => {
|
||||||
setError(null);
|
if (isEditing) {
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const languageObj = languageOptions.find(
|
const languageObj = languageOptions.find(
|
||||||
(o) => o.code === draftSettings.language,
|
(o) => o.code === draftSettings.language,
|
||||||
);
|
);
|
||||||
const calendarObj = calendarOptions.find(
|
const calendarObj = calendarOptions.find(
|
||||||
(c) => c.key === draftSettings.calendar,
|
(c) => c.key === draftSettings.calendar,
|
||||||
);
|
);
|
||||||
const apiThemeValue = themeApiMap[draftSettings.theme];
|
|
||||||
|
|
||||||
if (!languageObj || !calendarObj) {
|
if (languageObj && calendarObj) {
|
||||||
setError(t('settings.invalidSelection'));
|
executeSaveSettings({
|
||||||
setLoading(false);
|
theme: themeApiMap[draftSettings.theme],
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await apiClient.post<SaveSettingApiResponse>(
|
|
||||||
'/Profile/SaveSetting',
|
|
||||||
{
|
|
||||||
theme: apiThemeValue,
|
|
||||||
calendarType: calendarObj.apiValue,
|
calendarType: calendarObj.apiValue,
|
||||||
language: languageObj.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) {
|
} else {
|
||||||
setError(t('settings.saveFailed'));
|
setIsEditing(true);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const getErrorMessage = (error: unknown): string | null => {
|
||||||
const handleEditToggle = () => {
|
if (!error) return null;
|
||||||
isEditing ? handleSave() : setIsEditing(true);
|
if (error instanceof Error) return error.message;
|
||||||
|
return String(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<Box sx={{ px: { xs: 0, sm: 3 }, mx: 0 }}>
|
|
||||||
<CardContainer
|
<CardContainer
|
||||||
title={t('settings.title')}
|
title={t('settings.title')}
|
||||||
subtitle={t('settings.description')}
|
subtitle={t('settings.description')}
|
||||||
@@ -217,7 +158,7 @@ export function Setting() {
|
|||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontSize: { xs: '0.85rem', sm: '1rem' },
|
fontSize: { xs: '0.85rem', sm: '1rem' },
|
||||||
}}
|
}}
|
||||||
disabled={loading || isFetching}
|
disabled={isSaving || isFetching}
|
||||||
>
|
>
|
||||||
{t('settings.rejectButton')}
|
{t('settings.rejectButton')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -225,17 +166,10 @@ export function Setting() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleEditToggle}
|
onClick={handleEditToggle}
|
||||||
size="large"
|
size="large"
|
||||||
variant="outlined"
|
variant={isEditing ? 'contained' : 'outlined'}
|
||||||
sx={{
|
disabled={isSaving || isFetching}
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'primary.main',
|
|
||||||
borderRadius: 1,
|
|
||||||
bgcolor: isEditing ? 'primary.main' : 'background.default',
|
|
||||||
color: isEditing ? 'primary.contrastText' : 'primary.main',
|
|
||||||
}}
|
|
||||||
disabled={loading || isFetching}
|
|
||||||
>
|
>
|
||||||
{loading
|
{isSaving
|
||||||
? t('settings.saving')
|
? t('settings.saving')
|
||||||
: isEditing
|
: isEditing
|
||||||
? t('settings.saveButton')
|
? t('settings.saveButton')
|
||||||
@@ -258,7 +192,9 @@ export function Setting() {
|
|||||||
</Box>
|
</Box>
|
||||||
) : fetchError ? (
|
) : fetchError ? (
|
||||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||||
<Typography color="error.main">{fetchError}</Typography>
|
<Typography color="error.main">
|
||||||
|
{getErrorMessage(fetchError)}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<Box
|
||||||
@@ -268,30 +204,26 @@ export function Setting() {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
bgcolor: 'background.paper',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{error && (
|
{getErrorMessage(saveError) && (
|
||||||
<Typography color="error.main" variant="body2" mb={2}>
|
<Typography color="error.main" variant="body2" mb={2}>
|
||||||
{error}
|
{getErrorMessage(saveError)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: { xs: 'column', sm: 'row' },
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
gap: 2,
|
gap: 4,
|
||||||
mt: 2,
|
mt: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
{isEditing ? (
|
<Typography variant="caption" color="text.secondary">
|
||||||
<Box
|
|
||||||
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
||||||
{t('settings.theme')}
|
{t('settings.theme')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{isEditing ? (
|
||||||
<ThemeToggleButton
|
<ThemeToggleButton
|
||||||
value={draftSettings.theme}
|
value={draftSettings.theme}
|
||||||
onChange={(newTheme) => {
|
onChange={(newTheme) => {
|
||||||
@@ -301,35 +233,23 @@ export function Setting() {
|
|||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
|
||||||
) : (
|
) : (
|
||||||
<Box>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
{t('settings.theme')}
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
Component={
|
Component={savedSettings.theme === 'light' ? Sun1 : Moon}
|
||||||
savedSettings.theme === 'light' ? Sun1 : Moon
|
|
||||||
}
|
|
||||||
size="medium"
|
size="medium"
|
||||||
variant="Bold"
|
variant="Bold"
|
||||||
color={
|
|
||||||
savedSettings.theme === 'light'
|
|
||||||
? 'text.primary'
|
|
||||||
: 'primary.main'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
{t(`settings.${savedSettings.theme}`)}
|
{t(`settings.${savedSettings.theme}`)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{t('settings.language')}
|
||||||
|
</Typography>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={languageOptions}
|
options={languageOptions}
|
||||||
@@ -337,7 +257,7 @@ export function Setting() {
|
|||||||
value={
|
value={
|
||||||
languageOptions.find(
|
languageOptions.find(
|
||||||
(o) => o.code === draftSettings.language,
|
(o) => o.code === draftSettings.language,
|
||||||
) || null
|
) // ✅ FIX: Removed '|| null'
|
||||||
}
|
}
|
||||||
onChange={(_, v) =>
|
onChange={(_, v) =>
|
||||||
v &&
|
v &&
|
||||||
@@ -346,17 +266,12 @@ export function Setting() {
|
|||||||
language: v.code,
|
language: v.code,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
renderInput={(p) => (
|
renderInput={(p) => <TextField {...p} />}
|
||||||
<TextField {...p} label={t('settings.language')} />
|
size="small"
|
||||||
)}
|
|
||||||
size="medium"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
|
disableClearable
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
{t('settings.language')}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
{
|
{
|
||||||
languageOptions.find(
|
languageOptions.find(
|
||||||
@@ -364,50 +279,38 @@ export function Setting() {
|
|||||||
)?.label
|
)?.label
|
||||||
}
|
}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ mt: 2, width: { xs: '100%', md: '50%' } }}>
|
<Box sx={{ mt: 2, width: { xs: '100%', sm: 'calc(50% - 16px)' } }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{t('settings.calendar')}
|
||||||
|
</Typography>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={calendarOptions.map((c) => c.key)}
|
options={calendarOptions.map((c) => c.key)}
|
||||||
getOptionLabel={(key) => t(`settings.${key}`)}
|
getOptionLabel={(key) => t(`settings.${key}`)}
|
||||||
value={draftSettings.calendar}
|
value={draftSettings.calendar}
|
||||||
onChange={(_, v) =>
|
onChange={(_, v) =>
|
||||||
v &&
|
v && setDraftSettings((prev) => ({ ...prev, calendar: v }))
|
||||||
setDraftSettings((prev) => ({ ...prev, calendar: v }))
|
|
||||||
}
|
}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => <TextField {...params} />}
|
||||||
<TextField {...params} label={t('settings.calendar')} />
|
size="small"
|
||||||
)}
|
|
||||||
size="medium"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
|
disableClearable
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Icon Component={Calendar1} size="medium" variant="Bold" />
|
||||||
{t('settings.calendar')}
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
||||||
<Icon
|
|
||||||
Component={Calendar1}
|
|
||||||
size="medium"
|
|
||||||
color={mode === 'light' ? 'black' : 'primary.main'}
|
|
||||||
variant="Bold"
|
|
||||||
/>
|
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
{t(`settings.${savedSettings.calendar}`)}
|
{t(`settings.${savedSettings.calendar}`)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</CardContainer>
|
</CardContainer>
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { InfoRowEdit } from './personalInformation/InfoRowEdit';
|
|||||||
import { PageWrapper } from '../PageWrapper';
|
import { PageWrapper } from '../PageWrapper';
|
||||||
import { useManualApi } from '@/hooks/useApi';
|
import { useManualApi } from '@/hooks/useApi';
|
||||||
import { Gender, type InfoRowData } from '../../types/settingsType';
|
import { Gender, type InfoRowData } from '../../types/settingsType';
|
||||||
import { fetchProfile, saveProfile, getToken } from '../../api/settingsApi';
|
import { fetchProfile, saveProfile } from '../../api/settingsApi';
|
||||||
|
|
||||||
export function PersonalInformation() {
|
export function PersonalInformation() {
|
||||||
const { t } = useTranslation('setting');
|
const { t } = useTranslation('setting');
|
||||||
@@ -37,13 +37,6 @@ export function PersonalInformation() {
|
|||||||
execute: executeSaveProfile,
|
execute: executeSaveProfile,
|
||||||
} = useManualApi(saveProfile);
|
} = useManualApi(saveProfile);
|
||||||
|
|
||||||
const {
|
|
||||||
data: tokenData,
|
|
||||||
loading: isGettingToken,
|
|
||||||
error: tokenError,
|
|
||||||
execute: executeGetToken,
|
|
||||||
} = useManualApi(getToken);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
executeFetchProfile();
|
executeFetchProfile();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -91,10 +84,6 @@ export function PersonalInformation() {
|
|||||||
executeSaveProfile({ data, imageUrl: uploadedImageUrl });
|
executeSaveProfile({ data, imageUrl: uploadedImageUrl });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGetTokenClick = async () => {
|
|
||||||
executeGetToken();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown): string | null => {
|
const getErrorMessage = (error: unknown): string | null => {
|
||||||
if (!error) return null;
|
if (!error) return null;
|
||||||
if (error instanceof Error) return error.message;
|
if (error instanceof Error) return error.message;
|
||||||
@@ -124,16 +113,6 @@ export function PersonalInformation() {
|
|||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
onClick={handleGetTokenClick}
|
|
||||||
size="large"
|
|
||||||
sx={{ textTransform: 'none' }}
|
|
||||||
disabled={isLoadingProfile || isGettingToken || isSavingProfile}
|
|
||||||
>
|
|
||||||
{isGettingToken ? <CircularProgress size={24} /> : 'Get Token'}
|
|
||||||
</Button>
|
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@@ -145,7 +124,6 @@ export function PersonalInformation() {
|
|||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
width: { xs: '100%', sm: 'auto' },
|
width: { xs: '100%', sm: 'auto' },
|
||||||
}}
|
}}
|
||||||
disabled={isSavingProfile}
|
|
||||||
>
|
>
|
||||||
{t('settingForm.rejectButton')}
|
{t('settingForm.rejectButton')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -186,14 +164,6 @@ export function PersonalInformation() {
|
|||||||
{getErrorMessage(saveProfileError)}
|
{getErrorMessage(saveProfileError)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{tokenData?.success && (
|
|
||||||
<Typography
|
|
||||||
color="green"
|
|
||||||
sx={{ mt: 1, textAlign: 'right', width: '100%' }}
|
|
||||||
>
|
|
||||||
Token fetched and stored successfully!
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -218,11 +188,6 @@ export function PersonalInformation() {
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{getErrorMessage(tokenError) && (
|
|
||||||
<Box sx={{ p: 2, mx: { xs: 2, sm: 3, md: 4 }, color: 'red' }}>
|
|
||||||
Error: {getErrorMessage(tokenError)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
mx: { xs: 2, sm: 3, md: 4 },
|
mx: { xs: 2, sm: 3, md: 4 },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CardContainer } from '@/components/CardContainer';
|
import { CardContainer } from '@/components/CardContainer';
|
||||||
import { PageWrapper } from '../PageWrapper';
|
import { PageWrapper } from '../PageWrapper';
|
||||||
@@ -7,8 +7,14 @@ import SocialMediaMenu from './socialMedia/SocialMediaMenu';
|
|||||||
import SocialMediaDialog from './socialMedia/SocialMediaDialog';
|
import SocialMediaDialog from './socialMedia/SocialMediaDialog';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import type { Theme } from '@mui/material/styles';
|
import type { Theme } from '@mui/material/styles';
|
||||||
import apiClient from '@/lib/apiClient';
|
|
||||||
import { Box, CircularProgress, Typography } from '@mui/material';
|
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||||
|
import { useManualApi } from '@/hooks/useApi';
|
||||||
|
import {
|
||||||
|
fetchProfile,
|
||||||
|
sendEmailCode,
|
||||||
|
confirmEmailCode,
|
||||||
|
changeEmail,
|
||||||
|
} from '../../api/settingsApi';
|
||||||
|
|
||||||
interface EmailAccount {
|
interface EmailAccount {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -16,31 +22,8 @@ interface EmailAccount {
|
|||||||
time: string;
|
time: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetProfileApiResponse {
|
|
||||||
success: boolean;
|
|
||||||
message?: string;
|
|
||||||
email?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SendCodeApiResponse {
|
|
||||||
success: boolean;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConfirmCodeApiResponse {
|
|
||||||
success: boolean;
|
|
||||||
confirm?: boolean;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChangeEmailApiResponse {
|
|
||||||
success: boolean;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SocialMedia() {
|
export function SocialMedia() {
|
||||||
const { t } = useTranslation('setting');
|
const { t } = useTranslation('setting');
|
||||||
const token = localStorage.getItem('authToken');
|
|
||||||
|
|
||||||
const [openDialog, setOpenDialog] = useState(false);
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
const [emailInput, setEmailInput] = useState('');
|
const [emailInput, setEmailInput] = useState('');
|
||||||
@@ -48,12 +31,36 @@ export function SocialMedia() {
|
|||||||
const [dialogStep, setDialogStep] = useState<'enterEmail' | 'enterCode'>(
|
const [dialogStep, setDialogStep] = useState<'enterEmail' | 'enterCode'>(
|
||||||
'enterEmail',
|
'enterEmail',
|
||||||
);
|
);
|
||||||
const [apiError, setApiError] = useState<string | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [emailList, setEmailList] = useState<EmailAccount[]>([]);
|
const [emailList, setEmailList] = useState<EmailAccount[]>([]);
|
||||||
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [isFetching, setIsFetching] = useState(true);
|
const {
|
||||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
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) =>
|
const fullScreen = useMediaQuery((theme: Theme) =>
|
||||||
theme.breakpoints.down('sm'),
|
theme.breakpoints.down('sm'),
|
||||||
@@ -67,128 +74,85 @@ export function SocialMedia() {
|
|||||||
| 'xl';
|
| 'xl';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchInitialEmail = async () => {
|
executeFetchProfile();
|
||||||
setIsFetching(true);
|
}, []);
|
||||||
setFetchError(null);
|
|
||||||
if (!token) {
|
|
||||||
setIsFetching(false);
|
|
||||||
setFetchError(t('settingForm.notLoggedIn'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await apiClient.post<GetProfileApiResponse>(
|
|
||||||
'/Profile/GetProfile',
|
|
||||||
{},
|
|
||||||
{ headers: { Authorization: `Bearer ${token}` } },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.data.success && res.data.email) {
|
useEffect(() => {
|
||||||
const userEmail = res.data.email;
|
if (profileData?.success && profileData.email) {
|
||||||
const newAccount: EmailAccount = {
|
const { email } = profileData;
|
||||||
email: userEmail,
|
setEmailList([
|
||||||
provider: userEmail.includes('gmail.com') ? 'google' : 'email',
|
{
|
||||||
|
email,
|
||||||
|
provider: email.includes('@gmail.') ? 'google' : 'email',
|
||||||
time: '',
|
time: '',
|
||||||
};
|
},
|
||||||
setEmailList([newAccount]);
|
]);
|
||||||
} else if (!res.data.success) {
|
|
||||||
throw new Error(res.data.message || t('settingForm.failFetchEmail'));
|
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
}, [profileData]);
|
||||||
let message = t('settingForm.errorFetchEmail');
|
|
||||||
if (err instanceof Error) {
|
|
||||||
message = err.message;
|
|
||||||
}
|
|
||||||
setFetchError(message);
|
|
||||||
} finally {
|
|
||||||
setIsFetching(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchInitialEmail();
|
useEffect(() => {
|
||||||
}, [token, t]);
|
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 = () => {
|
const resetDialog = () => {
|
||||||
setOpenDialog(false);
|
setOpenDialog(false);
|
||||||
setEmailInput('');
|
setEmailInput('');
|
||||||
setVerificationCode('');
|
setVerificationCode('');
|
||||||
setApiError(null);
|
setFormError(null);
|
||||||
setIsLoading(false);
|
|
||||||
setDialogStep('enterEmail');
|
setDialogStep('enterEmail');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendCode = async () => {
|
const handleSendCode = () => {
|
||||||
if (!/^\S+@\S+\.\S+$/.test(emailInput)) {
|
if (!/^\S+@\S+\.\S+$/.test(emailInput)) {
|
||||||
setApiError(t('settingForm.emailIsInvalid'));
|
setFormError(t('settingForm.emailIsInvalid'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setFormError(null);
|
||||||
setApiError(null);
|
executeSendCode({ email: emailInput });
|
||||||
try {
|
|
||||||
const res = await apiClient.post<SendCodeApiResponse>(
|
|
||||||
'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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmAndChangeEmail = async () => {
|
const handleConfirmAndChangeEmail = () => {
|
||||||
if (verificationCode.length < 4) {
|
if (verificationCode.length < 4) {
|
||||||
setApiError(t('settingForm.verificationCodeRequired'));
|
setFormError(t('settingForm.verificationCodeRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setFormError(null);
|
||||||
setApiError(null);
|
executeConfirmCode({ email: emailInput, verifyCode: verificationCode });
|
||||||
try {
|
|
||||||
const confirmRes = await apiClient.post<ConfirmCodeApiResponse>(
|
|
||||||
'Profile/ConfirmEmailChangeCode',
|
|
||||||
{ email: emailInput, verifyCode: verificationCode },
|
|
||||||
{ headers: { Authorization: `Bearer ${token}` } },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmRes.data.success && confirmRes.data.confirm) {
|
|
||||||
const changeRes = await apiClient.post<ChangeEmailApiResponse>(
|
|
||||||
'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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<CardContainer
|
<CardContainer
|
||||||
@@ -212,7 +176,9 @@ export function SocialMedia() {
|
|||||||
</Box>
|
</Box>
|
||||||
) : fetchError ? (
|
) : fetchError ? (
|
||||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||||
<Typography color="error.main">{fetchError}</Typography>
|
<Typography color="error.main">
|
||||||
|
{getErrorMessage(fetchError)}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<SocialMediaList t={t} emailList={emailList} />
|
<SocialMediaList t={t} emailList={emailList} />
|
||||||
@@ -225,8 +191,8 @@ export function SocialMedia() {
|
|||||||
setEmailInput={setEmailInput}
|
setEmailInput={setEmailInput}
|
||||||
verificationCode={verificationCode}
|
verificationCode={verificationCode}
|
||||||
setVerificationCode={setVerificationCode}
|
setVerificationCode={setVerificationCode}
|
||||||
apiError={apiError}
|
apiError={formError || apiError}
|
||||||
isLoading={isLoading}
|
isLoading={isSendingCode || isConfirming || isChangingEmail}
|
||||||
dialogStep={dialogStep}
|
dialogStep={dialogStep}
|
||||||
onSendCode={handleSendCode}
|
onSendCode={handleSendCode}
|
||||||
onConfirmEmail={handleConfirmAndChangeEmail}
|
onConfirmEmail={handleConfirmAndChangeEmail}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Edit2, TickCircle } from 'iconsax-react';
|
import { Edit2, TickCircle } from 'iconsax-react';
|
||||||
import { CountDownTimer } from '@/components/CountDownTimer';
|
import { CountDownTimer } from '@/components/CountDownTimer';
|
||||||
import { Toast } from '@/components/Toast';
|
|
||||||
import { CountryCodeSelector } from '../../CountryCodeSelector';
|
import { CountryCodeSelector } from '../../CountryCodeSelector';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
|
|
||||||
@@ -217,14 +216,6 @@ export default function PhoneEditForm({
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Toast
|
|
||||||
color="success"
|
|
||||||
open={showToast}
|
|
||||||
onClose={() => setShowToast(false)}
|
|
||||||
>
|
|
||||||
{t('settingForm.successfulChangePhone')}
|
|
||||||
</Toast>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { RouterProvider } from 'react-router-dom';
|
|
||||||
import { router } from '../components/layout/Routes';
|
|
||||||
|
|
||||||
export function Settings() {
|
|
||||||
return <RouterProvider router={router} />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,29 @@
|
|||||||
import { Gender } from './settingsType';
|
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 */
|
/* Profile API Types */
|
||||||
export interface GetProfileApiResponse {
|
export interface GetProfileApiResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -10,9 +34,13 @@ export interface GetProfileApiResponse {
|
|||||||
gender?: Gender;
|
gender?: Gender;
|
||||||
countryCode?: string;
|
countryCode?: string;
|
||||||
profileImageUrl?: string;
|
profileImageUrl?: string;
|
||||||
phoneNumber?: string; // ✅ ADDED
|
phoneNumber?: string;
|
||||||
|
userSettings?: UserSettingsFromApi;
|
||||||
|
activeSessions?: ActiveSessionsData;
|
||||||
|
loginLogs?: LoginLog[];
|
||||||
|
email?: string;
|
||||||
|
hasPassword?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SaveProfileApiResponse {
|
export interface SaveProfileApiResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -20,8 +48,37 @@ export interface SaveProfileApiResponse {
|
|||||||
email?: string;
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TokenApiResponse {
|
/* Settings API Types */
|
||||||
access_token: string;
|
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 */
|
/* Phone Number API Types */
|
||||||
@@ -29,7 +86,6 @@ export interface PhoneNumberApiResponse {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfirmPhoneNumberApiResponse extends PhoneNumberApiResponse {
|
export interface ConfirmPhoneNumberApiResponse extends PhoneNumberApiResponse {
|
||||||
confirm?: boolean;
|
confirm?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,36 @@ export interface InfoRowData {
|
|||||||
country: string;
|
country: string;
|
||||||
gender: Gender;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const apiClient = axios.create({
|
|||||||
|
|
||||||
// Set default headers
|
// Set default headers
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
// Accept: 'application/json',
|
||||||
|
Authorization: 'Bearer ' + getToken(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
13
src/types/apiResponse.ts
Normal file
13
src/types/apiResponse.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
38
src/utils/formatSessionDate.tsx
Normal file
38
src/utils/formatSessionDate.tsx
Normal file
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user