chore: move api to api folder and seperate the types into another file
This commit is contained in:
@@ -9,32 +9,11 @@ import {
|
||||
Button,
|
||||
Link,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { CloseCircle } from 'iconsax-react';
|
||||
import { PasswordValidationItem } from './PasswordValidation';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
|
||||
interface PasswordDialogProps {
|
||||
open: boolean;
|
||||
fullScreen: boolean;
|
||||
handleClose: () => void;
|
||||
password: string;
|
||||
setPassword: (val: string) => void;
|
||||
confirmPassword: string;
|
||||
setConfirmPassword: (val: string) => void;
|
||||
currentPassword: string;
|
||||
setCurrentPassword: (val: string) => void;
|
||||
showValidation: boolean;
|
||||
hasNumber: boolean;
|
||||
hasMinLength: boolean;
|
||||
hasUpperAndLower: boolean;
|
||||
hasSpecialChar: boolean;
|
||||
matchPassword: boolean;
|
||||
loading: boolean;
|
||||
handleShowAlert: () => void;
|
||||
changePassword: boolean;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
import { type PasswordDialogProps } from '../../types/settingsType';
|
||||
|
||||
export function PasswordDialog({
|
||||
open,
|
||||
@@ -47,14 +26,12 @@ export function PasswordDialog({
|
||||
currentPassword,
|
||||
setCurrentPassword,
|
||||
showValidation,
|
||||
hasNumber,
|
||||
hasMinLength,
|
||||
hasUpperAndLower,
|
||||
hasSpecialChar,
|
||||
validPassword,
|
||||
matchPassword,
|
||||
loading,
|
||||
handleShowAlert,
|
||||
handleSubmit,
|
||||
changePassword,
|
||||
apiError,
|
||||
t,
|
||||
}: PasswordDialogProps) {
|
||||
return (
|
||||
@@ -73,7 +50,9 @@ export function PasswordDialog({
|
||||
<Icon Component={CloseCircle} size="large" color="primary.main" />
|
||||
</IconButton>
|
||||
<Box component="span" fontWeight="bold" fontSize={18}>
|
||||
{t('securityForm.addPassword')}
|
||||
{changePassword
|
||||
? t('securityForm.changePassword')
|
||||
: t('securityForm.addPassword')}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
@@ -86,16 +65,24 @@ export function PasswordDialog({
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
{/* ✅ 4. API ERROR DISPLAY ADDED */}
|
||||
{apiError && (
|
||||
<Typography color="error" variant="body2" textAlign="center">
|
||||
{apiError}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{changePassword && (
|
||||
<>
|
||||
<TextField
|
||||
label={t('securityForm.currentPassword')}
|
||||
type="password"
|
||||
fullWidth
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
/>
|
||||
<Link sx={{ fontSize: '15px' }}>
|
||||
<Link sx={{ fontSize: '15px', cursor: 'pointer' }}>
|
||||
{t('securityForm.forgetPassword')}
|
||||
</Link>
|
||||
</>
|
||||
@@ -110,27 +97,15 @@ export function PasswordDialog({
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
/>
|
||||
|
||||
{password && showValidation && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ maxWidth: '364px', width: '100%' }}>
|
||||
<PasswordValidationItem
|
||||
isValid={hasNumber}
|
||||
label={t('securityForm.hasNumber')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasMinLength}
|
||||
label={t('securityForm.hasMinLength')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasUpperAndLower}
|
||||
label={t('securityForm.hasUpperAndLower')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasSpecialChar}
|
||||
label={t('securityForm.hasSpecialChar')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{showValidation && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={validPassword ? 'success.main' : 'error.main'}
|
||||
>
|
||||
{validPassword
|
||||
? t('securityForm.passwordIsValid')
|
||||
: t('securityForm.passwordIsInvalid')}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
@@ -154,10 +129,14 @@ export function PasswordDialog({
|
||||
fullWidth
|
||||
sx={{ height: 48, textTransform: 'none' }}
|
||||
variant="contained"
|
||||
onClick={handleShowAlert}
|
||||
disabled={!password || password !== confirmPassword || loading}
|
||||
onClick={handleSubmit}
|
||||
disabled={!validPassword || !matchPassword || loading}
|
||||
>
|
||||
{loading ? <CircularProgress size={20} /> : t('securityForm.confirm')}
|
||||
{loading ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
) : (
|
||||
t('securityForm.confirm')
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
@@ -10,9 +11,13 @@ import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
import { PasswordDialog } from './PasswordDialog';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { regex } from '@/utils/regex';
|
||||
import apiClient from '@/lib/apiClient';
|
||||
import { useManualApi } from '@/hooks/useApi';
|
||||
import {
|
||||
addPassword,
|
||||
changePassword,
|
||||
fetchProfile,
|
||||
} from '../../api/settingsApi';
|
||||
|
||||
export function PasswordSecurity() {
|
||||
const theme = useTheme();
|
||||
@@ -25,158 +30,161 @@ export function PasswordSecurity() {
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [showValidation, setShowValidation] = useState(false);
|
||||
const [showPasswordAlert, setShowPasswordAlert] = useState(false);
|
||||
const [changePassword, setChangePassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [changePasswordUI, setChangePasswordUI] = useState(false);
|
||||
|
||||
const { validPassword } = regex(password);
|
||||
const matchPassword = password === confirmPassword;
|
||||
|
||||
const {
|
||||
hasNumber,
|
||||
hasMinLength,
|
||||
hasUpperAndLower,
|
||||
hasSpecialChar,
|
||||
validPassword,
|
||||
} = regex(password);
|
||||
data: profileData,
|
||||
loading: isFetching,
|
||||
error: fetchError,
|
||||
execute: executeFetchProfile,
|
||||
} = useManualApi(fetchProfile);
|
||||
|
||||
const matchPassword = password === confirmPassword;
|
||||
const {
|
||||
data: addData,
|
||||
loading: isAdding,
|
||||
error: addError,
|
||||
execute: executeAddPassword,
|
||||
} = useManualApi(addPassword);
|
||||
|
||||
const {
|
||||
data: changeData,
|
||||
loading: isChanging,
|
||||
error: changeError,
|
||||
execute: executeChangePassword,
|
||||
} = useManualApi(changePassword);
|
||||
|
||||
useEffect(() => {
|
||||
executeFetchProfile();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (profileData?.success) {
|
||||
setChangePasswordUI(profileData.hasPassword || false);
|
||||
}
|
||||
}, [profileData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (password) {
|
||||
if (!validPassword) {
|
||||
setShowValidation(true);
|
||||
} else {
|
||||
const timer = setTimeout(() => setShowValidation(false), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
setShowValidation(!validPassword);
|
||||
} else {
|
||||
setShowValidation(false);
|
||||
}
|
||||
}, [password, validPassword]);
|
||||
|
||||
useEffect(() => {
|
||||
if (addData?.success || changeData?.success) {
|
||||
setShowPasswordAlert(true);
|
||||
if (!changePasswordUI) {
|
||||
setChangePasswordUI(true);
|
||||
}
|
||||
setOpen(false);
|
||||
setPassword('');
|
||||
setConfirmPassword('');
|
||||
setCurrentPassword('');
|
||||
}
|
||||
}, [addData, changeData, changePasswordUI]);
|
||||
|
||||
const handleOpen = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
const handleShowAlert = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const url = changePassword
|
||||
? 'Profile/ChangePassword'
|
||||
: 'Profile/AddPassword';
|
||||
|
||||
const payload = changePassword
|
||||
? {
|
||||
oldPassword: currentPassword,
|
||||
newPassword: password,
|
||||
}
|
||||
: {
|
||||
password,
|
||||
// confirmPassword,
|
||||
};
|
||||
|
||||
const res = await apiClient.post(url, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('authToken')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
const handlePasswordSubmit = () => {
|
||||
if (changePasswordUI) {
|
||||
executeChangePassword({
|
||||
oldPassword: currentPassword,
|
||||
newPassword: password,
|
||||
});
|
||||
|
||||
if (res.data?.success) {
|
||||
setShowPasswordAlert(true);
|
||||
if (!changePassword) setChangePassword(true);
|
||||
handleClose();
|
||||
setPassword('');
|
||||
setConfirmPassword('');
|
||||
setCurrentPassword('');
|
||||
} else {
|
||||
console.error('Password update failed:', res.data?.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating password:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
} else {
|
||||
executeAddPassword({ password });
|
||||
}
|
||||
};
|
||||
|
||||
const getErrorMessage = (error: unknown): string | null => {
|
||||
if (!error) return null;
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
};
|
||||
|
||||
const apiError = useMemo(
|
||||
() => addError || changeError,
|
||||
[addError, changeError],
|
||||
);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<CardContainer
|
||||
title={t('securityForm.password')}
|
||||
subtitle={t('securityForm.determinePassword')}
|
||||
action={
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'background.paper',
|
||||
textTransform: 'none',
|
||||
}}
|
||||
>
|
||||
{changePassword
|
||||
<Button variant="contained" onClick={handleOpen}>
|
||||
{changePasswordUI
|
||||
? t('securityForm.changePassword')
|
||||
: t('securityForm.addPassword')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
px: { xs: 2, sm: 3, md: 4 },
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
bgcolor: 'background.paper',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{changePassword ? (
|
||||
<Box sx={{ flexDirection: 'column', px: 2, py: 2 }}>
|
||||
<Typography variant="h6">
|
||||
{t('securityForm.activePassword')}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('securityForm.lastChange')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', py: { xs: 4, sm: '43.5px' } }}
|
||||
>
|
||||
{t('securityForm.notDeterminedPassword')}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<PasswordDialog
|
||||
open={open}
|
||||
fullScreen={fullScreen}
|
||||
handleClose={handleClose}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
confirmPassword={confirmPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
currentPassword={currentPassword}
|
||||
setCurrentPassword={setCurrentPassword}
|
||||
showValidation={showValidation}
|
||||
hasNumber={hasNumber}
|
||||
hasMinLength={hasMinLength}
|
||||
hasUpperAndLower={hasUpperAndLower}
|
||||
hasSpecialChar={hasSpecialChar}
|
||||
matchPassword={matchPassword}
|
||||
loading={loading}
|
||||
handleShowAlert={handleShowAlert}
|
||||
changePassword={changePassword}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<Toast
|
||||
color="success"
|
||||
open={showPasswordAlert}
|
||||
onClose={() => setShowPasswordAlert(false)}
|
||||
{isFetching ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: 4,
|
||||
minHeight: '120px',
|
||||
}}
|
||||
>
|
||||
{t('securityForm.alertSuccess')}
|
||||
</Toast>
|
||||
</Box>
|
||||
<CircularProgress />
|
||||
</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">
|
||||
{t('securityForm.activePassword')}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('securityForm.lastChange')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', py: 4 }}
|
||||
>
|
||||
{t('securityForm.notDeterminedPassword')}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<PasswordDialog
|
||||
open={open}
|
||||
fullScreen={fullScreen}
|
||||
handleClose={handleClose}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
confirmPassword={confirmPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
currentPassword={currentPassword}
|
||||
setCurrentPassword={setCurrentPassword}
|
||||
showValidation={showValidation}
|
||||
validPassword={validPassword}
|
||||
matchPassword={matchPassword}
|
||||
loading={isAdding || isChanging}
|
||||
handleSubmit={handlePasswordSubmit}
|
||||
changePassword={changePasswordUI}
|
||||
apiError={getErrorMessage(apiError)}
|
||||
t={t}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</CardContainer>
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { TickCircle } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
|
||||
interface ValidationItemProps {
|
||||
isValid: boolean;
|
||||
label: string;
|
||||
}
|
||||
import { type ValidationItemProps } from '../../types/settingsType';
|
||||
|
||||
export function PasswordValidationItem({
|
||||
isValid,
|
||||
|
||||
@@ -2,116 +2,46 @@ import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
import apiClient from '@/lib/apiClient';
|
||||
|
||||
function formatLoginDate(isoDate: string, lang: string, t: TFunction): string {
|
||||
const date = new Date(isoDate);
|
||||
const now = new Date();
|
||||
const diffInMinutes = Math.floor(
|
||||
(now.getTime() - date.getTime()) / (1000 * 60),
|
||||
);
|
||||
|
||||
if (diffInMinutes < 1) return t('active.justNow');
|
||||
if (diffInMinutes < 60)
|
||||
return t('active.minutesAgo', { count: diffInMinutes });
|
||||
|
||||
let displayLocale: string;
|
||||
let options: Intl.DateTimeFormatOptions;
|
||||
|
||||
if (lang.startsWith('fa')) {
|
||||
displayLocale = 'fa-IR';
|
||||
options = {
|
||||
calendar: 'persian',
|
||||
numberingSystem: 'arab',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
};
|
||||
} else {
|
||||
displayLocale = 'en-US';
|
||||
options = {
|
||||
calendar: 'gregory',
|
||||
numberingSystem: 'latn',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
};
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(displayLocale, options).format(date);
|
||||
}
|
||||
|
||||
interface LoginLog {
|
||||
loginDateTime: string;
|
||||
ipAddress: string;
|
||||
deviceName: string;
|
||||
deviceOs: string;
|
||||
}
|
||||
|
||||
interface ProfileApiResponse {
|
||||
success: boolean;
|
||||
loginLogs?: LoginLog[];
|
||||
}
|
||||
import { fetchProfile } from '../../api/settingsApi';
|
||||
import type { LoginLog } from '../../types/settingsApiType';
|
||||
import { useManualApi } from '@/hooks/useApi';
|
||||
import { formatDate } from '@/utils/formatSessionDate'; // ✅ 1. IMPORT the shared function
|
||||
|
||||
export function RecentLogins() {
|
||||
const { t, i18n } = useTranslation('setting');
|
||||
const token = localStorage.getItem('authToken');
|
||||
|
||||
const [logs, setLogs] = useState<LoginLog[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||
|
||||
const theme = useTheme();
|
||||
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
||||
|
||||
const {
|
||||
data: profileData,
|
||||
loading: isLoading,
|
||||
error: fetchError,
|
||||
execute: executeFetchProfile,
|
||||
} = useManualApi(fetchProfile);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLoginLogs = async () => {
|
||||
setIsLoading(true);
|
||||
setFetchError(null);
|
||||
executeFetchProfile();
|
||||
}, [i18n.language]);
|
||||
|
||||
if (!token) {
|
||||
setIsLoading(false);
|
||||
setFetchError(t('active.notLoggedIn'));
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (profileData?.success && Array.isArray(profileData.loginLogs)) {
|
||||
setLogs(profileData.loginLogs);
|
||||
}
|
||||
}, [profileData]);
|
||||
|
||||
try {
|
||||
const res = await apiClient.post<ProfileApiResponse>(
|
||||
'/Profile/GetProfile',
|
||||
{},
|
||||
{ headers: { Authorization: `Bearer ${token}` } },
|
||||
);
|
||||
|
||||
if (res.data.success && Array.isArray(res.data.loginLogs)) {
|
||||
setLogs(res.data.loginLogs);
|
||||
} else {
|
||||
throw new Error(t('active.failFetchActiveSessions'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(t('active.failFetchActiveSessions'), err);
|
||||
setFetchError(t('active.errorFetch'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLoginLogs();
|
||||
}, [token, i18n.language, t]);
|
||||
const getErrorMessage = (error: unknown): string | null => {
|
||||
if (!error) return null;
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
@@ -133,7 +63,9 @@ export function RecentLogins() {
|
||||
</Box>
|
||||
) : fetchError ? (
|
||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||
<Typography color="error.main">{fetchError}</Typography>
|
||||
<Typography color="error.main">
|
||||
{getErrorMessage(fetchError) || t('active.errorFetch')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ px: 4 }}>
|
||||
@@ -142,45 +74,26 @@ export function RecentLogins() {
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
minHeight: 50,
|
||||
py: 1.5,
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
flexBasis: { xs: '100%', sm: 'auto' },
|
||||
mb: { xs: 1, sm: 0 },
|
||||
minWidth: { sm: '172.5px' },
|
||||
}}
|
||||
>
|
||||
{formatLoginDate(log.loginDateTime, i18n.language, t)}
|
||||
<Typography sx={{ flex: 1, minWidth: 160 }}>
|
||||
{formatDate(log.loginDateTime, i18n.language, t)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
flexBasis: { xs: '100%', sm: 'auto' },
|
||||
mb: { xs: 1, sm: 0 },
|
||||
minWidth: { sm: '172.5px' },
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ flex: 1, minWidth: 160 }}>
|
||||
{`${log.deviceOs} ${log.deviceName}`}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
flexBasis: { xs: '100%', sm: 'auto' },
|
||||
mb: { xs: 1, sm: 0 },
|
||||
minWidth: { sm: '172.5px' },
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ flex: 1, minWidth: 120 }}>
|
||||
{log.ipAddress}
|
||||
</Typography>
|
||||
</Box>
|
||||
{isXsup && (
|
||||
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
|
||||
{isXsup && index < logs.length - 1 && (
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user