Files
maternal-app/maternal-web/components/settings/SessionsManagement.tsx
Andrei b56f9546c2 feat: Complete high-priority i18n localization with date/time support
This commit implements comprehensive localization for high-priority components:

## Tracking Pages (4 files)
- Localized feeding, sleep, diaper, and medicine tracking pages
- Replaced hardcoded strings with translation keys from tracking namespace
- Added useTranslation hook integration
- All form labels, buttons, and messages now support multiple languages

## Child Dialog Components (2 files)
- Localized ChildDialog (add/edit child form)
- Localized DeleteConfirmDialog
- Added new translation keys to children.json for dialog content
- Includes validation messages and action buttons

## Date/Time Localization (14 files + new hook)
- Created useLocalizedDate hook wrapping date-fns with locale support
- Supports 5 languages: English, Spanish, French, Portuguese, Chinese
- Updated all date formatting across:
  * Tracking pages (feeding, sleep, diaper, medicine)
  * Activity pages (activities, history, track activity)
  * Settings components (sessions, biometric, device trust)
  * Analytics components (insights, growth, sleep chart, feeding graph)
- Date displays automatically adapt to user's language (e.g., "2 hours ago" → "hace 2 horas")

## Translation Updates
- Enhanced children.json with dialog section containing:
  * Form field labels (name, birthDate, gender, photoUrl)
  * Action buttons (add, update, delete, cancel, saving, deleting)
  * Delete confirmation messages
  * Validation error messages

Files changed: 17 files (+164, -113)
Languages supported: en, es, fr, pt-BR, zh-CN

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 11:49:48 +00:00

280 lines
9.5 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Alert,
CircularProgress,
Chip,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
IconButton,
Divider,
} from '@mui/material';
import {
Devices,
Computer,
PhoneAndroid,
Tablet,
Delete,
CheckCircle,
} from '@mui/icons-material';
import { sessionsApi, type SessionInfo } from '@/lib/api/sessions';
import { motion } from 'framer-motion';
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
export function SessionsManagement() {
const { formatDistanceToNow } = useLocalizedDate();
const [sessions, setSessions] = useState<SessionInfo[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
// Revoke session dialog
const [revokeDialogOpen, setRevokeDialogOpen] = useState(false);
const [sessionToRevoke, setSessionToRevoke] = useState<SessionInfo | null>(null);
const [isRevoking, setIsRevoking] = useState(false);
// Revoke all dialog
const [revokeAllDialogOpen, setRevokeAllDialogOpen] = useState(false);
const [isRevokingAll, setIsRevokingAll] = useState(false);
useEffect(() => {
loadSessions();
}, []);
const loadSessions = async () => {
try {
setIsLoading(true);
const response = await sessionsApi.getSessions();
setSessions(response.sessions);
setError(null);
} catch (err: any) {
console.error('Failed to load sessions:', err);
setError('Failed to load sessions');
} finally {
setIsLoading(false);
}
};
const handleRevokeSession = async () => {
if (!sessionToRevoke) return;
try {
setIsRevoking(true);
await sessionsApi.revokeSession(sessionToRevoke.id);
setSuccessMessage('Session revoked successfully');
setRevokeDialogOpen(false);
setSessionToRevoke(null);
await loadSessions();
} catch (err: any) {
console.error('Failed to revoke session:', err);
setError(err.response?.data?.message || 'Failed to revoke session');
} finally {
setIsRevoking(false);
}
};
const handleRevokeAllSessions = async () => {
try {
setIsRevokingAll(true);
const response = await sessionsApi.revokeAllSessions();
setSuccessMessage(`${response.revokedCount} session(s) revoked successfully`);
setRevokeAllDialogOpen(false);
await loadSessions();
} catch (err: any) {
console.error('Failed to revoke all sessions:', err);
setError(err.response?.data?.message || 'Failed to revoke sessions');
} finally {
setIsRevokingAll(false);
}
};
const getPlatformIcon = (platform?: string) => {
if (!platform) return <Devices />;
const p = platform.toLowerCase();
if (p.includes('android') || p.includes('mobile')) return <PhoneAndroid />;
if (p.includes('ios') || p.includes('iphone') || p.includes('ipad')) return <PhoneAndroid />;
if (p.includes('tablet')) return <Tablet />;
return <Computer />;
};
if (isLoading) {
return (
<Card>
<CardContent sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</CardContent>
</Card>
);
}
return (
<>
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Devices color="primary" />
<Typography variant="h6" fontWeight="600">
Active Sessions
</Typography>
<Chip label={`${sessions.length} active`} size="small" sx={{ ml: 'auto' }} />
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Manage your active sessions across different devices. You can revoke access from any device.
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
{error}
</Alert>
)}
{successMessage && (
<Alert severity="success" sx={{ mb: 2 }} onClose={() => setSuccessMessage(null)}>
{successMessage}
</Alert>
)}
{sessions.length === 0 ? (
<Alert severity="info">No active sessions found.</Alert>
) : (
<>
<List>
{sessions.map((session, index) => (
<Box key={session.id}>
{index > 0 && <Divider />}
<ListItem>
<Box sx={{ mr: 2 }}>{getPlatformIcon(session.platform)}</Box>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body1">
{session.platform || 'Unknown Platform'}
</Typography>
{session.isCurrent && (
<Chip label="Current" color="success" size="small" icon={<CheckCircle />} />
)}
</Box>
}
secondary={
<Box>
<Typography variant="body2" color="text.secondary">
Device: {session.deviceFingerprint}
</Typography>
<Typography variant="body2" color="text.secondary">
Last active: {formatDistanceToNow(new Date(session.lastUsed))} ago
</Typography>
<Typography variant="body2" color="text.secondary">
Created: {formatDistanceToNow(new Date(session.createdAt))} ago
</Typography>
</Box>
}
/>
<ListItemSecondaryAction>
{!session.isCurrent && (
<IconButton
edge="end"
onClick={() => {
setSessionToRevoke(session);
setRevokeDialogOpen(true);
}}
color="error"
>
<Delete />
</IconButton>
)}
</ListItemSecondaryAction>
</ListItem>
</Box>
))}
</List>
{sessions.length > 1 && (
<Box sx={{ mt: 3 }}>
<Button
variant="outlined"
color="error"
onClick={() => setRevokeAllDialogOpen(true)}
fullWidth
>
Revoke All Other Sessions
</Button>
</Box>
)}
</>
)}
</CardContent>
</Card>
{/* Revoke Session Dialog */}
<Dialog open={revokeDialogOpen} onClose={() => setRevokeDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Revoke Session?</DialogTitle>
<DialogContent>
<Alert severity="warning" sx={{ mb: 2 }}>
This device will be logged out and will need to log in again.
</Alert>
{sessionToRevoke && (
<Box>
<Typography variant="body2" gutterBottom>
<strong>Device:</strong> {sessionToRevoke.deviceFingerprint}
</Typography>
<Typography variant="body2" gutterBottom>
<strong>Platform:</strong> {sessionToRevoke.platform || 'Unknown'}
</Typography>
<Typography variant="body2">
<strong>Last active:</strong> {formatDistanceToNow(new Date(sessionToRevoke.lastUsed))} ago
</Typography>
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setRevokeDialogOpen(false)} disabled={isRevoking}>
Cancel
</Button>
<Button onClick={handleRevokeSession} variant="contained" color="error" disabled={isRevoking}>
{isRevoking ? <CircularProgress size={20} /> : 'Revoke Session'}
</Button>
</DialogActions>
</Dialog>
{/* Revoke All Sessions Dialog */}
<Dialog open={revokeAllDialogOpen} onClose={() => setRevokeAllDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Revoke All Other Sessions?</DialogTitle>
<DialogContent>
<Alert severity="warning" sx={{ mb: 2 }}>
This will log out all devices except your current one. They will need to log in again.
</Alert>
<Typography variant="body2">
You are about to revoke {sessions.filter((s) => !s.isCurrent).length} session(s).
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setRevokeAllDialogOpen(false)} disabled={isRevokingAll}>
Cancel
</Button>
<Button
onClick={handleRevokeAllSessions}
variant="contained"
color="error"
disabled={isRevokingAll}
>
{isRevokingAll ? <CircularProgress size={20} /> : 'Revoke All'}
</Button>
</DialogActions>
</Dialog>
</>
);
}