Added Redux Provider to app layout and simplified Redux store to work properly with Next.js SSR. **Changes:** - Added ReduxProvider wrapper to root layout (app/layout.tsx) - Fixed ReduxProvider TypeScript type (React.ReactNode) - Simplified store configuration by removing @redux-offline package - Removed packages incompatible with SSR: - @redux-offline/redux-offline - redux-persist - localforage - Re-added NetworkStatusIndicator to main page (now works with Redux) - Kept custom offline middleware and sync middleware for offline-first functionality **Why:** The @redux-offline package and localforage try to access browser APIs (IndexedDB, localStorage) during SSR, causing "No available storage method found" errors. Our custom offline middleware provides the same functionality without SSR issues. **Result:** Redux store now works correctly with: - Network status detection - Offline action queuing - Custom sync middleware - Activities and children slices with optimistic updates Next step: Can add redux-persist back with proper SSR guards if needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
225 lines
8.3 KiB
TypeScript
225 lines
8.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Box, Typography, Button, Paper, Grid, CircularProgress } from '@mui/material';
|
|
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
|
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
|
import { EmailVerificationBanner } from '@/components/common/EmailVerificationBanner';
|
|
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
|
|
import { DataErrorFallback } from '@/components/common/ErrorFallbacks';
|
|
import { NetworkStatusIndicator } from '@/components/common/NetworkStatusIndicator';
|
|
import {
|
|
Restaurant,
|
|
Hotel,
|
|
BabyChangingStation,
|
|
Insights,
|
|
SmartToy,
|
|
Analytics,
|
|
} from '@mui/icons-material';
|
|
import { motion } from 'framer-motion';
|
|
import { useAuth } from '@/lib/auth/AuthContext';
|
|
import { useRouter } from 'next/navigation';
|
|
import { trackingApi, DailySummary } from '@/lib/api/tracking';
|
|
import { childrenApi, Child } from '@/lib/api/children';
|
|
import { format } from 'date-fns';
|
|
|
|
export default function HomePage() {
|
|
const { user } = useAuth();
|
|
const router = useRouter();
|
|
const [children, setChildren] = useState<Child[]>([]);
|
|
const [selectedChild, setSelectedChild] = useState<Child | null>(null);
|
|
const [dailySummary, setDailySummary] = useState<DailySummary | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const familyId = user?.families?.[0]?.familyId;
|
|
// Load children and daily summary
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
if (!familyId) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Load children
|
|
const childrenData = await childrenApi.getChildren(familyId);
|
|
setChildren(childrenData);
|
|
|
|
if (childrenData.length > 0) {
|
|
const firstChild = childrenData[0];
|
|
setSelectedChild(firstChild);
|
|
|
|
// Load today's summary for first child
|
|
const today = format(new Date(), 'yyyy-MM-dd');
|
|
const summary = await trackingApi.getDailySummary(firstChild.id, today);
|
|
setDailySummary(summary);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load data:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadData();
|
|
}, [familyId]);
|
|
|
|
const quickActions = [
|
|
{ icon: <Restaurant />, label: 'Feeding', color: '#FFB6C1', path: '/track/feeding' },
|
|
{ icon: <Hotel />, label: 'Sleep', color: '#B6D7FF', path: '/track/sleep' },
|
|
{ icon: <BabyChangingStation />, label: 'Diaper', color: '#FFE4B5', path: '/track/diaper' },
|
|
{ icon: <SmartToy />, label: 'AI Assistant', color: '#FFD3B6', path: '/ai-assistant' },
|
|
{ icon: <Analytics />, label: 'Analytics', color: '#D4B5FF', path: '/analytics' },
|
|
];
|
|
|
|
const formatSleepHours = (minutes: number) => {
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
if (hours > 0 && mins > 0) {
|
|
return `${hours}h ${mins}m`;
|
|
} else if (hours > 0) {
|
|
return `${hours}h`;
|
|
} else {
|
|
return `${mins}m`;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ProtectedRoute>
|
|
<AppShell>
|
|
<NetworkStatusIndicator />
|
|
<Box>
|
|
<EmailVerificationBanner />
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<Typography variant="h4" gutterBottom fontWeight="600" sx={{ mb: 1 }}>
|
|
Welcome Back{user?.name ? `, ${user.name}` : ''}! 👋
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
|
Track your child's activities and get AI-powered insights
|
|
</Typography>
|
|
|
|
{/* Quick Actions */}
|
|
<Typography variant="h6" gutterBottom fontWeight="600" sx={{ mb: 2 }}>
|
|
Quick Actions
|
|
</Typography>
|
|
<Grid container spacing={2} sx={{ mb: 4 }}>
|
|
{quickActions.map((action, index) => (
|
|
<Grid item xs={6} sm={2.4} key={action.label}>
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.3, delay: index * 0.1 }}
|
|
>
|
|
<Paper
|
|
onClick={() => router.push(action.path)}
|
|
sx={{
|
|
p: 3,
|
|
textAlign: 'center',
|
|
cursor: 'pointer',
|
|
bgcolor: action.color,
|
|
color: 'white',
|
|
transition: 'transform 0.2s',
|
|
'&:hover': {
|
|
transform: 'scale(1.05)',
|
|
},
|
|
}}
|
|
>
|
|
<Box sx={{ fontSize: 48, mb: 1 }}>{action.icon}</Box>
|
|
<Typography variant="body1" fontWeight="600">
|
|
{action.label}
|
|
</Typography>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
|
|
{/* Today's Summary */}
|
|
<Typography variant="h6" gutterBottom fontWeight="600" sx={{ mb: 2 }}>
|
|
Today's Summary{selectedChild ? ` - ${selectedChild.name}` : ''}
|
|
</Typography>
|
|
<ErrorBoundary
|
|
isolate
|
|
fallback={<DataErrorFallback error={new Error('Failed to load summary')} />}
|
|
>
|
|
<Paper sx={{ p: 3 }}>
|
|
{loading ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
) : !dailySummary ? (
|
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{children.length === 0
|
|
? 'Add a child to start tracking'
|
|
: 'No activities tracked today'}
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={4}>
|
|
<Box textAlign="center">
|
|
<Restaurant sx={{ fontSize: 32, color: 'primary.main', mb: 1 }} />
|
|
<Typography variant="h5" fontWeight="600">
|
|
{dailySummary.feedingCount || 0}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Feedings
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid item xs={4}>
|
|
<Box textAlign="center">
|
|
<Hotel sx={{ fontSize: 32, color: 'info.main', mb: 1 }} />
|
|
<Typography variant="h5" fontWeight="600">
|
|
{dailySummary.sleepTotalMinutes
|
|
? formatSleepHours(dailySummary.sleepTotalMinutes)
|
|
: '0m'}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Sleep
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid item xs={4}>
|
|
<Box textAlign="center">
|
|
<BabyChangingStation sx={{ fontSize: 32, color: 'warning.main', mb: 1 }} />
|
|
<Typography variant="h5" fontWeight="600">
|
|
{dailySummary.diaperCount || 0}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Diapers
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
</Grid>
|
|
)}
|
|
</Paper>
|
|
</ErrorBoundary>
|
|
|
|
{/* Next Predicted Activity */}
|
|
<Box sx={{ mt: 4 }}>
|
|
<Paper sx={{ p: 3, bgcolor: 'primary.light' }}>
|
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
Next Predicted Activity
|
|
</Typography>
|
|
<Typography variant="h6" fontWeight="600" gutterBottom>
|
|
Nap time in 45 minutes
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Based on your child's sleep patterns
|
|
</Typography>
|
|
</Paper>
|
|
</Box>
|
|
</motion.div>
|
|
</Box>
|
|
</AppShell>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|