# 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 ); } ``` ### 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)} )} {/* 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 && ( )} {alt} 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.