Files
maternal-app/docs/maternal-web-frontend-plan.md
2025-10-01 19:01:52 +00:00

44 KiB

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

// Package versions for consistency
{
  "next": "^14.2.0",
  "react": "^18.3.0",
  "react-dom": "^18.3.0",
  "typescript": "^5.4.0"
}

UI Framework & Styling

{
  // 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

{
  // 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

{
  "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

# 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

# .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

// 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

// 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

// 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

// 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

// 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.