feat: Add collapsible sections and mobile grid layout
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

- Convert Active Sessions and Trusted Devices to collapsible Accordion components
- Display count badge in collapsed state
- Show loading state in accordion header
- Implement 2-card grid layout on mobile (xs=6)
- Responsive card sizing and spacing
- Centered layout on mobile, horizontal on desktop
- Hide full birthdate on mobile, show age only

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-04 08:08:24 +00:00
parent ec3f0264a0
commit 426b5a309e
5 changed files with 79 additions and 43 deletions

View File

@@ -214,9 +214,9 @@ export default function ChildrenPage() {
</Grid> </Grid>
</Grid> </Grid>
) : ( ) : (
<Grid container spacing={3}> <Grid container spacing={{ xs: 2, sm: 3 }}>
{children.map((child, index) => ( {children.map((child, index) => (
<Grid item xs={12} sm={6} key={child.id}> <Grid item xs={6} sm={6} md={4} key={child.id}>
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@@ -225,43 +225,43 @@ export default function ChildrenPage() {
<Paper <Paper
elevation={0} elevation={0}
sx={{ sx={{
p: 3, p: { xs: 2, sm: 3 },
borderRadius: 3, borderRadius: 3,
bgcolor: 'background.paper', bgcolor: 'background.paper',
border: 1, border: 1,
borderColor: 'divider' borderColor: 'divider'
}} }}
> >
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, alignItems: { xs: 'center', sm: 'flex-start' }, gap: { xs: 1, sm: 2 } }}>
<Avatar <Avatar
src={child.photoUrl} src={child.photoUrl}
sx={{ sx={{
width: 64, width: { xs: 48, sm: 64 },
height: 64, height: { xs: 48, sm: 64 },
bgcolor: 'primary.light', bgcolor: 'primary.light',
fontSize: 24 fontSize: { xs: 20, sm: 24 }
}} }}
> >
{child.name[0]} {child.name[0]}
</Avatar> </Avatar>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1, textAlign: { xs: 'center', sm: 'left' }, width: '100%' }}>
<Typography variant="h6" fontWeight="600">{child.name}</Typography> <Typography variant="h6" fontWeight="600" sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>{child.name}</Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
{t(`gender.${child.gender}`)} {t(`gender.${child.gender}`)}
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}> <Box sx={{ display: { xs: 'none', sm: 'flex' }, alignItems: 'center', gap: 1, mt: 1 }}>
<CalendarToday fontSize="small" color="action" /> <CalendarToday fontSize="small" color="action" />
<Typography variant="caption"> <Typography variant="caption">
{formatDate(child.birthDate, 'PPP')} {formatDate(child.birthDate, 'PPP')}
</Typography> </Typography>
</Box> </Box>
<Typography variant="caption" color="primary.main"> <Typography variant="caption" color="primary.main" sx={{ display: 'block', mt: 0.5 }}>
{t('age')}: {calculateAge(child.birthDate)} {t('age')}: {calculateAge(child.birthDate)}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1, mt: 2 }}> <Box sx={{ display: 'flex', gap: 1, mt: 2, justifyContent: { xs: 'center', sm: 'flex-start' } }}>
<IconButton size="small" color="primary" onClick={() => handleEditChild(child)}> <IconButton size="small" color="primary" onClick={() => handleEditChild(child)}>
<Edit /> <Edit />
</IconButton> </IconButton>

View File

@@ -266,10 +266,9 @@ export default function SettingsPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.3 }} transition={{ duration: 0.4, delay: 0.3 }}
style={{ marginBottom: '24px' }}
> >
<Box sx={{ mb: 3 }}> <SessionsManagement />
<SessionsManagement />
</Box>
</motion.div> </motion.div>
{/* Device Trust Management */} {/* Device Trust Management */}
@@ -277,10 +276,9 @@ export default function SettingsPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.33 }} transition={{ duration: 0.4, delay: 0.33 }}
style={{ marginBottom: '24px' }}
> >
<Box sx={{ mb: 3 }}> <DeviceTrustManagement />
<DeviceTrustManagement />
</Box>
</motion.div> </motion.div>
{/* Biometric Authentication */} {/* Biometric Authentication */}

View File

@@ -22,6 +22,9 @@ import {
Divider, Divider,
ToggleButton, ToggleButton,
ToggleButtonGroup, ToggleButtonGroup,
Accordion,
AccordionSummary,
AccordionDetails,
} from '@mui/material'; } from '@mui/material';
import { import {
Devices, Devices,
@@ -32,6 +35,7 @@ import {
CheckCircle, CheckCircle,
Shield, Shield,
ShieldOutlined, ShieldOutlined,
ExpandMore,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { devicesApi, type DeviceInfo } from '@/lib/api/devices'; import { devicesApi, type DeviceInfo } from '@/lib/api/devices';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
@@ -143,26 +147,41 @@ export function DeviceTrustManagement() {
if (isLoading) { if (isLoading) {
return ( return (
<Card> <Accordion>
<CardContent sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> <AccordionSummary expandIcon={<ExpandMore />}>
<CircularProgress /> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}>
</CardContent> <Shield color="primary" />
</Card> <Typography variant="h6" fontWeight="600">
Trusted Devices
</Typography>
<CircularProgress size={20} sx={{ ml: 'auto' }} />
</Box>
</AccordionSummary>
</Accordion>
); );
} }
return ( return (
<> <>
<Card> <Accordion>
<CardContent> <AccordionSummary
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}> expandIcon={<ExpandMore />}
sx={{
'& .MuiAccordionSummary-content': {
margin: '12px 0',
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}>
<Shield color="primary" /> <Shield color="primary" />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Device Trust Management Trusted Devices
</Typography> </Typography>
<Chip label={`${devices.length} devices`} size="small" sx={{ ml: 'auto' }} /> <Chip label={`${devices.length}`} size="small" sx={{ ml: 'auto', mr: 1 }} />
</Box> </Box>
</AccordionSummary>
<AccordionDetails>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Manage which devices are trusted to access your account. Untrusted devices will require additional verification. Manage which devices are trusted to access your account. Untrusted devices will require additional verification.
</Typography> </Typography>
@@ -278,8 +297,8 @@ export function DeviceTrustManagement() {
)} )}
</> </>
)} )}
</CardContent> </AccordionDetails>
</Card> </Accordion>
{/* Remove Device Dialog */} {/* Remove Device Dialog */}
<Dialog open={removeDialogOpen} onClose={() => setRemoveDialogOpen(false)} maxWidth="sm" fullWidth> <Dialog open={removeDialogOpen} onClose={() => setRemoveDialogOpen(false)} maxWidth="sm" fullWidth>

View File

@@ -20,6 +20,9 @@ import {
ListItemSecondaryAction, ListItemSecondaryAction,
IconButton, IconButton,
Divider, Divider,
Accordion,
AccordionSummary,
AccordionDetails,
} from '@mui/material'; } from '@mui/material';
import { import {
Devices, Devices,
@@ -28,6 +31,7 @@ import {
Tablet, Tablet,
Delete, Delete,
CheckCircle, CheckCircle,
ExpandMore,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { sessionsApi, type SessionInfo } from '@/lib/api/sessions'; import { sessionsApi, type SessionInfo } from '@/lib/api/sessions';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
@@ -116,26 +120,41 @@ export function SessionsManagement() {
if (isLoading) { if (isLoading) {
return ( return (
<Card> <Accordion>
<CardContent sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> <AccordionSummary expandIcon={<ExpandMore />}>
<CircularProgress /> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}>
</CardContent> <Devices color="primary" />
</Card> <Typography variant="h6" fontWeight="600">
Active Sessions
</Typography>
<CircularProgress size={20} sx={{ ml: 'auto' }} />
</Box>
</AccordionSummary>
</Accordion>
); );
} }
return ( return (
<> <>
<Card> <Accordion>
<CardContent> <AccordionSummary
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}> expandIcon={<ExpandMore />}
sx={{
'& .MuiAccordionSummary-content': {
margin: '12px 0',
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}>
<Devices color="primary" /> <Devices color="primary" />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Active Sessions Active Sessions
</Typography> </Typography>
<Chip label={`${sessions.length} active`} size="small" sx={{ ml: 'auto' }} /> <Chip label={`${sessions.length}`} size="small" sx={{ ml: 'auto', mr: 1 }} />
</Box> </Box>
</AccordionSummary>
<AccordionDetails>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Manage your active sessions across different devices. You can revoke access from any device. Manage your active sessions across different devices. You can revoke access from any device.
</Typography> </Typography>
@@ -220,8 +239,8 @@ export function SessionsManagement() {
)} )}
</> </>
)} )}
</CardContent> </AccordionDetails>
</Card> </Accordion>
{/* Revoke Session Dialog */} {/* Revoke Session Dialog */}
<Dialog open={revokeDialogOpen} onClose={() => setRevokeDialogOpen(false)} maxWidth="sm" fullWidth> <Dialog open={revokeDialogOpen} onClose={() => setRevokeDialogOpen(false)} maxWidth="sm" fullWidth>

File diff suppressed because one or more lines are too long