Add missing pages with AppShell layout integration

- Created /track, /insights, /children, /family, /settings, /logout pages
- Wrapped all authenticated pages with AppShell and ProtectedRoute
- Updated AI assistant page to use AppShell layout
- All pages now have proper header/navigation and footer/tabbar
- Added responsive mobile and desktop layouts
- Integrated with existing navigation system

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
andupetcu
2025-09-30 22:05:56 +03:00
parent b48aaded05
commit b62342fe2d
10 changed files with 899 additions and 9 deletions

View File

@@ -15,6 +15,8 @@ import { Send, SmartToy, Person, AutoAwesome } from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '@/lib/auth/AuthContext';
import apiClient from '@/lib/api/client';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
interface Message {
id: string;
@@ -93,14 +95,18 @@ export default function AIAssistantPage() {
};
return (
<Box
sx={{
height: 'calc(100vh - 64px)',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(135deg, #FFF5F5 0%, #FFE4E1 100%)',
}}
>
<ProtectedRoute>
<AppShell>
<Box
sx={{
height: 'calc(100vh - 200px)',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(135deg, #FFF5F5 0%, #FFE4E1 100%)',
borderRadius: 2,
overflow: 'hidden',
}}
>
{/* Header */}
<Paper
elevation={0}
@@ -325,6 +331,8 @@ export default function AIAssistantPage() {
for medical advice.
</Typography>
</Paper>
</Box>
</Box>
</AppShell>
</ProtectedRoute>
);
}

View File

@@ -0,0 +1,60 @@
'use client';
import { Box, Typography, Grid, Card, CardContent, Button } from '@mui/material';
import { Add, ChildCare } from '@mui/icons-material';
import { useRouter } from 'next/navigation';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
export default function ChildrenPage() {
const router = useRouter();
return (
<ProtectedRoute>
<AppShell>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Box>
<Typography variant="h4" fontWeight="600" gutterBottom>
Children
</Typography>
<Typography variant="body1" color="text.secondary">
Manage your family's children profiles
</Typography>
</Box>
<Button
variant="contained"
startIcon={<Add />}
onClick={() => router.push('/children/new')}
>
Add Child
</Button>
</Box>
<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" color="text.secondary" gutterBottom>
No children added yet
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Add your first child to start tracking their activities
</Typography>
<Button
variant="contained"
startIcon={<Add />}
onClick={() => router.push('/children/new')}
>
Add First Child
</Button>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
</AppShell>
</ProtectedRoute>
);
}

View File

@@ -0,0 +1,127 @@
'use client';
import { Box, Typography, Grid, Card, CardContent, Button, Avatar, Chip } from '@mui/material';
import { PersonAdd, ContentCopy, People } from '@mui/icons-material';
import { useAuth } from '@/lib/auth/AuthContext';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
export default function FamilyPage() {
const { user } = useAuth();
const handleInvite = () => {
// Invite functionality to be implemented
alert('Family invitation feature coming soon!');
};
const handleCopyCode = () => {
// Copy share code to clipboard
navigator.clipboard.writeText('FAMILY-CODE-123');
alert('Family code copied to clipboard!');
};
return (
<ProtectedRoute>
<AppShell>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Box>
<Typography variant="h4" fontWeight="600" gutterBottom>
Family
</Typography>
<Typography variant="body1" color="text.secondary">
Manage your family members and share access
</Typography>
</Box>
<Button
variant="contained"
startIcon={<PersonAdd />}
onClick={handleInvite}
>
Invite Member
</Button>
</Box>
<Grid container spacing={3}>
{/* Family Share Code */}
<Grid item xs={12}>
<Card>
<CardContent>
<Typography variant="h6" fontWeight="600" gutterBottom>
Family Share Code
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Share this code with family members to give them access to your family's data
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Chip
label="FAMILY-CODE-123"
sx={{
fontSize: '1.1rem',
fontWeight: 600,
py: 2.5,
px: 1,
}}
/>
<Button
variant="outlined"
startIcon={<ContentCopy />}
onClick={handleCopyCode}
>
Copy Code
</Button>
</Box>
</CardContent>
</Card>
</Grid>
{/* Family Members */}
<Grid item xs={12}>
<Card>
<CardContent>
<Typography variant="h6" fontWeight="600" gutterBottom sx={{ mb: 3 }}>
Family Members
</Typography>
{/* Current User */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Avatar sx={{ bgcolor: 'primary.main' }}>
{user?.name?.charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ flex: 1 }}>
<Typography variant="body1" fontWeight="600">
{user?.name}
</Typography>
<Typography variant="body2" color="text.secondary">
{user?.email}
</Typography>
</Box>
<Chip label="Admin" color="primary" size="small" />
</Box>
{/* Empty State */}
<Box sx={{ textAlign: 'center', py: 4, borderTop: '1px solid', borderColor: 'divider' }}>
<People sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
<Typography variant="body2" color="text.secondary" gutterBottom>
No other family members yet
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Invite family members to collaborate on child care
</Typography>
<Button
variant="outlined"
startIcon={<PersonAdd />}
onClick={handleInvite}
>
Invite First Member
</Button>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
</AppShell>
</ProtectedRoute>
);
}

View File

@@ -0,0 +1,79 @@
'use client';
import { Box, Typography, Grid, Card, CardContent } from '@mui/material';
import { TrendingUp, Insights as InsightsIcon, Timeline } from '@mui/icons-material';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
export default function InsightsPage() {
return (
<ProtectedRoute>
<AppShell>
<Box>
<Typography variant="h4" fontWeight="600" gutterBottom>
Insights & Analytics
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
Track patterns and get insights about your child's activities
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<TrendingUp sx={{ mr: 1, color: 'primary.main' }} />
<Typography variant="h6" fontWeight="600">
Sleep Patterns
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
Average sleep duration: Coming soon
</Typography>
<Typography variant="body2" color="text.secondary">
Sleep quality: Coming soon
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<InsightsIcon sx={{ mr: 1, color: 'success.main' }} />
<Typography variant="h6" fontWeight="600">
Feeding Patterns
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
Average feeding frequency: Coming soon
</Typography>
<Typography variant="body2" color="text.secondary">
Total daily intake: Coming soon
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Timeline sx={{ mr: 1, color: 'info.main' }} />
<Typography variant="h6" fontWeight="600">
Activity Timeline
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
Detailed analytics and trends will be displayed here
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
</AppShell>
</ProtectedRoute>
);
}

View File

@@ -0,0 +1,34 @@
'use client';
import { useEffect } from 'react';
import { useAuth } from '@/lib/auth/AuthContext';
import { Box, CircularProgress, Typography } from '@mui/material';
export default function LogoutPage() {
const { logout } = useAuth();
useEffect(() => {
const performLogout = async () => {
await logout();
};
performLogout();
}, [logout]);
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
gap: 2,
}}
>
<CircularProgress />
<Typography variant="body1" color="text.secondary">
Logging out...
</Typography>
</Box>
);
}

View File

@@ -0,0 +1,140 @@
'use client';
import { Box, Typography, Card, CardContent, TextField, Button, Divider, Switch, FormControlLabel } from '@mui/material';
import { Save, Logout } from '@mui/icons-material';
import { useAuth } from '@/lib/auth/AuthContext';
import { useState } from 'react';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
export default function SettingsPage() {
const { user, logout } = useAuth();
const [settings, setSettings] = useState({
notifications: true,
emailUpdates: false,
darkMode: false,
});
const handleSave = () => {
// Save settings functionality to be implemented
alert('Settings saved successfully!');
};
const handleLogout = async () => {
await logout();
};
return (
<ProtectedRoute>
<AppShell>
<Box sx={{ maxWidth: 'md', mx: 'auto' }}>
<Typography variant="h4" fontWeight="600" gutterBottom>
Settings
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
Manage your account settings and preferences
</Typography>
{/* Profile Settings */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" fontWeight="600" gutterBottom>
Profile Information
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}>
<TextField
label="Name"
defaultValue={user?.name}
fullWidth
/>
<TextField
label="Email"
defaultValue={user?.email}
fullWidth
disabled
/>
<Button
variant="contained"
startIcon={<Save />}
onClick={handleSave}
sx={{ alignSelf: 'flex-start' }}
>
Save Changes
</Button>
</Box>
</CardContent>
</Card>
{/* Notification Settings */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" fontWeight="600" gutterBottom>
Notifications
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.notifications}
onChange={(e) => setSettings({ ...settings, notifications: e.target.checked })}
/>
}
label="Push Notifications"
/>
<FormControlLabel
control={
<Switch
checked={settings.emailUpdates}
onChange={(e) => setSettings({ ...settings, emailUpdates: e.target.checked })}
/>
}
label="Email Updates"
/>
</Box>
</CardContent>
</Card>
{/* Appearance Settings */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" fontWeight="600" gutterBottom>
Appearance
</Typography>
<Box sx={{ mt: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.darkMode}
onChange={(e) => setSettings({ ...settings, darkMode: e.target.checked })}
/>
}
label="Dark Mode (Coming Soon)"
disabled
/>
</Box>
</CardContent>
</Card>
{/* Account Actions */}
<Card>
<CardContent>
<Typography variant="h6" fontWeight="600" gutterBottom>
Account Actions
</Typography>
<Divider sx={{ my: 2 }} />
<Button
variant="outlined"
color="error"
startIcon={<Logout />}
onClick={handleLogout}
fullWidth
>
Logout
</Button>
</CardContent>
</Card>
</Box>
</AppShell>
</ProtectedRoute>
);
}

View File

@@ -0,0 +1,89 @@
'use client';
import { Box, Typography, Grid, Card, CardContent, CardActionArea } from '@mui/material';
import { Restaurant, Hotel, BabyChangingStation, ChildCare } from '@mui/icons-material';
import { useRouter } from 'next/navigation';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
export default function TrackPage() {
const router = useRouter();
const trackingOptions = [
{
title: 'Feeding',
icon: <Restaurant sx={{ fontSize: 48, color: 'primary.main' }} />,
path: '/track/feeding',
color: '#FFE4E1',
},
{
title: 'Sleep',
icon: <Hotel sx={{ fontSize: 48, color: 'info.main' }} />,
path: '/track/sleep',
color: '#E1F5FF',
},
{
title: 'Diaper',
icon: <BabyChangingStation sx={{ fontSize: 48, color: 'warning.main' }} />,
path: '/track/diaper',
color: '#FFF4E1',
},
{
title: 'Activity',
icon: <ChildCare sx={{ fontSize: 48, color: 'success.main' }} />,
path: '/track/activity',
color: '#E8F5E9',
},
];
return (
<ProtectedRoute>
<AppShell>
<Box>
<Typography variant="h4" fontWeight="600" gutterBottom>
Track Activity
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
Select an activity to track
</Typography>
<Grid container spacing={3}>
{trackingOptions.map((option) => (
<Grid item xs={12} sm={6} md={3} key={option.title}>
<Card
sx={{
height: '100%',
bgcolor: option.color,
'&:hover': {
transform: 'translateY(-4px)',
transition: 'transform 0.2s',
},
}}
>
<CardActionArea
onClick={() => router.push(option.path)}
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
py: 4,
}}
>
<CardContent sx={{ textAlign: 'center' }}>
{option.icon}
<Typography variant="h6" fontWeight="600" sx={{ mt: 2 }}>
{option.title}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
</Box>
</AppShell>
</ProtectedRoute>
);
}

View File

@@ -0,0 +1,158 @@
'use client';
import { useEffect, useState } from 'react';
import { Alert, LinearProgress, Box, Typography } from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import { CloudOff, CloudQueue, CloudDone } from '@mui/icons-material';
interface OfflineIndicatorProps {
isOnline?: boolean;
pendingActionsCount?: number;
syncInProgress?: boolean;
}
export const OfflineIndicator = ({
isOnline: propIsOnline,
pendingActionsCount = 0,
syncInProgress = false,
}: OfflineIndicatorProps) => {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// Set initial online status
setIsOnline(navigator.onLine);
// Listen for online/offline events
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const effectiveIsOnline = propIsOnline !== undefined ? propIsOnline : isOnline;
return (
<>
<AnimatePresence>
{!effectiveIsOnline && (
<motion.div
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -100, opacity: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 9999,
}}
>
<Alert
severity="warning"
icon={<CloudOff />}
sx={{
borderRadius: 0,
boxShadow: 2,
}}
>
<Box>
<Typography variant="body2" fontWeight="600">
You're offline
</Typography>
{pendingActionsCount > 0 && (
<Typography variant="caption">
{pendingActionsCount} action{pendingActionsCount !== 1 ? 's' : ''} will sync when you're back online
</Typography>
)}
</Box>
</Alert>
</motion.div>
)}
{effectiveIsOnline && syncInProgress && (
<motion.div
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -100, opacity: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 9999,
}}
>
<Alert
severity="info"
icon={<CloudQueue />}
sx={{
borderRadius: 0,
boxShadow: 2,
}}
>
<Box>
<Typography variant="body2" fontWeight="600">
Syncing data...
</Typography>
{pendingActionsCount > 0 && (
<Typography variant="caption">
{pendingActionsCount} action{pendingActionsCount !== 1 ? 's' : ''} remaining
</Typography>
)}
</Box>
</Alert>
<LinearProgress />
</motion.div>
)}
{effectiveIsOnline && !syncInProgress && pendingActionsCount === 0 &&
typeof propIsOnline !== 'undefined' && propIsOnline && (
<motion.div
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -100, opacity: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
onAnimationComplete={() => {
// Auto-hide after 3 seconds
setTimeout(() => {
const element = document.getElementById('sync-complete-alert');
if (element) {
element.style.display = 'none';
}
}, 3000);
}}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 9999,
}}
id="sync-complete-alert"
>
<Alert
severity="success"
icon={<CloudDone />}
sx={{
borderRadius: 0,
boxShadow: 2,
}}
>
<Typography variant="body2" fontWeight="600">
All data synced successfully!
</Typography>
</Alert>
</motion.div>
)}
</AnimatePresence>
</>
);
};

View File

@@ -0,0 +1,121 @@
import { useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
setOnlineStatus,
setSyncInProgress,
removePendingAction,
incrementRetryCount,
updateLastSyncTime,
} from '@/store/slices/offlineSlice';
import apiClient from '@/lib/api/client';
interface RootState {
offline: {
isOnline: boolean;
pendingActions: any[];
syncInProgress: boolean;
};
}
const MAX_RETRY_ATTEMPTS = 3;
export const useOfflineSync = () => {
const dispatch = useDispatch();
const { isOnline, pendingActions, syncInProgress } = useSelector(
(state: RootState) => state.offline
);
// Monitor online/offline status
useEffect(() => {
const handleOnline = () => {
dispatch(setOnlineStatus(true));
};
const handleOffline = () => {
dispatch(setOnlineStatus(false));
};
// Set initial status
dispatch(setOnlineStatus(navigator.onLine));
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, [dispatch]);
// Sync pending actions when online
const syncPendingActions = useCallback(async () => {
if (!isOnline || pendingActions.length === 0 || syncInProgress) {
return;
}
dispatch(setSyncInProgress(true));
for (const action of pendingActions) {
try {
// Attempt to replay the action
await replayAction(action);
// Remove from pending actions on success
dispatch(removePendingAction(action.id));
} catch (error) {
console.error(`Failed to sync action ${action.id}:`, error);
// Increment retry count
dispatch(incrementRetryCount(action.id));
// If max retries exceeded, remove the action
if (action.retryCount >= MAX_RETRY_ATTEMPTS) {
console.warn(`Max retries exceeded for action ${action.id}, removing from queue`);
dispatch(removePendingAction(action.id));
}
}
}
dispatch(setSyncInProgress(false));
dispatch(updateLastSyncTime());
}, [isOnline, pendingActions, syncInProgress, dispatch]);
// Trigger sync when coming online
useEffect(() => {
if (isOnline && pendingActions.length > 0) {
syncPendingActions();
}
}, [isOnline, pendingActions.length, syncPendingActions]);
// Replay a specific action
const replayAction = async (action: any) => {
const { type, payload } = action;
switch (type) {
case 'CREATE_ACTIVITY':
return await apiClient.post('/api/v1/activities', payload);
case 'UPDATE_ACTIVITY':
return await apiClient.put(`/api/v1/activities/${payload.id}`, payload);
case 'DELETE_ACTIVITY':
return await apiClient.delete(`/api/v1/activities/${payload.id}`);
case 'CREATE_CHILD':
return await apiClient.post('/api/v1/children', payload);
case 'UPDATE_CHILD':
return await apiClient.put(`/api/v1/children/${payload.id}`, payload);
default:
throw new Error(`Unknown action type: ${type}`);
}
};
return {
isOnline,
pendingActionsCount: pendingActions.length,
syncInProgress,
syncPendingActions,
};
};

View File

@@ -0,0 +1,74 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface PendingAction {
id: string;
type: string;
payload: any;
timestamp: string;
retryCount: number;
}
interface OfflineState {
isOnline: boolean;
pendingActions: PendingAction[];
lastSyncTime: string | null;
syncInProgress: boolean;
}
const initialState: OfflineState = {
isOnline: typeof navigator !== 'undefined' ? navigator.onLine : true,
pendingActions: [],
lastSyncTime: null,
syncInProgress: false,
};
const offlineSlice = createSlice({
name: 'offline',
initialState,
reducers: {
setOnlineStatus: (state, action: PayloadAction<boolean>) => {
state.isOnline = action.payload;
if (action.payload && state.pendingActions.length > 0) {
state.syncInProgress = true;
}
},
addPendingAction: (state, action: PayloadAction<Omit<PendingAction, 'id' | 'timestamp' | 'retryCount'>>) => {
state.pendingActions.push({
...action.payload,
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
retryCount: 0,
});
},
removePendingAction: (state, action: PayloadAction<string>) => {
state.pendingActions = state.pendingActions.filter(a => a.id !== action.payload);
},
incrementRetryCount: (state, action: PayloadAction<string>) => {
const action_ = state.pendingActions.find(a => a.id === action.payload);
if (action_) {
action_.retryCount += 1;
}
},
clearPendingActions: (state) => {
state.pendingActions = [];
},
setSyncInProgress: (state, action: PayloadAction<boolean>) => {
state.syncInProgress = action.payload;
},
updateLastSyncTime: (state) => {
state.lastSyncTime = new Date().toISOString();
},
},
});
export const {
setOnlineStatus,
addPendingAction,
removePendingAction,
incrementRetryCount,
clearPendingActions,
setSyncInProgress,
updateLastSyncTime,
} = offlineSlice.actions;
export default offlineSlice.reducer;