Files
maternal-app/docs/maternal-web-frontend-plan.md
andupetcu 1de21044d6 Remove unused stub backend folder and add web frontend plan
Cleanup project structure by removing duplicate maternal-app-backend/ folder at root level. The working backend is located at maternal-app/maternal-app-backend/. Also added web frontend implementation plan documentation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 20:48:57 +03:00

1733 lines
44 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Web Frontend Implementation Plan - Maternal Organization App
## Mobile-First Progressive Web Application
---
## Executive Summary
This document outlines the implementation plan for building the Maternal Organization App as a **mobile-first responsive web application** that will serve as the foundation for the native mobile apps. The web app will be built using **Next.js 14** with **TypeScript**, implementing all core features while maintaining a native app-like experience through PWA capabilities.
### Key Principles
- **Mobile-first design** with responsive scaling for tablets and desktop
- **PWA features** for offline support and app-like experience
- **Component reusability** for future React Native migration
- **Touch-optimized** interactions and gestures
- **Performance-first** with sub-2-second load times
---
## Technology Stack
### Core Framework
```javascript
// Package versions for consistency
{
"next": "^14.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"typescript": "^5.4.0"
}
```
### UI Framework & Styling
```javascript
{
// Material UI for consistent Material Design
"@mui/material": "^5.15.0",
"@mui/icons-material": "^5.15.0",
"@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0",
// Tailwind for utility classes
"tailwindcss": "^3.4.0",
// Animation libraries
"framer-motion": "^11.0.0",
"react-spring": "^9.7.0"
}
```
### State Management & Data
```javascript
{
// Redux Toolkit for state management
"@reduxjs/toolkit": "^2.2.0",
"react-redux": "^9.1.0",
"redux-persist": "^6.0.0",
// API and real-time
"@tanstack/react-query": "^5.28.0",
"socket.io-client": "^4.7.0",
"axios": "^1.6.0",
// Forms and validation
"react-hook-form": "^7.51.0",
"zod": "^3.22.0"
}
```
### PWA & Performance
```javascript
{
"workbox-webpack-plugin": "^7.0.0",
"next-pwa": "^5.6.0",
"@sentry/nextjs": "^7.100.0",
"web-vitals": "^3.5.0"
}
```
---
## Project Structure
```
maternal-web/
├── src/
│ ├── app/ # Next.js 14 app directory
│ │ ├── (auth)/ # Auth group routes
│ │ │ ├── login/
│ │ │ ├── register/
│ │ │ └── onboarding/
│ │ ├── (dashboard)/ # Protected routes
│ │ │ ├── page.tsx # Family dashboard
│ │ │ ├── children/
│ │ │ │ ├── [id]/
│ │ │ │ └── new/
│ │ │ ├── track/
│ │ │ │ ├── feeding/
│ │ │ │ ├── sleep/
│ │ │ │ └── diaper/
│ │ │ ├── ai-assistant/
│ │ │ ├── insights/
│ │ │ └── settings/
│ │ ├── api/ # API routes
│ │ │ └── auth/[...nextauth]/
│ │ ├── layout.tsx
│ │ └── global.css
│ │
│ ├── components/
│ │ ├── ui/ # Base UI components
│ │ │ ├── Button/
│ │ │ ├── Card/
│ │ │ ├── Input/
│ │ │ ├── Modal/
│ │ │ └── ...
│ │ ├── features/ # Feature-specific components
│ │ │ ├── tracking/
│ │ │ ├── ai-chat/
│ │ │ ├── family/
│ │ │ └── analytics/
│ │ ├── layouts/
│ │ │ ├── MobileNav/
│ │ │ ├── TabBar/
│ │ │ └── AppShell/
│ │ └── common/
│ │ ├── ErrorBoundary/
│ │ ├── LoadingStates/
│ │ └── OfflineIndicator/
│ │
│ ├── hooks/
│ │ ├── useVoiceInput.ts
│ │ ├── useOfflineSync.ts
│ │ ├── useRealtime.ts
│ │ └── useMediaQuery.ts
│ │
│ ├── lib/
│ │ ├── api/
│ │ ├── websocket/
│ │ ├── storage/
│ │ └── utils/
│ │
│ ├── store/ # Redux store
│ │ ├── slices/
│ │ ├── middleware/
│ │ └── store.ts
│ │
│ ├── styles/
│ │ ├── themes/
│ │ └── globals.css
│ │
│ └── types/
├── public/
│ ├── manifest.json
│ ├── service-worker.js
│ └── icons/
└── tests/
```
---
## Phase 1: Foundation & Setup (Week 1)
### 1.1 Project Initialization
```bash
# Create Next.js project with TypeScript
npx create-next-app@latest maternal-web --typescript --tailwind --app
# Install core dependencies
cd maternal-web
npm install @mui/material @emotion/react @emotion/styled
npm install @reduxjs/toolkit react-redux redux-persist
npm install @tanstack/react-query axios socket.io-client
npm install react-hook-form zod
npm install framer-motion
```
### 1.2 PWA Configuration
```javascript
// next.config.js
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
runtimeCaching: [
{
urlPattern: /^https?.*/,
handler: 'NetworkFirst',
options: {
cacheName: 'offlineCache',
expiration: {
maxEntries: 200,
},
},
},
],
});
module.exports = withPWA({
reactStrictMode: true,
images: {
domains: ['api.maternalapp.com'],
},
});
```
### 1.3 Theme Configuration
```typescript
// src/styles/themes/maternalTheme.ts
import { createTheme } from '@mui/material/styles';
export const maternalTheme = createTheme({
palette: {
primary: {
main: '#FFB6C1', // Light pink/rose
light: '#FFE4E1', // Misty rose
dark: '#DB7093', // Pale violet red
},
secondary: {
main: '#FFDAB9', // Peach puff
light: '#FFE5CC',
dark: '#FFB347', // Deep peach
},
background: {
default: '#FFF9F5', // Warm white
paper: '#FFFFFF',
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
h1: {
fontSize: '2rem',
fontWeight: 600,
},
},
shape: {
borderRadius: 16,
},
components: {
MuiButton: {
styleOverrides: {
root: {
borderRadius: 24,
textTransform: 'none',
minHeight: 48, // Touch target size
fontSize: '1rem',
fontWeight: 500,
},
},
},
MuiTextField: {
styleOverrides: {
root: {
'& .MuiInputBase-root': {
minHeight: 48,
},
},
},
},
},
});
```
### 1.4 Mobile-First Layout Component
```typescript
// src/components/layouts/AppShell/AppShell.tsx
import { useState, useEffect } from 'react';
import { Box, Container } from '@mui/material';
import { MobileNav } from '../MobileNav';
import { TabBar } from '../TabBar';
import { useMediaQuery } from '@/hooks/useMediaQuery';
export const AppShell: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(max-width: 1024px)');
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
pb: isMobile ? '56px' : 0, // Space for tab bar
}}>
{!isMobile && <MobileNav />}
<Container
maxWidth={isTablet ? 'md' : 'lg'}
sx={{
flex: 1,
px: isMobile ? 2 : 3,
py: 3,
}}
>
{children}
</Container>
{isMobile && <TabBar />}
</Box>
);
};
```
---
## Phase 2: Authentication & Onboarding (Week 1-2)
### 2.1 Auth Provider Setup
```typescript
// src/lib/auth/AuthContext.tsx
import { createContext, useContext, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { api } from '@/lib/api';
interface AuthContextType {
user: User | null;
login: (credentials: LoginCredentials) => Promise<void>;
register: (data: RegisterData) => Promise<void>;
logout: () => void;
isLoading: boolean;
}
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
useEffect(() => {
// Check for stored token and validate
const checkAuth = async () => {
const token = localStorage.getItem('accessToken');
if (token) {
try {
const response = await api.get('/auth/me');
setUser(response.data);
} catch {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
}
setIsLoading(false);
};
checkAuth();
}, []);
// Implementation continues...
};
```
### 2.2 Mobile-Optimized Auth Screens
```typescript
// src/app/(auth)/login/page.tsx
'use client';
import { useState } from 'react';
import {
Box,
TextField,
Button,
Typography,
Paper,
InputAdornment,
IconButton,
Divider
} from '@mui/material';
import { Visibility, VisibilityOff, Google, Apple } from '@mui/icons-material';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { motion } from 'framer-motion';
import * as z from 'zod';
const loginSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
export default function LoginPage() {
const [showPassword, setShowPassword] = useState(false);
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(loginSchema),
});
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
px: 3,
background: 'linear-gradient(135deg, #FFE4E1 0%, #FFDAB9 100%)',
}}
>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Paper
elevation={0}
sx={{
p: 4,
borderRadius: 4,
maxWidth: 400,
mx: 'auto',
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(10px)',
}}
>
<Typography variant="h4" gutterBottom align="center" fontWeight="600">
Welcome Back
</Typography>
<Box component="form" onSubmit={handleSubmit(onSubmit)} sx={{ mt: 3 }}>
<TextField
fullWidth
label="Email"
type="email"
margin="normal"
error={!!errors.email}
helperText={errors.email?.message}
{...register('email')}
InputProps={{
sx: { borderRadius: 3 }
}}
/>
<TextField
fullWidth
label="Password"
type={showPassword ? 'text' : 'password'}
margin="normal"
error={!!errors.password}
helperText={errors.password?.message}
{...register('password')}
InputProps={{
sx: { borderRadius: 3 },
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword(!showPassword)}>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
<Button
fullWidth
type="submit"
variant="contained"
size="large"
sx={{ mt: 3, mb: 2 }}
>
Sign In
</Button>
</Box>
<Divider sx={{ my: 3 }}>OR</Divider>
<Button
fullWidth
variant="outlined"
startIcon={<Google />}
size="large"
sx={{ mb: 2 }}
>
Continue with Google
</Button>
<Button
fullWidth
variant="outlined"
startIcon={<Apple />}
size="large"
>
Continue with Apple
</Button>
</Paper>
</motion.div>
</Box>
);
}
```
### 2.3 Progressive Onboarding Flow
```typescript
// src/app/(auth)/onboarding/page.tsx
import { useState } from 'react';
import { Stepper, Step, StepLabel } from '@mui/material';
import { WelcomeStep } from '@/components/onboarding/WelcomeStep';
import { AddChildStep } from '@/components/onboarding/AddChildStep';
import { InviteFamilyStep } from '@/components/onboarding/InviteFamilyStep';
import { NotificationStep } from '@/components/onboarding/NotificationStep';
const steps = ['Welcome', 'Add Child', 'Invite Family', 'Notifications'];
export default function OnboardingPage() {
const [activeStep, setActiveStep] = useState(0);
return (
<Box sx={{ width: '100%', p: 3 }}>
{/* Mobile-optimized stepper */}
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<Box sx={{ mt: 4 }}>
{activeStep === 0 && <WelcomeStep onNext={handleNext} />}
{activeStep === 1 && <AddChildStep onNext={handleNext} onBack={handleBack} />}
{activeStep === 2 && <InviteFamilyStep onNext={handleNext} onBack={handleBack} onSkip={handleSkip} />}
{activeStep === 3 && <NotificationStep onComplete={handleComplete} onBack={handleBack} />}
</Box>
</Box>
);
}
```
---
## Phase 3: Core Tracking Features (Week 2-3)
### 3.1 Quick Action FAB
```typescript
// src/components/features/tracking/QuickActionFAB.tsx
import { useState } from 'react';
import { SpeedDial, SpeedDialAction, SpeedDialIcon } from '@mui/material';
import {
Restaurant,
Hotel,
BabyChangingStation,
Mic,
Add
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
export const QuickActionFAB = () => {
const [open, setOpen] = useState(false);
const actions = [
{ icon: <Restaurant />, name: 'Feeding', color: '#FFB6C1', route: '/track/feeding' },
{ icon: <Hotel />, name: 'Sleep', color: '#B6D7FF', route: '/track/sleep' },
{ icon: <BabyChangingStation />, name: 'Diaper', color: '#FFE4B5', route: '/track/diaper' },
{ icon: <Mic />, name: 'Voice', color: '#E6E6FA', action: 'voice' },
];
return (
<SpeedDial
ariaLabel="Quick actions"
sx={{
position: 'fixed',
bottom: 72, // Above tab bar
right: 16,
'& .MuiSpeedDial-fab': {
bgcolor: 'secondary.main',
width: 64,
height: 64,
}
}}
icon={<SpeedDialIcon icon={<Add />} />}
onClose={() => setOpen(false)}
onOpen={() => setOpen(true)}
open={open}
>
{actions.map((action) => (
<SpeedDialAction
key={action.name}
icon={action.icon}
tooltipTitle={action.name}
onClick={() => handleAction(action)}
sx={{
bgcolor: action.color,
'&:hover': {
bgcolor: action.color,
transform: 'scale(1.1)',
}
}}
/>
))}
</SpeedDial>
);
};
```
### 3.2 Voice Input Hook
```typescript
// src/hooks/useVoiceInput.ts
import { useState, useEffect, useCallback } from 'react';
interface VoiceInputOptions {
language?: string;
continuous?: boolean;
onResult?: (transcript: string) => void;
onEnd?: () => void;
}
export const useVoiceInput = (options: VoiceInputOptions = {}) => {
const [isListening, setIsListening] = useState(false);
const [transcript, setTranscript] = useState('');
const [error, setError] = useState<string | null>(null);
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
useEffect(() => {
recognition.continuous = options.continuous ?? false;
recognition.interimResults = true;
recognition.lang = options.language ?? 'en-US';
recognition.onresult = (event) => {
const current = event.resultIndex;
const transcript = event.results[current][0].transcript;
setTranscript(transcript);
if (event.results[current].isFinal) {
options.onResult?.(transcript);
}
};
recognition.onerror = (event) => {
setError(event.error);
setIsListening(false);
};
recognition.onend = () => {
setIsListening(false);
options.onEnd?.();
};
}, []);
const startListening = useCallback(() => {
setTranscript('');
setError(null);
recognition.start();
setIsListening(true);
}, [recognition]);
const stopListening = useCallback(() => {
recognition.stop();
setIsListening(false);
}, [recognition]);
return {
isListening,
transcript,
error,
startListening,
stopListening,
};
};
```
### 3.3 Tracking Form Component
```typescript
// src/components/features/tracking/FeedingTracker.tsx
import { useState } from 'react';
import {
Box,
Paper,
TextField,
ToggleButtonGroup,
ToggleButton,
Button,
Typography,
Chip,
IconButton
} from '@mui/material';
import { Timer, Mic, MicOff } from '@mui/icons-material';
import { useVoiceInput } from '@/hooks/useVoiceInput';
import { motion } from 'framer-motion';
export const FeedingTracker = () => {
const [feedingType, setFeedingType] = useState<'breast' | 'bottle' | 'solid'>('breast');
const [duration, setDuration] = useState(0);
const [isTimerRunning, setIsTimerRunning] = useState(false);
const { isListening, transcript, startListening, stopListening } = useVoiceInput({
onResult: (text) => {
// Parse voice input for commands
parseVoiceCommand(text);
},
});
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<Paper
elevation={0}
sx={{
p: 3,
borderRadius: 4,
background: 'linear-gradient(135deg, #FFE4E1 0%, #FFF 100%)',
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h5" fontWeight="600">
Track Feeding
</Typography>
<IconButton
color={isListening ? 'primary' : 'default'}
onClick={isListening ? stopListening : startListening}
sx={{
bgcolor: isListening ? 'primary.light' : 'background.paper',
animation: isListening ? 'pulse 1.5s infinite' : 'none',
}}
>
{isListening ? <Mic /> : <MicOff />}
</IconButton>
</Box>
<ToggleButtonGroup
value={feedingType}
exclusive
onChange={(e, value) => value && setFeedingType(value)}
fullWidth
sx={{ mb: 3 }}
>
<ToggleButton value="breast" sx={{ borderRadius: 3 }}>
Breastfeeding
</ToggleButton>
<ToggleButton value="bottle" sx={{ borderRadius: 3 }}>
Bottle
</ToggleButton>
<ToggleButton value="solid" sx={{ borderRadius: 3 }}>
Solid Food
</ToggleButton>
</ToggleButtonGroup>
{/* Timer component for breastfeeding */}
{feedingType === 'breast' && (
<Box sx={{ textAlign: 'center', my: 4 }}>
<Typography variant="h2" fontWeight="300">
{formatDuration(duration)}
</Typography>
<Button
variant="contained"
size="large"
startIcon={<Timer />}
onClick={toggleTimer}
sx={{ mt: 2 }}
>
{isTimerRunning ? 'Stop Timer' : 'Start Timer'}
</Button>
</Box>
)}
{/* Form fields for bottle/solid */}
{feedingType !== 'breast' && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Additional form fields */}
</Box>
)}
<Button
fullWidth
variant="contained"
size="large"
sx={{ mt: 3 }}
onClick={handleSave}
>
Save Feeding
</Button>
</Paper>
</motion.div>
);
};
```
---
## Phase 4: AI Assistant Integration (Week 3-4)
### 4.1 AI Chat Interface
```typescript
// src/components/features/ai-chat/AIChatInterface.tsx
import { useState, useRef, useEffect } from 'react';
import {
Box,
TextField,
IconButton,
Paper,
Typography,
Avatar,
Chip,
CircularProgress
} from '@mui/material';
import { Send, Mic, AttachFile } from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
import { useAIChat } from '@/hooks/useAIChat';
export const AIChatInterface = () => {
const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef<null | HTMLDivElement>(null);
const { messages, sendMessage, isLoading } = useAIChat();
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
return (
<Box sx={{ height: 'calc(100vh - 120px)', display: 'flex', flexDirection: 'column' }}>
{/* Quick action chips */}
<Box sx={{ p: 2, display: 'flex', gap: 1, overflowX: 'auto' }}>
{['Sleep tips', 'Feeding schedule', 'Developmental milestones', 'Emergency'].map((action) => (
<Chip
key={action}
label={action}
onClick={() => handleQuickAction(action)}
sx={{
bgcolor: 'background.paper',
'&:hover': { bgcolor: 'primary.light' }
}}
/>
))}
</Box>
{/* Messages */}
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
<AnimatePresence>
{messages.map((message, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<Box
sx={{
display: 'flex',
justifyContent: message.role === 'user' ? 'flex-end' : 'flex-start',
mb: 2,
}}
>
{message.role === 'assistant' && (
<Avatar sx={{ bgcolor: 'primary.main', mr: 1 }}>AI</Avatar>
)}
<Paper
sx={{
p: 2,
maxWidth: '70%',
bgcolor: message.role === 'user' ? 'primary.light' : 'background.paper',
borderRadius: message.role === 'user' ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
}}
>
<Typography variant="body1">{message.content}</Typography>
{message.disclaimer && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
{message.disclaimer}
</Typography>
)}
</Paper>
{message.role === 'user' && (
<Avatar sx={{ bgcolor: 'secondary.main', ml: 1 }}>U</Avatar>
)}
</Box>
</motion.div>
))}
</AnimatePresence>
{isTyping && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Avatar sx={{ bgcolor: 'primary.main' }}>AI</Avatar>
<Box sx={{ display: 'flex', gap: 0.5 }}>
<CircularProgress size={8} />
<CircularProgress size={8} />
<CircularProgress size={8} />
</Box>
</Box>
)}
<div ref={messagesEndRef} />
</Box>
{/* Input area */}
<Paper
elevation={3}
sx={{
p: 2,
borderRadius: 0,
borderTop: '1px solid',
borderColor: 'divider',
}}
>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
<IconButton>
<AttachFile />
</IconButton>
<TextField
fullWidth
multiline
maxRows={4}
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask about your child's development, sleep, feeding..."
variant="outlined"
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
},
}}
onKeyPress={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
/>
<IconButton>
<Mic />
</IconButton>
<IconButton
color="primary"
disabled={!input.trim() || isLoading}
onClick={handleSend}
>
<Send />
</IconButton>
</Box>
</Paper>
</Box>
);
};
```
### 4.2 AI Context Provider
```typescript
// src/lib/ai/AIContextProvider.tsx
import { createContext, useContext, useState } from 'react';
import { api } from '@/lib/api';
interface AIContextType {
childContext: ChildContext[];
recentActivities: Activity[];
updateContext: (data: Partial<AIContextType>) => void;
getRelevantContext: (query: string) => Promise<any>;
}
export const AIContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [childContext, setChildContext] = useState<ChildContext[]>([]);
const [recentActivities, setRecentActivities] = useState<Activity[]>([]);
const getRelevantContext = async (query: string) => {
// Implement context prioritization based on query
const context = {
children: childContext,
recentActivities: recentActivities.slice(0, 10),
timestamp: new Date().toISOString(),
};
return context;
};
return (
<AIContext.Provider value={{
childContext,
recentActivities,
updateContext,
getRelevantContext,
}}>
{children}
</AIContext.Provider>
);
};
```
---
## Phase 5: Family Dashboard & Analytics (Week 4-5)
### 5.1 Responsive Dashboard Grid
```typescript
// src/app/(dashboard)/page.tsx
import { Grid, Box } from '@mui/material';
import { ChildCard } from '@/components/features/family/ChildCard';
import { ActivityTimeline } from '@/components/features/tracking/ActivityTimeline';
import { SleepPrediction } from '@/components/features/analytics/SleepPrediction';
import { QuickStats } from '@/components/features/analytics/QuickStats';
import { useMediaQuery } from '@/hooks/useMediaQuery';
export default function DashboardPage() {
const isMobile = useMediaQuery('(max-width: 768px)');
const { children, activities } = useDashboardData();
return (
<Box sx={{ pb: 8 }}>
{/* Child selector carousel for mobile */}
{isMobile && (
<Box sx={{ mb: 3, overflow: 'auto', display: 'flex', gap: 2, pb: 1 }}>
{children.map((child) => (
<ChildCard key={child.id} child={child} compact />
))}
</Box>
)}
<Grid container spacing={3}>
{/* Desktop child cards */}
{!isMobile && children.map((child) => (
<Grid item xs={12} md={6} lg={4} key={child.id}>
<ChildCard child={child} />
</Grid>
))}
{/* Sleep prediction */}
<Grid item xs={12} md={6}>
<SleepPrediction />
</Grid>
{/* Quick stats */}
<Grid item xs={12} md={6}>
<QuickStats />
</Grid>
{/* Activity timeline */}
<Grid item xs={12}>
<ActivityTimeline activities={activities} />
</Grid>
</Grid>
</Box>
);
}
```
### 5.2 Interactive Charts
```typescript
// src/components/features/analytics/SleepChart.tsx
import { Line } from 'react-chartjs-2';
import { Box, Paper, Typography, ToggleButtonGroup, ToggleButton } from '@mui/material';
import { useState } from 'react';
export const SleepChart = () => {
const [timeRange, setTimeRange] = useState<'week' | 'month'>('week');
const chartData = {
labels: generateLabels(timeRange),
datasets: [
{
label: 'Sleep Duration',
data: sleepData,
fill: true,
backgroundColor: 'rgba(182, 215, 255, 0.2)',
borderColor: '#B6D7FF',
tension: 0.4,
},
{
label: 'Predicted',
data: predictedData,
borderColor: '#FFB6C1',
borderDash: [5, 5],
fill: false,
},
],
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
tooltip: {
mode: 'index',
intersect: false,
},
},
scales: {
y: {
beginAtZero: true,
grid: {
display: false,
},
},
x: {
grid: {
display: false,
},
},
},
};
return (
<Paper sx={{ p: 3, height: '100%' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" fontWeight="600">
Sleep Patterns
</Typography>
<ToggleButtonGroup
value={timeRange}
exclusive
onChange={(e, value) => value && setTimeRange(value)}
size="small"
>
<ToggleButton value="week">Week</ToggleButton>
<ToggleButton value="month">Month</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box sx={{ height: 300 }}>
<Line data={chartData} options={options} />
</Box>
</Paper>
);
};
```
---
## Phase 6: Offline Support & PWA Features (Week 5)
### 6.1 Service Worker Setup
```javascript
// public/service-worker.js
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
import { Queue } from 'workbox-background-sync';
// Precache all static assets
precacheAndRoute(self.__WB_MANIFEST);
// Create sync queue for offline requests
const queue = new Queue('maternal-sync-queue', {
onSync: async ({ queue }) => {
let entry;
while ((entry = await queue.shiftRequest())) {
try {
await fetch(entry.request);
} catch (error) {
await queue.unshiftRequest(entry);
throw error;
}
}
},
});
// API routes - network first with offline queue
registerRoute(
/^https:\/\/api\.maternalapp\.com\/api/,
async (args) => {
try {
const response = await new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 5,
}).handle(args);
return response;
} catch (error) {
await queue.pushRequest({ request: args.request });
return new Response(
JSON.stringify({
offline: true,
message: 'Your request has been queued and will be synced when online'
}),
{ headers: { 'Content-Type': 'application/json' } }
);
}
}
);
// Static assets - cache first
registerRoute(
/\.(?:png|jpg|jpeg|svg|gif|webp)$/,
new CacheFirst({
cacheName: 'image-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
```
### 6.2 Offline State Management
```typescript
// src/store/slices/offlineSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { REHYDRATE } from 'redux-persist';
interface OfflineState {
isOnline: boolean;
pendingActions: PendingAction[];
lastSyncTime: string | null;
syncInProgress: boolean;
}
const offlineSlice = createSlice({
name: 'offline',
initialState: {
isOnline: true,
pendingActions: [],
lastSyncTime: null,
syncInProgress: false,
} as OfflineState,
reducers: {
setOnlineStatus: (state, action: PayloadAction<boolean>) => {
state.isOnline = action.payload;
if (action.payload && state.pendingActions.length > 0) {
state.syncInProgress = true;
}
},
addPendingAction: (state, action: PayloadAction<PendingAction>) => {
state.pendingActions.push({
...action.payload,
id: generateId(),
timestamp: new Date().toISOString(),
});
},
removePendingAction: (state, action: PayloadAction<string>) => {
state.pendingActions = state.pendingActions.filter(a => a.id !== action.payload);
},
setSyncInProgress: (state, action: PayloadAction<boolean>) => {
state.syncInProgress = action.payload;
},
updateLastSyncTime: (state) => {
state.lastSyncTime = new Date().toISOString();
},
},
extraReducers: (builder) => {
builder.addCase(REHYDRATE, (state, action) => {
// Handle rehydration from localStorage
if (action.payload) {
return {
...state,
...action.payload.offline,
isOnline: navigator.onLine,
};
}
});
},
});
export const offlineActions = offlineSlice.actions;
export default offlineSlice.reducer;
```
### 6.3 Offline Indicator Component
```typescript
// src/components/common/OfflineIndicator.tsx
import { Alert, Snackbar, LinearProgress } from '@mui/material';
import { useSelector } from 'react-redux';
import { motion, AnimatePresence } from 'framer-motion';
export const OfflineIndicator = () => {
const { isOnline, pendingActions, syncInProgress } = useSelector(state => state.offline);
return (
<AnimatePresence>
{!isOnline && (
<motion.div
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -100, opacity: 0 }}
>
<Alert
severity="warning"
sx={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
borderRadius: 0,
zIndex: 9999,
}}
>
You're offline. {pendingActions.length} actions will sync when you're back online.
</Alert>
</motion.div>
)}
{syncInProgress && (
<LinearProgress
sx={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 9998,
}}
/>
)}
</AnimatePresence>
);
};
```
---
## Phase 7: Performance Optimization (Week 6)
### 7.1 Code Splitting & Lazy Loading
```typescript
// src/app/(dashboard)/layout.tsx
import { lazy, Suspense } from 'react';
import { LoadingFallback } from '@/components/common/LoadingFallback';
// Lazy load heavy components
const AIChatInterface = lazy(() => import('@/components/features/ai-chat/AIChatInterface'));
const AnalyticsDashboard = lazy(() => import('@/components/features/analytics/AnalyticsDashboard'));
export default function DashboardLayout({ children }) {
return (
<Suspense fallback={<LoadingFallback />}>
{children}
</Suspense>
);
}
```
### 7.2 Image Optimization
```typescript
// src/components/common/OptimizedImage.tsx
import Image from 'next/image';
import { useState } from 'react';
import { Skeleton } from '@mui/material';
export const OptimizedImage = ({ src, alt, ...props }) => {
const [isLoading, setIsLoading] = useState(true);
return (
<Box position="relative">
{isLoading && (
<Skeleton
variant="rectangular"
width={props.width}
height={props.height}
sx={{ position: 'absolute', top: 0, left: 0 }}
/>
)}
<Image
src={src}
alt={alt}
{...props}
onLoadingComplete={() => setIsLoading(false)}
placeholder="blur"
blurDataURL={generateBlurDataURL()}
/>
</Box>
);
};
```
### 7.3 Performance Monitoring
```typescript
// src/lib/performance/monitoring.ts
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
export const initPerformanceMonitoring = () => {
// Send metrics to analytics
const sendToAnalytics = (metric: any) => {
// Send to your analytics service
if (window.gtag) {
window.gtag('event', metric.name, {
value: Math.round(metric.value),
metric_id: metric.id,
metric_value: metric.value,
metric_delta: metric.delta,
});
}
};
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
};
```
---
## Phase 8: Testing & Deployment (Week 6-7)
### 8.1 Testing Setup
```typescript
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.tsx',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
```
### 8.2 E2E Testing with Playwright
```typescript
// tests/e2e/tracking.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Activity Tracking Flow', () => {
test('should track feeding activity', async ({ page }) => {
// Login
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Navigate to tracking
await page.waitForSelector('[data-testid="quick-action-fab"]');
await page.click('[data-testid="quick-action-fab"]');
await page.click('[data-testid="action-feeding"]');
// Fill form
await page.click('[value="bottle"]');
await page.fill('[name="amount"]', '120');
await page.click('[data-testid="save-feeding"]');
// Verify
await expect(page.locator('[data-testid="success-toast"]')).toBeVisible();
});
});
```
### 8.3 Deployment Configuration
```yaml
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm test
- run: npm run test:e2e
build-and-deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run build
# Deploy to Vercel
- uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
working-directory: ./
```
---
## Migration Path to Mobile
### Component Compatibility Strategy
```typescript
// Shared component interface example
// src/components/shared/Button.tsx
interface ButtonProps {
variant?: 'contained' | 'outlined' | 'text';
color?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
onPress?: () => void; // Mobile
onClick?: () => void; // Web
children: React.ReactNode;
}
// Web implementation
export const Button: React.FC<ButtonProps> = ({
onClick,
onPress,
children,
...props
}) => {
return (
<MuiButton onClick={onClick || onPress} {...props}>
{children}
</MuiButton>
);
};
// Future React Native implementation
// export const Button: React.FC<ButtonProps> = ({
// onClick,
// onPress,
// children,
// ...props
// }) => {
// return (
// <TouchableOpacity onPress={onPress || onClick}>
// <Text>{children}</Text>
// </TouchableOpacity>
// );
// };
```
### Shared Business Logic
```typescript
// src/lib/shared/tracking.logic.ts
// This file can be shared between web and mobile
export const calculateFeedingDuration = (start: Date, end: Date): number => {
return Math.round((end.getTime() - start.getTime()) / 60000);
};
export const validateFeedingData = (data: FeedingData): ValidationResult => {
const errors = [];
if (!data.type) {
errors.push('Feeding type is required');
}
if (data.type === 'bottle' && !data.amount) {
errors.push('Amount is required for bottle feeding');
}
return {
isValid: errors.length === 0,
errors,
};
};
export const formatActivityForDisplay = (activity: Activity): DisplayActivity => {
// Shared formatting logic
return {
...activity,
displayTime: formatRelativeTime(activity.timestamp),
icon: getActivityIcon(activity.type),
color: getActivityColor(activity.type),
};
};
```
---
## Performance Targets
### Web Vitals Goals
- **LCP** (Largest Contentful Paint): < 2.5s
- **FID** (First Input Delay): < 100ms
- **CLS** (Cumulative Layout Shift): < 0.1
- **TTI** (Time to Interactive): < 3.5s
- **Bundle Size**: < 200KB initial JS
### Mobile Experience Metrics
- Touch target size: minimum 44x44px
- Font size: minimum 16px for body text
- Scroll performance: 60fps
- Offline capability: Full CRUD operations
- Data usage: < 1MB per session average
---
## Security Considerations
### Authentication & Authorization
```typescript
// src/lib/auth/security.ts
export const securityHeaders = {
'Content-Security-Policy': `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.maternalapp.com wss://api.maternalapp.com;
`,
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(self), geolocation=()',
};
```
### Data Protection
- Implement field-level encryption for sensitive data
- Use secure HttpOnly cookies for auth tokens
- Sanitize all user inputs
- Implement rate limiting on all API calls
- Regular security audits with OWASP guidelines
---
## Monitoring & Analytics
### Analytics Implementation
```typescript
// src/lib/analytics/index.ts
export const trackEvent = (eventName: string, properties?: any) => {
// PostHog
if (window.posthog) {
window.posthog.capture(eventName, properties);
}
// Google Analytics
if (window.gtag) {
window.gtag('event', eventName, properties);
}
};
// Usage
trackEvent('feeding_tracked', {
type: 'bottle',
amount: 120,
duration: 15,
});
```
### Error Tracking
```typescript
// src/lib/monitoring/sentry.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1,
beforeSend(event, hint) {
// Filter sensitive data
if (event.request) {
delete event.request.cookies;
}
return event;
},
});
```
---
## Launch Checklist
### Pre-Launch
- [ ] Cross-browser testing (Chrome, Safari, Firefox, Edge)
- [ ] Mobile device testing (iOS Safari, Chrome Android)
- [ ] Accessibility audit (WCAG AA compliance)
- [ ] Performance audit (Lighthouse score > 90)
- [ ] Security audit
- [ ] SEO optimization
- [ ] Analytics implementation verified
- [ ] Error tracking configured
- [ ] SSL certificate installed
- [ ] CDN configured
- [ ] Backup system tested
### Post-Launch
- [ ] Monitor error rates
- [ ] Track Core Web Vitals
- [ ] Gather user feedback
- [ ] A/B testing setup
- [ ] Performance monitoring dashboard
- [ ] User behavior analytics
- [ ] Conversion funnel tracking
- [ ] Support system ready
---
## Conclusion
This web-first approach provides a solid foundation for the Maternal Organization App while maintaining the flexibility to expand to native mobile platforms. The progressive web app capabilities ensure users get a native-like experience on mobile devices, while the component architecture and shared business logic will facilitate the eventual migration to React Native.
Key advantages of this approach:
1. **Faster time to market** - Single codebase to maintain initially
2. **Broader reach** - Works on any device with a modern browser
3. **Easy updates** - No app store approval process for web updates
4. **Cost-effective** - Reduced development and maintenance costs
5. **SEO benefits** - Web app can be indexed by search engines
6. **Progressive enhancement** - Can add native features gradually
The architecture is designed to support the future mobile app development by keeping business logic separate and using compatible patterns wherever possible.