# 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 (
{!isMobile && }
{children}
{isMobile && }
);
};
```
---
## 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;
register: (data: RegisterData) => Promise;
logout: () => void;
isLoading: boolean;
}
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState(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 (
Welcome Back
setShowPassword(!showPassword)}>
{showPassword ? : }
),
}}
/>
OR
}
size="large"
sx={{ mb: 2 }}
>
Continue with Google
}
size="large"
>
Continue with Apple
);
}
```
### 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 (
{/* Mobile-optimized stepper */}
{steps.map((label) => (
{label}
))}
{activeStep === 0 && }
{activeStep === 1 && }
{activeStep === 2 && }
{activeStep === 3 && }
);
}
```
---
## 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: , name: 'Feeding', color: '#FFB6C1', route: '/track/feeding' },
{ icon: , name: 'Sleep', color: '#B6D7FF', route: '/track/sleep' },
{ icon: , name: 'Diaper', color: '#FFE4B5', route: '/track/diaper' },
{ icon: , name: 'Voice', color: '#E6E6FA', action: 'voice' },
];
return (
} />}
onClose={() => setOpen(false)}
onOpen={() => setOpen(true)}
open={open}
>
{actions.map((action) => (
handleAction(action)}
sx={{
bgcolor: action.color,
'&:hover': {
bgcolor: action.color,
transform: 'scale(1.1)',
}
}}
/>
))}
);
};
```
### 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(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 (
Track Feeding
{isListening ? : }
value && setFeedingType(value)}
fullWidth
sx={{ mb: 3 }}
>
Breastfeeding
Bottle
Solid Food
{/* Timer component for breastfeeding */}
{feedingType === 'breast' && (
{formatDuration(duration)}
}
onClick={toggleTimer}
sx={{ mt: 2 }}
>
{isTimerRunning ? 'Stop Timer' : 'Start Timer'}
)}
{/* Form fields for bottle/solid */}
{feedingType !== 'breast' && (
{/* Additional form fields */}
)}
);
};
```
---
## 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);
const { messages, sendMessage, isLoading } = useAIChat();
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
return (
{/* Quick action chips */}
{['Sleep tips', 'Feeding schedule', 'Developmental milestones', 'Emergency'].map((action) => (
handleQuickAction(action)}
sx={{
bgcolor: 'background.paper',
'&:hover': { bgcolor: 'primary.light' }
}}
/>
))}
{/* Messages */}
{messages.map((message, index) => (
{message.role === 'assistant' && (
AI
)}
{message.content}
{message.disclaimer && (
⚠️ {message.disclaimer}
)}
{message.role === 'user' && (
U
)}
))}
{isTyping && (
AI
)}
{/* Input area */}
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();
}
}}
/>
);
};
```
### 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) => void;
getRelevantContext: (query: string) => Promise;
}
export const AIContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [childContext, setChildContext] = useState([]);
const [recentActivities, setRecentActivities] = useState([]);
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 (
{children}
);
};
```
---
## 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 (
{/* Child selector carousel for mobile */}
{isMobile && (
{children.map((child) => (
))}
)}
{/* Desktop child cards */}
{!isMobile && children.map((child) => (
))}
{/* Sleep prediction */}
{/* Quick stats */}
{/* Activity timeline */}
);
}
```
### 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 (
Sleep Patterns
value && setTimeRange(value)}
size="small"
>
Week
Month
);
};
```
---
## 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) => {
state.isOnline = action.payload;
if (action.payload && state.pendingActions.length > 0) {
state.syncInProgress = true;
}
},
addPendingAction: (state, action: PayloadAction) => {
state.pendingActions.push({
...action.payload,
id: generateId(),
timestamp: new Date().toISOString(),
});
},
removePendingAction: (state, action: PayloadAction) => {
state.pendingActions = state.pendingActions.filter(a => a.id !== action.payload);
},
setSyncInProgress: (state, action: PayloadAction) => {
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 (
{!isOnline && (
You're offline. {pendingActions.length} actions will sync when you're back online.
)}
{syncInProgress && (
)}
);
};
```
---
## 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 (
}>
{children}
);
}
```
### 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 (
{isLoading && (
)}
setIsLoading(false)}
placeholder="blur"
blurDataURL={generateBlurDataURL()}
/>
);
};
```
### 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: ['/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '/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 = ({
onClick,
onPress,
children,
...props
}) => {
return (
{children}
);
};
// Future React Native implementation
// export const Button: React.FC = ({
// onClick,
// onPress,
// children,
// ...props
// }) => {
// return (
//
// {children}
//
// );
// };
```
### 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.