Files
maternal-app/maternal-web/app/children/page.tsx
Andrei 75e5c2866d
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
feat: Redesign UI with consistent card styling and mobile header
- Updated track page cards to match home page styling with vibrant colors
- Applied consistent 140px height cards across track and insights pages
- Added mobile header bar with connection status and user menu
- Moved user menu from floating top-left to fixed header top-right
- Updated insights dashboard with home page color palette (#E91E63, #1976D2, etc.)
- Centered cards with minWidth constraints (200px for stats, 400px for charts)
- Fixed hydration mismatch by replacing JS media queries with CSS breakpoints
- Improved accessibility with viewport settings (removed zoom restrictions)
- Added UI improvements documentation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 20:34:06 +00:00

297 lines
9.7 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Grid,
Paper,
Button,
Avatar,
IconButton,
CircularProgress,
Alert,
Chip,
Container,
Card,
CardContent,
} from '@mui/material';
import { Add, ChildCare, Edit, Delete, CalendarToday } from '@mui/icons-material';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { useAuth } from '@/lib/auth/AuthContext';
import { childrenApi, Child, CreateChildData } from '@/lib/api/children';
import { ChildDialog } from '@/components/children/ChildDialog';
import { DeleteConfirmDialog } from '@/components/children/DeleteConfirmDialog';
import { motion } from 'framer-motion';
import { useTranslation } from '@/hooks/useTranslation';
export default function ChildrenPage() {
const { t } = useTranslation('children');
const { user } = useAuth();
const [children, setChildren] = useState<Child[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
const [dialogOpen, setDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedChild, setSelectedChild] = useState<Child | null>(null);
const [childToDelete, setChildToDelete] = useState<Child | null>(null);
const [actionLoading, setActionLoading] = useState(false);
// Get familyId from user
const familyId = user?.families?.[0]?.familyId;
useEffect(() => {
if (familyId) {
fetchChildren();
} else {
setLoading(false);
setError(t('errors.noFamily'));
}
}, [familyId]);
const fetchChildren = async () => {
if (!familyId) return;
try {
setLoading(true);
setError('');
const data = await childrenApi.getChildren(familyId);
setChildren(data);
} catch (err: any) {
console.error('Failed to fetch children:', err);
setError(err.response?.data?.message || t('errors.loadFailed'));
} finally {
setLoading(false);
}
};
const handleAddChild = () => {
setSelectedChild(null);
setDialogOpen(true);
};
const handleEditChild = (child: Child) => {
setSelectedChild(child);
setDialogOpen(true);
};
const handleDeleteClick = (child: Child) => {
setChildToDelete(child);
setDeleteDialogOpen(true);
};
const handleSubmit = async (data: CreateChildData) => {
if (!familyId) {
throw new Error(t('errors.noFamilyId'));
}
try {
setActionLoading(true);
if (selectedChild) {
await childrenApi.updateChild(selectedChild.id, data);
} else {
await childrenApi.createChild(familyId, data);
}
await fetchChildren();
setDialogOpen(false);
} catch (err: any) {
console.error('Failed to save child:', err);
throw new Error(err.response?.data?.message || t('errors.saveFailed'));
} finally {
setActionLoading(false);
}
};
const handleDeleteConfirm = async () => {
if (!childToDelete) return;
try {
setActionLoading(true);
await childrenApi.deleteChild(childToDelete.id);
await fetchChildren();
setDeleteDialogOpen(false);
setChildToDelete(null);
} catch (err: any) {
console.error('Failed to delete child:', err);
setError(err.response?.data?.message || t('errors.deleteFailed'));
} finally {
setActionLoading(false);
}
};
const calculateAge = (birthDate: string): string => {
const birth = new Date(birthDate);
const today = new Date();
let years = today.getFullYear() - birth.getFullYear();
let months = today.getMonth() - birth.getMonth();
if (months < 0) {
years--;
months += 12;
}
if (today.getDate() < birth.getDate()) {
months--;
if (months < 0) {
years--;
months += 12;
}
}
if (years === 0) {
return `${months} ${months !== 1 ? t('ageFormat.months') : t('ageFormat.month')}`;
} else if (months === 0) {
return `${years} ${years !== 1 ? t('ageFormat.years') : t('ageFormat.year')}`;
} else {
return `${years} ${years !== 1 ? t('ageFormat.years') : t('ageFormat.year')}, ${months} ${months !== 1 ? t('ageFormat.months') : t('ageFormat.month')}`;
}
};
return (
<ProtectedRoute>
<AppShell>
<Container maxWidth="md" sx={{ px: { xs: 2, sm: 3 }, py: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Box>
<Typography variant="h4" component="h1" fontWeight="600">
{t('title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('subtitle')}
</Typography>
</Box>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAddChild}
disabled={loading || !familyId}
sx={{
borderRadius: 2,
textTransform: 'none',
px: 3
}}
>
{t('addChild')}
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 3 }} onClose={() => setError('')}>
{error}
</Alert>
)}
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
) : children.length === 0 ? (
<Grid container spacing={3}>
<Grid item xs={12}>
<Card>
<CardContent sx={{ textAlign: 'center', py: 8 }}>
<ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" component="h2" color="text.secondary" gutterBottom>
{t('noChildren')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('noChildrenSubtitle')}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAddChild}
disabled={!familyId}
>
{t('addFirstChild')}
</Button>
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Grid container spacing={3}>
{children.map((child, index) => (
<Grid item xs={12} sm={6} key={child.id}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<Paper
elevation={0}
sx={{
p: 3,
borderRadius: 3,
bgcolor: 'background.paper',
border: 1,
borderColor: 'divider'
}}
>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<Avatar
src={child.photoUrl}
sx={{
width: 64,
height: 64,
bgcolor: 'primary.light',
fontSize: 24
}}
>
{child.name[0]}
</Avatar>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h6" fontWeight="600">{child.name}</Typography>
<Typography variant="body2" color="text.secondary">
{t(`gender.${child.gender}`)}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
<CalendarToday fontSize="small" color="action" />
<Typography variant="caption">
{new Date(child.birthDate).toLocaleDateString()}
</Typography>
</Box>
<Typography variant="caption" color="primary.main">
{t('age')}: {calculateAge(child.birthDate)}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
<IconButton size="small" color="primary" onClick={() => handleEditChild(child)}>
<Edit />
</IconButton>
<IconButton size="small" color="error" onClick={() => handleDeleteClick(child)}>
<Delete />
</IconButton>
</Box>
</Paper>
</motion.div>
</Grid>
))}
</Grid>
)}
</Container>
<ChildDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
onSubmit={handleSubmit}
child={selectedChild}
isLoading={actionLoading}
/>
<DeleteConfirmDialog
open={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
onConfirm={handleDeleteConfirm}
childName={childToDelete?.name || ''}
isLoading={actionLoading}
/>
</AppShell>
</ProtectedRoute>
);
}