Files
Account/src/features/profile/routes/ActiveDevicesPage.tsx
2025-08-21 14:51:56 +03:30

302 lines
9.7 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
useTheme,
useMediaQuery,
CircularProgress,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { DeviceMessage, Logout } from 'iconsax-react';
import { CardContainer } from '@/components/CardContainer';
import { PageWrapper } from '../components/PageWrapper';
import { Icon } from '@rkheftan/harmony-ui';
import { deleteSessions } from '../api/settingsApi';
import { type ApiSession } from '../types/settingsApiType';
import { useApi } from '@/hooks/useApi';
import { formatDate } from '@/utils/formatSessionDate';
import { type Device } from '../types/settingsType';
import { useToast } from '@rkheftan/harmony-ui';
import { useProfile } from '../hooks/useProfile';
export function ActiveDevicesPage() {
const { t, i18n } = useTranslation('setting');
const [devices, setDevices] = useState<Device[]>([]);
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
const theme = useTheme();
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
const showToast = useToast();
const { isLoadingProfile, refetchProfile } = useProfile();
const { loading: isTerminating, execute: executeTerminate } =
useApi(deleteSessions);
useEffect(() => {
const loadProfile = async () => {
const profileData = await refetchProfile();
if (!profileData) return;
if (profileData.success) {
if (!profileData.activeSessions) return;
const { sessions, currentKey } = profileData.activeSessions;
const formattedDevices = sessions.map((session: ApiSession) => ({
id: session.key,
timeAndDate: formatDate(session.created, i18n.language, t),
deviceModel: `${session.deviceOs} ${session.deviceName}`,
ip: session.ipAddress,
current: session.key === currentKey,
}));
const sortedDevices = formattedDevices.sort((a, b) => {
if (a.current && !b.current) return -1;
if (!a.current && b.current) return 1;
return 0;
});
setDevices(sortedDevices);
} else {
showToast({
message: profileData.message || t('message.serverError'),
severity: 'error',
});
}
};
loadProfile();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleDeleteDevice = async (id: string) => {
if (loadingDeleteIds.includes(id)) return;
setLoadingDeleteIds((prev) => [...prev, id]);
try {
const data = await executeTerminate({ keys: [id] });
if (!data) return;
if (data.success) {
setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id));
showToast({ message: t('active.successDelete'), severity: 'success' });
} else {
showToast({
message: data.message || t('active.deleteFailed'),
severity: 'error',
});
}
} finally {
setLoadingDeleteIds((prev) =>
prev.filter((loadingId) => loadingId !== id),
);
}
};
const handleTerminateAllOtherSessions = async () => {
const otherSessionKeys = devices.filter((d) => !d.current).map((d) => d.id);
if (otherSessionKeys.length > 0) {
const terminateData = await executeTerminate({ keys: otherSessionKeys });
if (!terminateData) return;
if (terminateData?.success) {
setDevices((prev) => prev.filter((d) => d.current));
} else {
showToast({
message: terminateData.message || t('active.deleteFailed'),
severity: 'error',
});
}
}
};
return (
<PageWrapper>
<CardContainer
title={t('active.activeDevices')}
subtitle={t('active.activeDevicesCaption')}
action={
<Button
size="medium"
variant="outlined"
onClick={handleTerminateAllOtherSessions}
sx={{
borderRadius: 1,
borderColor: 'error.main',
color: 'error.main',
textTransform: 'none',
flexShrink: 0,
flexGrow: 0,
width: 'auto',
}}
disabled={isLoadingProfile || isTerminating}
>
{isTerminating
? t('active.deleting')
: t('active.deleteDevicesButton')}
</Button>
}
>
{isLoadingProfile ? (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
p: 4,
minHeight: '200px',
}}
>
<CircularProgress />
</Box>
) : (
<Box
sx={{
px: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
}}
>
{devices.map((device) => (
<React.Fragment key={device.id}>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'flex-start', sm: 'center' },
minHeight: 50,
py: 1.5,
}}
>
<Typography
variant="body2"
sx={{
flexBasis: { xs: '100%', sm: 'auto' },
mb: { xs: 1, sm: 0 },
minWidth: { sm: '138px' },
order: { xs: 1, sm: 1 },
}}
>
{device.timeAndDate}
</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
width: '100%',
order: { xs: 2, sm: 2 },
}}
>
<Icon
Component={DeviceMessage}
size="medium"
color="primary.main"
/>
<Typography variant="body2" noWrap>
{device.deviceModel}
</Typography>
</Box>
<Typography
variant="body2"
sx={{
flexBasis: { xs: '100%', sm: 'auto' },
mb: { xs: 1, sm: 0 },
minWidth: { sm: '138px' },
order: { xs: 3, sm: 3 },
}}
>
{device.ip}
</Typography>
<Box
sx={{
flexBasis: { xs: '100%', sm: 'auto' },
mb: { xs: 1, sm: 0 },
minWidth: { sm: '138px' },
order: { xs: 4, sm: 4 },
alignItems: 'center',
justifyContent: 'center',
}}
>
{device.current && (
<Button
variant="outlined"
size="medium"
sx={{
borderRadius: 12.5,
border: '1px solid',
borderColor: 'success.main',
whiteSpace: 'nowrap',
color: 'success.main',
textTransform: 'none',
maxWidth: '125px',
'&.Mui-disabled': {
color: 'success.main',
borderColor: 'success.main',
},
}}
disabled
>
{t('active.currentDevice')}
</Button>
)}
</Box>
<Box
sx={{
flexBasis: { xs: '100%', sm: 'auto' },
mb: { xs: 1, sm: 0 },
textAlign: { xs: 'left', sm: 'center' },
minWidth: { sm: '138px' },
order: { xs: 5, sm: 5 },
}}
>
<Button
size="small"
variant="outlined"
startIcon={<Icon Component={Logout} size="small" />}
disabled={
device.current || loadingDeleteIds.includes(device.id)
}
onClick={() => handleDeleteDevice(device.id)}
sx={{
color: 'error.main',
borderRadius: 1,
borderColor: 'error.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
whiteSpace: 'nowrap',
textTransform: 'none',
'& .MuiButton-startIcon': {
marginRight: 0.5,
marginLeft: 0,
},
}}
>
{loadingDeleteIds.includes(device.id) ? (
<CircularProgress size={20} color="error" />
) : (
t('active.deleteDevice')
)}
</Button>
</Box>
</Box>
{isXsup && (
<Box sx={{ borderBottom: 1, borderColor: 'divider' }} />
)}
</React.Fragment>
))}
</Box>
)}
</CardContainer>
</PageWrapper>
);
}