feat: Redesign UI with consistent card styling and mobile header
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

- 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>
This commit is contained in:
2025-10-03 20:34:06 +00:00
parent 0dc2fcf284
commit 75e5c2866d
8 changed files with 1424 additions and 451 deletions

View File

@@ -408,7 +408,13 @@ export const AIChatInterface: React.FC = () => {
variant="contained"
startIcon={<Add />}
onClick={handleNewConversation}
sx={{ borderRadius: 2 }}
sx={{
borderRadius: 2,
textTransform: 'none',
bgcolor: 'primary.light',
color: 'primary.main',
'&:hover': { bgcolor: 'primary.main', color: 'white' }
}}
>
{t('chat.newChat')}
</Button>
@@ -621,10 +627,15 @@ export const AIChatInterface: React.FC = () => {
label={question}
onClick={() => handleSuggestedQuestion(question)}
sx={{
borderRadius: 3,
py: 2,
borderRadius: 2,
fontSize: '0.875rem',
bgcolor: 'background.paper',
border: 1,
borderColor: 'divider',
'&:hover': {
bgcolor: 'primary.light',
color: 'white',
borderColor: 'primary.main',
},
}}
/>
@@ -776,6 +787,9 @@ export const AIChatInterface: React.FC = () => {
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
bgcolor: 'background.default',
'&:hover fieldset': { borderColor: 'primary.main' },
'&.Mui-focused fieldset': { borderColor: 'primary.main' }
},
}}
/>

View File

@@ -68,11 +68,11 @@ interface ActivityTypeData {
}
const COLORS = {
feeding: '#FFB6C1',
sleep: '#B6D7FF',
diaper: '#FFE4B5',
medication: '#D4B5FF',
milestone: '#B5FFD4',
feeding: '#E91E63',
sleep: '#1976D2',
diaper: '#F57C00',
medication: '#C62828',
milestone: '#558B2F',
note: '#FFD3B6',
wet: '#87CEEB',
dirty: '#D2691E',
@@ -269,50 +269,49 @@ export const InsightsDashboard: React.FC = () => {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Box>
<Typography variant="h4" fontWeight="600" gutterBottom>
<Box sx={{ px: { xs: 2, sm: 3 }, py: 3 }}>
<Typography variant="h4" sx={{ mb: 1, fontWeight: 600 }}>
{t('title')}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
{t('subtitle')}
</Typography>
{/* Filters */}
<Paper sx={{ p: 3, mb: 3 }}>
<Grid container spacing={2} alignItems="center">
{children.length > 1 && (
<Grid item xs={12} sm={6} md={4}>
<FormControl fullWidth>
<InputLabel>{t('filters.child')}</InputLabel>
<Select
value={selectedChild}
onChange={(e) => setSelectedChild(e.target.value)}
label={t('filters.child')}
>
{children.map((child) => (
<MenuItem key={child.id} value={child.id}>
{child.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
)}
<Grid item xs={12} sm={6} md={4}>
<ToggleButtonGroup
value={dateRange}
exclusive
onChange={(_, newValue) => newValue && setDateRange(newValue)}
fullWidth
size="large"
{/* Time period selector */}
<Box sx={{ mb: 4, display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
{children.length > 1 && (
<FormControl sx={{ minWidth: 200 }}>
<InputLabel>{t('filters.child')}</InputLabel>
<Select
value={selectedChild}
onChange={(e) => setSelectedChild(e.target.value)}
label={t('filters.child')}
>
<ToggleButton value="7days">{t('filters.dateRange.7days')}</ToggleButton>
<ToggleButton value="30days">{t('filters.dateRange.30days')}</ToggleButton>
<ToggleButton value="3months">{t('filters.dateRange.3months')}</ToggleButton>
</ToggleButtonGroup>
</Grid>
</Grid>
</Paper>
{children.map((child) => (
<MenuItem key={child.id} value={child.id}>
{child.name}
</MenuItem>
))}
</Select>
</FormControl>
)}
<ToggleButtonGroup
value={dateRange}
exclusive
onChange={(_, newValue) => newValue && setDateRange(newValue)}
sx={{
'& .MuiToggleButton-root': {
textTransform: 'none',
fontWeight: 500,
minWidth: { xs: 80, sm: 120 }
}
}}
>
<ToggleButton value="7days">{t('filters.dateRange.7days')}</ToggleButton>
<ToggleButton value="30days">{t('filters.dateRange.30days')}</ToggleButton>
<ToggleButton value="3months">{t('filters.dateRange.3months')}</ToggleButton>
</ToggleButtonGroup>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
@@ -355,148 +354,167 @@ export const InsightsDashboard: React.FC = () => {
{!loading && !noChildren && !noActivities && (
<>
{/* Summary Statistics */}
<Grid container spacing={3} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6} md={3}>
{/* Stats cards */}
<Grid container spacing={2} sx={{ mb: 4 }} justifyContent="center">
<Grid item xs={6} sm={3} sx={{ minWidth: 200 }}>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0 }}
>
<Card sx={{ bgcolor: COLORS.feeding, color: 'white', height: '160px', minHeight: '160px', width: '100%' }}>
<Box
sx={{
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '160px',
minHeight: '160px',
width: '100%',
p: 2,
}}
>
<Restaurant sx={{ fontSize: 32, mb: 1 }} />
<Typography variant="h3" fontWeight="700">
{stats.totalFeedings}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
{t('stats.feedings.subtitle')}
</Typography>
<Paper
elevation={0}
sx={{
p: 3,
height: '140px',
minHeight: '140px',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
bgcolor: COLORS.feeding,
color: 'white',
}}
>
<Box sx={{ fontSize: 48, mb: 1 }} aria-hidden="true">
<Restaurant sx={{ fontSize: 48 }} />
</Box>
</Card>
<Typography variant="h3" fontWeight={600}>
{stats.totalFeedings}
</Typography>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
{t('stats.feedings.subtitle')}
</Typography>
</Paper>
</motion.div>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid item xs={6} sm={3} sx={{ minWidth: 200 }}>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<Card sx={{ bgcolor: COLORS.sleep, color: 'white', height: '160px', minHeight: '160px', width: '100%' }}>
<Box
sx={{
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '160px',
minHeight: '160px',
width: '100%',
p: 2,
}}
>
<Hotel sx={{ fontSize: 32, mb: 1 }} />
<Typography variant="h3" fontWeight="700">
{stats.avgSleepHours}h
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
{t('stats.sleep.subtitle')}
</Typography>
<Paper
elevation={0}
sx={{
p: 3,
height: '140px',
minHeight: '140px',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
bgcolor: COLORS.sleep,
color: 'white',
}}
>
<Box sx={{ fontSize: 48, mb: 1 }} aria-hidden="true">
<Hotel sx={{ fontSize: 48 }} />
</Box>
</Card>
<Typography variant="h3" fontWeight={600}>
{stats.avgSleepHours}h
</Typography>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
{t('stats.sleep.subtitle')}
</Typography>
</Paper>
</motion.div>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid item xs={6} sm={3} sx={{ minWidth: 200 }}>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<Card sx={{ bgcolor: COLORS.diaper, color: 'white', height: '160px', minHeight: '160px', width: '100%' }}>
<Box
sx={{
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '160px',
minHeight: '160px',
width: '100%',
p: 2,
}}
>
<BabyChangingStation sx={{ fontSize: 32, mb: 1 }} />
<Typography variant="h3" fontWeight="700">
{stats.totalDiapers}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
{t('stats.diapers.subtitle')}
</Typography>
<Paper
elevation={0}
sx={{
p: 3,
height: '140px',
minHeight: '140px',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
bgcolor: COLORS.diaper,
color: 'white',
}}
>
<Box sx={{ fontSize: 48, mb: 1 }} aria-hidden="true">
<BabyChangingStation sx={{ fontSize: 48 }} />
</Box>
</Card>
<Typography variant="h3" fontWeight={600}>
{stats.totalDiapers}
</Typography>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
{t('stats.diapers.subtitle')}
</Typography>
</Paper>
</motion.div>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid item xs={6} sm={3} sx={{ minWidth: 200 }}>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.3 }}
>
<Card sx={{ bgcolor: COLORS.milestone, color: 'white', height: '160px', minHeight: '160px', width: '100%' }}>
<Box
sx={{
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '160px',
minHeight: '160px',
width: '100%',
p: 2,
}}
>
<TrendingUp sx={{ fontSize: 32, mb: 1 }} />
<Typography variant="h3" fontWeight="700" sx={{ textTransform: 'capitalize' }}>
{t(`activityTypes.${stats.mostCommonType}`)}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
{t('stats.topActivity.subtitle')}
</Typography>
<Paper
elevation={0}
sx={{
p: 3,
height: '140px',
minHeight: '140px',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
bgcolor: COLORS.milestone,
color: 'white',
}}
>
<Box sx={{ fontSize: 48, mb: 1 }} aria-hidden="true">
<TrendingUp sx={{ fontSize: 48 }} />
</Box>
</Card>
<Typography variant="h3" fontWeight={600} sx={{ textTransform: 'capitalize' }}>
{t(`activityTypes.${stats.mostCommonType}`)}
</Typography>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
{t('stats.topActivity.subtitle')}
</Typography>
</Paper>
</motion.div>
</Grid>
</Grid>
{/* Charts */}
<Grid container spacing={3} sx={{ mb: 3 }}>
<Grid item xs={12} md={6}>
<Card sx={{ height: '350px', minHeight: '350px', width: '100%' }}>
<CardContent sx={{ height: '100%', display: 'flex', flexDirection: 'column', p: 2, '&:last-child': { pb: 2 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Restaurant sx={{ mr: 1, color: COLORS.feeding }} />
<Typography variant="h6" fontWeight="600">
{t('charts.feedingFrequency')}
</Typography>
</Box>
{/* Charts grid */}
<Grid container spacing={3} justifyContent="center" sx={{ mb: 3 }}>
<Grid item xs={12} md={6} sx={{ minWidth: 400 }}>
<Paper
elevation={0}
sx={{
p: 3,
bgcolor: 'background.paper',
height: 400,
width: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, fontWeight: 600 }}>
<Restaurant sx={{ color: COLORS.feeding }} /> {t('charts.feedingFrequency')}
</Typography>
<Box sx={{ flexGrow: 1, position: 'relative' }}>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={dailyData}>
<CartesianGrid strokeDasharray="3 3" />
@@ -506,19 +524,26 @@ export const InsightsDashboard: React.FC = () => {
<Bar dataKey="feedings" fill={COLORS.feeding} name={t('charts.chartLabels.feedings')} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</Box>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Card sx={{ height: '350px', minHeight: '350px', width: '100%' }}>
<CardContent sx={{ height: '100%', display: 'flex', flexDirection: 'column', p: 2, '&:last-child': { pb: 2 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Hotel sx={{ mr: 1, color: COLORS.sleep }} />
<Typography variant="h6" fontWeight="600">
{t('charts.sleepDuration')}
</Typography>
</Box>
<Grid item xs={12} md={6} sx={{ minWidth: 400 }}>
<Paper
elevation={0}
sx={{
p: 3,
bgcolor: 'background.paper',
height: 400,
width: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, fontWeight: 600 }}>
<Hotel sx={{ color: COLORS.sleep }} /> {t('charts.sleepDuration')}
</Typography>
<Box sx={{ flexGrow: 1, position: 'relative' }}>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={dailyData}>
<CartesianGrid strokeDasharray="3 3" />
@@ -535,20 +560,27 @@ export const InsightsDashboard: React.FC = () => {
/>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</Box>
</Paper>
</Grid>
{diaperData.length > 0 && (
<Grid item xs={12} md={6}>
<Card sx={{ height: '350px', minHeight: '350px', width: '100%' }}>
<CardContent sx={{ height: '100%', display: 'flex', flexDirection: 'column', p: 2, '&:last-child': { pb: 2 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<BabyChangingStation sx={{ mr: 1, color: COLORS.diaper }} />
<Typography variant="h6" fontWeight="600">
{t('charts.diaperChangesByType')}
</Typography>
</Box>
<Grid item xs={12} md={6} sx={{ minWidth: 400 }}>
<Paper
elevation={0}
sx={{
p: 3,
bgcolor: 'background.paper',
height: 400,
width: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, fontWeight: 600 }}>
<BabyChangingStation sx={{ color: COLORS.diaper }} /> {t('charts.diaperChangesByType')}
</Typography>
<Box sx={{ flexGrow: 1, position: 'relative' }}>
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
@@ -568,20 +600,27 @@ export const InsightsDashboard: React.FC = () => {
<Tooltip />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
</Box>
</Paper>
</Grid>
)}
<Grid item xs={12} md={diaperData.length > 0 ? 6 : 12}>
<Card sx={{ height: '350px', minHeight: '350px', width: '100%' }}>
<CardContent sx={{ height: '100%', display: 'flex', flexDirection: 'column', p: 2, '&:last-child': { pb: 2 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Assessment sx={{ mr: 1, color: 'primary.main' }} />
<Typography variant="h6" fontWeight="600">
{t('charts.activityTimeline')}
</Typography>
</Box>
<Grid item xs={12} md={diaperData.length > 0 ? 6 : 12} sx={{ minWidth: 400 }}>
<Paper
elevation={0}
sx={{
p: 3,
bgcolor: 'background.paper',
height: 400,
width: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, fontWeight: 600 }}>
<Assessment sx={{ color: 'primary.main' }} /> {t('charts.activityTimeline')}
</Typography>
<Box sx={{ flexGrow: 1, position: 'relative' }}>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={dailyData}>
<CartesianGrid strokeDasharray="3 3" />
@@ -594,8 +633,8 @@ export const InsightsDashboard: React.FC = () => {
<Bar dataKey="sleepHours" fill={COLORS.sleep} name={t('charts.chartLabels.sleepHours')} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</Box>
</Paper>
</Grid>
</Grid>

View File

@@ -62,20 +62,65 @@ export const AppShell = ({ children }: AppShellProps) => {
flexDirection: 'column',
minHeight: '100vh',
bgcolor: 'background.default',
pb: isMobile ? '64px' : 0, // Space for tab bar
pb: { xs: '64px', md: 0 }, // Space for tab bar on mobile
}}>
{!isMobile && <MobileNav />}
{/* Mobile User Menu Button - Top Left */}
{/* Mobile Header Bar */}
{isMobile && (
<Box
sx={{
position: 'fixed',
top: 8,
left: 8,
top: 0,
left: 0,
right: 0,
height: 48,
bgcolor: 'background.paper',
borderBottom: '1px solid',
borderColor: 'divider',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2,
zIndex: 1200,
boxShadow: 1,
}}
>
{/* Connection Status & Presence Indicator */}
<Box
sx={{
display: 'flex',
gap: 1,
}}
>
<Tooltip title={isConnected ? t('connection.syncActive') : t('connection.syncDisconnected')}>
<Chip
icon={isConnected ? <Wifi /> : <WifiOff />}
label={isConnected ? t('connection.live') : t('connection.offline')}
size="small"
color={isConnected ? 'success' : 'default'}
sx={{
fontWeight: 600,
}}
/>
</Tooltip>
{isConnected && presence.count > 1 && (
<Tooltip title={t('connection.familyMembersOnline', { count: presence.count })}>
<Chip
icon={<People />}
label={presence.count}
size="small"
color="primary"
sx={{
fontWeight: 600,
}}
/>
</Tooltip>
)}
</Box>
{/* User Menu Button - Top Right */}
<IconButton
onClick={handleMenuOpen}
size="small"
@@ -83,14 +128,6 @@ export const AppShell = ({ children }: AppShellProps) => {
aria-controls={anchorEl ? 'user-menu' : undefined}
aria-haspopup="true"
aria-expanded={anchorEl ? 'true' : undefined}
sx={{
bgcolor: 'background.paper',
boxShadow: 1,
'&:hover': {
bgcolor: 'background.paper',
boxShadow: 2,
},
}}
>
<Avatar
sx={{
@@ -110,8 +147,8 @@ export const AppShell = ({ children }: AppShellProps) => {
open={Boolean(anchorEl)}
onClose={handleMenuClose}
onClick={handleMenuClose}
transformOrigin={{ horizontal: 'left', vertical: 'top' }}
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
sx={{
mt: 1,
}}
@@ -145,52 +182,55 @@ export const AppShell = ({ children }: AppShellProps) => {
</Box>
)}
{/* Connection Status & Presence Indicator */}
<Box
sx={{
position: 'fixed',
top: isMobile ? 8 : 16,
right: isMobile ? 8 : 16,
zIndex: 1200,
display: 'flex',
gap: 1,
}}
>
<Tooltip title={isConnected ? t('connection.syncActive') : t('connection.syncDisconnected')}>
<Chip
icon={isConnected ? <Wifi /> : <WifiOff />}
label={isConnected ? t('connection.live') : t('connection.offline')}
size="small"
color={isConnected ? 'success' : 'default'}
sx={{
fontWeight: 600,
boxShadow: 1,
}}
/>
</Tooltip>
{isConnected && presence.count > 1 && (
<Tooltip title={t('connection.familyMembersOnline', { count: presence.count })}>
{/* Connection Status & Presence Indicator - Desktop Only */}
{!isMobile && (
<Box
sx={{
position: 'fixed',
top: 16,
right: 16,
zIndex: 1200,
display: 'flex',
gap: 1,
}}
>
<Tooltip title={isConnected ? t('connection.syncActive') : t('connection.syncDisconnected')}>
<Chip
icon={<People />}
label={presence.count}
icon={isConnected ? <Wifi /> : <WifiOff />}
label={isConnected ? t('connection.live') : t('connection.offline')}
size="small"
color="primary"
color={isConnected ? 'success' : 'default'}
sx={{
fontWeight: 600,
boxShadow: 1,
}}
/>
</Tooltip>
)}
</Box>
{isConnected && presence.count > 1 && (
<Tooltip title={t('connection.familyMembersOnline', { count: presence.count })}>
<Chip
icon={<People />}
label={presence.count}
size="small"
color="primary"
sx={{
fontWeight: 600,
boxShadow: 1,
}}
/>
</Tooltip>
)}
</Box>
)}
<Container
maxWidth={isTablet ? 'md' : 'lg'}
sx={{
flex: 1,
px: isMobile ? 2 : 3,
px: { xs: 2, md: 3 },
py: 3,
pt: { xs: '64px', md: 3 }, // Add top padding for header bar on mobile
}}
>
{children}