diff --git a/maternal-web/app/ai-assistant/page.tsx b/maternal-web/app/ai-assistant/page.tsx
index 862f406..7d8f214 100644
--- a/maternal-web/app/ai-assistant/page.tsx
+++ b/maternal-web/app/ai-assistant/page.tsx
@@ -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 (
-
+
+
+
{/* Header */}
-
+
+
+
);
}
diff --git a/maternal-web/app/children/page.tsx b/maternal-web/app/children/page.tsx
new file mode 100644
index 0000000..79d24a3
--- /dev/null
+++ b/maternal-web/app/children/page.tsx
@@ -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 (
+
+
+
+
+
+
+ Children
+
+
+ Manage your family's children profiles
+
+
+ }
+ onClick={() => router.push('/children/new')}
+ >
+ Add Child
+
+
+
+
+
+
+
+
+
+ No children added yet
+
+
+ Add your first child to start tracking their activities
+
+ }
+ onClick={() => router.push('/children/new')}
+ >
+ Add First Child
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/maternal-web/app/family/page.tsx b/maternal-web/app/family/page.tsx
new file mode 100644
index 0000000..9960a1c
--- /dev/null
+++ b/maternal-web/app/family/page.tsx
@@ -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 (
+
+
+
+
+
+
+ Family
+
+
+ Manage your family members and share access
+
+
+ }
+ onClick={handleInvite}
+ >
+ Invite Member
+
+
+
+
+ {/* Family Share Code */}
+
+
+
+
+ Family Share Code
+
+
+ Share this code with family members to give them access to your family's data
+
+
+
+ }
+ onClick={handleCopyCode}
+ >
+ Copy Code
+
+
+
+
+
+
+ {/* Family Members */}
+
+
+
+
+ Family Members
+
+
+ {/* Current User */}
+
+
+ {user?.name?.charAt(0).toUpperCase()}
+
+
+
+ {user?.name}
+
+
+ {user?.email}
+
+
+
+
+
+ {/* Empty State */}
+
+
+
+ No other family members yet
+
+
+ Invite family members to collaborate on child care
+
+ }
+ onClick={handleInvite}
+ >
+ Invite First Member
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/maternal-web/app/insights/page.tsx b/maternal-web/app/insights/page.tsx
new file mode 100644
index 0000000..888ea9a
--- /dev/null
+++ b/maternal-web/app/insights/page.tsx
@@ -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 (
+
+
+
+
+ Insights & Analytics
+
+
+ Track patterns and get insights about your child's activities
+
+
+
+
+
+
+
+
+
+ Sleep Patterns
+
+
+
+ Average sleep duration: Coming soon
+
+
+ Sleep quality: Coming soon
+
+
+
+
+
+
+
+
+
+
+
+ Feeding Patterns
+
+
+
+ Average feeding frequency: Coming soon
+
+
+ Total daily intake: Coming soon
+
+
+
+
+
+
+
+
+
+
+
+ Activity Timeline
+
+
+
+ Detailed analytics and trends will be displayed here
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/maternal-web/app/logout/page.tsx b/maternal-web/app/logout/page.tsx
new file mode 100644
index 0000000..5a339dc
--- /dev/null
+++ b/maternal-web/app/logout/page.tsx
@@ -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 (
+
+
+
+ Logging out...
+
+
+ );
+}
diff --git a/maternal-web/app/settings/page.tsx b/maternal-web/app/settings/page.tsx
new file mode 100644
index 0000000..388e620
--- /dev/null
+++ b/maternal-web/app/settings/page.tsx
@@ -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 (
+
+
+
+
+ Settings
+
+
+ Manage your account settings and preferences
+
+
+ {/* Profile Settings */}
+
+
+
+ Profile Information
+
+
+
+
+ }
+ onClick={handleSave}
+ sx={{ alignSelf: 'flex-start' }}
+ >
+ Save Changes
+
+
+
+
+
+ {/* Notification Settings */}
+
+
+
+ Notifications
+
+
+ setSettings({ ...settings, notifications: e.target.checked })}
+ />
+ }
+ label="Push Notifications"
+ />
+ setSettings({ ...settings, emailUpdates: e.target.checked })}
+ />
+ }
+ label="Email Updates"
+ />
+
+
+
+
+ {/* Appearance Settings */}
+
+
+
+ Appearance
+
+
+ setSettings({ ...settings, darkMode: e.target.checked })}
+ />
+ }
+ label="Dark Mode (Coming Soon)"
+ disabled
+ />
+
+
+
+
+ {/* Account Actions */}
+
+
+
+ Account Actions
+
+
+ }
+ onClick={handleLogout}
+ fullWidth
+ >
+ Logout
+
+
+
+
+
+
+ );
+}
diff --git a/maternal-web/app/track/page.tsx b/maternal-web/app/track/page.tsx
new file mode 100644
index 0000000..4d4a013
--- /dev/null
+++ b/maternal-web/app/track/page.tsx
@@ -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: ,
+ path: '/track/feeding',
+ color: '#FFE4E1',
+ },
+ {
+ title: 'Sleep',
+ icon: ,
+ path: '/track/sleep',
+ color: '#E1F5FF',
+ },
+ {
+ title: 'Diaper',
+ icon: ,
+ path: '/track/diaper',
+ color: '#FFF4E1',
+ },
+ {
+ title: 'Activity',
+ icon: ,
+ path: '/track/activity',
+ color: '#E8F5E9',
+ },
+ ];
+
+ return (
+
+
+
+
+ Track Activity
+
+
+ Select an activity to track
+
+
+
+ {trackingOptions.map((option) => (
+
+
+ router.push(option.path)}
+ sx={{
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ py: 4,
+ }}
+ >
+
+ {option.icon}
+
+ {option.title}
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/maternal-web/components/common/OfflineIndicator.tsx b/maternal-web/components/common/OfflineIndicator.tsx
new file mode 100644
index 0000000..932f309
--- /dev/null
+++ b/maternal-web/components/common/OfflineIndicator.tsx
@@ -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 (
+ <>
+
+ {!effectiveIsOnline && (
+
+ }
+ sx={{
+ borderRadius: 0,
+ boxShadow: 2,
+ }}
+ >
+
+
+ You're offline
+
+ {pendingActionsCount > 0 && (
+
+ {pendingActionsCount} action{pendingActionsCount !== 1 ? 's' : ''} will sync when you're back online
+
+ )}
+
+
+
+ )}
+
+ {effectiveIsOnline && syncInProgress && (
+
+ }
+ sx={{
+ borderRadius: 0,
+ boxShadow: 2,
+ }}
+ >
+
+
+ Syncing data...
+
+ {pendingActionsCount > 0 && (
+
+ {pendingActionsCount} action{pendingActionsCount !== 1 ? 's' : ''} remaining
+
+ )}
+
+
+
+
+ )}
+
+ {effectiveIsOnline && !syncInProgress && pendingActionsCount === 0 &&
+ typeof propIsOnline !== 'undefined' && propIsOnline && (
+ {
+ // 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"
+ >
+ }
+ sx={{
+ borderRadius: 0,
+ boxShadow: 2,
+ }}
+ >
+
+ All data synced successfully!
+
+
+
+ )}
+
+ >
+ );
+};
diff --git a/maternal-web/hooks/useOfflineSync.ts b/maternal-web/hooks/useOfflineSync.ts
new file mode 100644
index 0000000..37d4c3e
--- /dev/null
+++ b/maternal-web/hooks/useOfflineSync.ts
@@ -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,
+ };
+};
diff --git a/maternal-web/store/slices/offlineSlice.ts b/maternal-web/store/slices/offlineSlice.ts
new file mode 100644
index 0000000..7a9d8ef
--- /dev/null
+++ b/maternal-web/store/slices/offlineSlice.ts
@@ -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) => {
+ state.isOnline = action.payload;
+ if (action.payload && state.pendingActions.length > 0) {
+ state.syncInProgress = true;
+ }
+ },
+ addPendingAction: (state, action: PayloadAction>) => {
+ 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) => {
+ state.pendingActions = state.pendingActions.filter(a => a.id !== action.payload);
+ },
+ incrementRetryCount: (state, action: PayloadAction) => {
+ const action_ = state.pendingActions.find(a => a.id === action.payload);
+ if (action_) {
+ action_.retryCount += 1;
+ }
+ },
+ clearPendingActions: (state) => {
+ state.pendingActions = [];
+ },
+ setSyncInProgress: (state, action: PayloadAction) => {
+ 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;