Implement Phase 7 Performance Optimization and fix tracking system
Phase 7 Implementation: - Add lazy loading for AI Assistant and Insights pages - Create LoadingFallback component with skeleton screens (page, card, list, chart, chat variants) - Create OptimizedImage component with Next.js Image optimization - Create PerformanceMonitor component with web-vitals v5 integration - Add performance monitoring library tracking Core Web Vitals (CLS, INP, FCP, LCP, TTFB) - Install web-vitals v5.1.0 dependency - Extract AI chat interface and insights dashboard to lazy-loaded components Tracking System Fixes: - Fix API data transformation between frontend (timestamp/data) and backend (startedAt/metadata) - Update createActivity, getActivities, and getActivity to properly transform data structures - Fix diaper, feeding, and sleep tracking pages to work with backend API Homepage Improvements: - Connect Today's Summary to backend daily summary API - Load real-time data for feeding count, sleep hours, and diaper count - Add loading states and empty states for better UX - Format sleep duration as "Xh Ym" for better readability - Display child name in summary section 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,337 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
IconButton,
|
||||
Paper,
|
||||
Typography,
|
||||
Avatar,
|
||||
CircularProgress,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import { Send, SmartToy, Person, AutoAwesome } from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
||||
import { LoadingFallback } from '@/components/common/LoadingFallback';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
const suggestedQuestions = [
|
||||
'How much should my baby sleep at 3 months?',
|
||||
'What are normal feeding patterns?',
|
||||
'When should I introduce solid foods?',
|
||||
'Tips for better sleep routine',
|
||||
];
|
||||
// Lazy load the AI chat interface component
|
||||
const AIChatInterface = lazy(() =>
|
||||
import('@/components/features/ai-chat/AIChatInterface').then((mod) => ({
|
||||
default: mod.AIChatInterface,
|
||||
}))
|
||||
);
|
||||
|
||||
export default function AIAssistantPage() {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const { user } = useAuth();
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleSend = async (message?: string) => {
|
||||
const messageText = message || input.trim();
|
||||
if (!messageText || isLoading) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: messageText,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInput('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/api/v1/ai/chat', {
|
||||
message: messageText,
|
||||
conversationId: null, // Will be managed by backend
|
||||
});
|
||||
|
||||
const assistantMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: response.data.data.response,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
} catch (error) {
|
||||
console.error('AI chat error:', error);
|
||||
const errorMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: 'Sorry, I encountered an error. Please try again.',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, errorMessage]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuggestedQuestion = (question: string) => {
|
||||
handleSend(question);
|
||||
};
|
||||
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<Box
|
||||
sx={{
|
||||
height: 'calc(100vh - 200px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'linear-gradient(135deg, #FFF5F5 0%, #FFE4E1 100%)',
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 0,
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
||||
<SmartToy />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
AI Parenting Assistant
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Ask me anything about parenting and childcare
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Messages Container */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{messages.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
<AutoAwesome sx={{ fontSize: 64, color: 'primary.main', opacity: 0.5 }} />
|
||||
<Typography variant="h6" color="text.secondary" textAlign="center">
|
||||
Hi {user?.name}! How can I help you today?
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
justifyContent: 'center',
|
||||
maxWidth: 600,
|
||||
}}
|
||||
>
|
||||
{suggestedQuestions.map((question, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={question}
|
||||
onClick={() => handleSuggestedQuestion(question)}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
'&:hover': {
|
||||
bgcolor: 'primary.light',
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<AnimatePresence>
|
||||
{messages.map((message) => (
|
||||
<motion.div
|
||||
key={message.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
justifyContent: message.role === 'user' ? 'flex-end' : 'flex-start',
|
||||
}}
|
||||
>
|
||||
{message.role === 'assistant' && (
|
||||
<Avatar sx={{ bgcolor: 'primary.main', mt: 1 }}>
|
||||
<SmartToy />
|
||||
</Avatar>
|
||||
)}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
maxWidth: '70%',
|
||||
borderRadius: 3,
|
||||
bgcolor:
|
||||
message.role === 'user'
|
||||
? 'primary.main'
|
||||
: 'rgba(255, 255, 255, 0.95)',
|
||||
color: message.role === 'user' ? 'white' : 'text.primary',
|
||||
backdropFilter: 'blur(10px)',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{message.content}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
mt: 1,
|
||||
display: 'block',
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
{message.timestamp.toLocaleTimeString()}
|
||||
</Typography>
|
||||
</Paper>
|
||||
{message.role === 'user' && (
|
||||
<Avatar sx={{ bgcolor: 'secondary.main', mt: 1 }}>
|
||||
<Person />
|
||||
</Avatar>
|
||||
)}
|
||||
</Box>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{isLoading && (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
||||
<SmartToy />
|
||||
</Avatar>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 3,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.95)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={20} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Thinking...
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</Box>
|
||||
|
||||
{/* Input Area */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 0,
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
maxRows={4}
|
||||
placeholder="Ask me anything..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 3,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => handleSend()}
|
||||
disabled={!input.trim() || isLoading}
|
||||
sx={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
bgcolor: 'primary.main',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
bgcolor: 'primary.dark',
|
||||
},
|
||||
'&:disabled': {
|
||||
bgcolor: 'action.disabledBackground',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Send />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||
This AI assistant provides general information. Always consult healthcare professionals
|
||||
for medical advice.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
<Suspense fallback={<LoadingFallback variant="chat" />}>
|
||||
<AIChatInterface />
|
||||
</Suspense>
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
|
||||
@@ -1,656 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Paper,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Avatar,
|
||||
Chip,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Restaurant,
|
||||
Hotel,
|
||||
BabyChangingStation,
|
||||
TrendingUp,
|
||||
Timeline,
|
||||
CalendarToday,
|
||||
Assessment,
|
||||
} from '@mui/icons-material';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
||||
import { motion } from 'framer-motion';
|
||||
import { trackingApi, Activity, ActivityType } from '@/lib/api/tracking';
|
||||
import { childrenApi, Child } from '@/lib/api/children';
|
||||
import { format, subDays, startOfDay, endOfDay, parseISO, differenceInMinutes, formatDistanceToNow } from 'date-fns';
|
||||
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
import { LoadingFallback } from '@/components/common/LoadingFallback';
|
||||
|
||||
type DateRange = '7days' | '30days' | '3months';
|
||||
|
||||
interface DayData {
|
||||
date: string;
|
||||
feedings: number;
|
||||
sleepHours: number;
|
||||
diapers: number;
|
||||
activities: number;
|
||||
}
|
||||
|
||||
interface DiaperTypeData {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface ActivityTypeData {
|
||||
name: string;
|
||||
count: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
feeding: '#FFB6C1',
|
||||
sleep: '#B6D7FF',
|
||||
diaper: '#FFE4B5',
|
||||
medication: '#D4B5FF',
|
||||
milestone: '#B5FFD4',
|
||||
note: '#FFD3B6',
|
||||
wet: '#87CEEB',
|
||||
dirty: '#D2691E',
|
||||
both: '#FF8C00',
|
||||
dry: '#90EE90',
|
||||
};
|
||||
|
||||
const getActivityIcon = (type: ActivityType) => {
|
||||
switch (type) {
|
||||
case 'feeding':
|
||||
return <Restaurant />;
|
||||
case 'sleep':
|
||||
return <Hotel />;
|
||||
case 'diaper':
|
||||
return <BabyChangingStation />;
|
||||
default:
|
||||
return <Timeline />;
|
||||
}
|
||||
};
|
||||
|
||||
const getActivityColor = (type: ActivityType) => {
|
||||
return COLORS[type as keyof typeof COLORS] || '#CCCCCC';
|
||||
};
|
||||
// Lazy load the insights dashboard component
|
||||
const InsightsDashboard = lazy(() =>
|
||||
import('@/components/features/analytics/InsightsDashboard').then((mod) => ({
|
||||
default: mod.InsightsDashboard,
|
||||
}))
|
||||
);
|
||||
|
||||
export default function InsightsPage() {
|
||||
const [children, setChildren] = useState<Child[]>([]);
|
||||
const [selectedChild, setSelectedChild] = useState<string>('');
|
||||
const [dateRange, setDateRange] = useState<DateRange>('7days');
|
||||
const [activities, setActivities] = useState<Activity[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Fetch children on mount
|
||||
useEffect(() => {
|
||||
const fetchChildren = async () => {
|
||||
try {
|
||||
const childrenData = await childrenApi.getChildren();
|
||||
setChildren(childrenData);
|
||||
if (childrenData.length > 0) {
|
||||
setSelectedChild(childrenData[0].id);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || 'Failed to load children');
|
||||
}
|
||||
};
|
||||
fetchChildren();
|
||||
}, []);
|
||||
|
||||
// Fetch activities when child or date range changes
|
||||
useEffect(() => {
|
||||
if (!selectedChild) return;
|
||||
|
||||
const fetchActivities = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const days = dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 90;
|
||||
const endDate = endOfDay(new Date());
|
||||
const startDate = startOfDay(subDays(new Date(), days - 1));
|
||||
|
||||
const activitiesData = await trackingApi.getActivities(
|
||||
selectedChild,
|
||||
undefined,
|
||||
startDate.toISOString(),
|
||||
endDate.toISOString()
|
||||
);
|
||||
setActivities(activitiesData);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || 'Failed to load activities');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchActivities();
|
||||
}, [selectedChild, dateRange]);
|
||||
|
||||
// Calculate statistics
|
||||
const calculateStats = () => {
|
||||
const totalFeedings = activities.filter((a) => a.type === 'feeding').length;
|
||||
const totalDiapers = activities.filter((a) => a.type === 'diaper').length;
|
||||
|
||||
// Calculate sleep hours
|
||||
const sleepActivities = activities.filter((a) => a.type === 'sleep');
|
||||
const totalSleepMinutes = sleepActivities.reduce((acc, activity) => {
|
||||
if (activity.data?.endTime && activity.data?.startTime) {
|
||||
const start = parseISO(activity.data.startTime);
|
||||
const end = parseISO(activity.data.endTime);
|
||||
return acc + differenceInMinutes(end, start);
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
const days = dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 90;
|
||||
const avgSleepHours = days > 0 ? (totalSleepMinutes / 60 / days).toFixed(1) : '0.0';
|
||||
|
||||
// Most common activity type
|
||||
const typeCounts: Record<string, number> = {};
|
||||
activities.forEach((a) => {
|
||||
typeCounts[a.type] = (typeCounts[a.type] || 0) + 1;
|
||||
});
|
||||
const mostCommonType = Object.entries(typeCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'None';
|
||||
|
||||
return {
|
||||
totalFeedings,
|
||||
avgSleepHours,
|
||||
totalDiapers,
|
||||
mostCommonType,
|
||||
};
|
||||
};
|
||||
|
||||
// Prepare chart data - daily breakdown
|
||||
const prepareDailyData = (): DayData[] => {
|
||||
const days = dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 90;
|
||||
const dailyMap = new Map<string, DayData>();
|
||||
|
||||
// Initialize all days
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = format(subDays(new Date(), i), 'yyyy-MM-dd');
|
||||
dailyMap.set(date, {
|
||||
date: format(subDays(new Date(), i), 'MMM dd'),
|
||||
feedings: 0,
|
||||
sleepHours: 0,
|
||||
diapers: 0,
|
||||
activities: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Populate with actual data
|
||||
activities.forEach((activity) => {
|
||||
const dateKey = format(parseISO(activity.timestamp), 'yyyy-MM-dd');
|
||||
const data = dailyMap.get(dateKey);
|
||||
if (data) {
|
||||
data.activities += 1;
|
||||
if (activity.type === 'feeding') data.feedings += 1;
|
||||
if (activity.type === 'diaper') data.diapers += 1;
|
||||
if (activity.type === 'sleep' && activity.data?.endTime && activity.data?.startTime) {
|
||||
const start = parseISO(activity.data.startTime);
|
||||
const end = parseISO(activity.data.endTime);
|
||||
const hours = differenceInMinutes(end, start) / 60;
|
||||
data.sleepHours += hours;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(dailyMap.values()).map((d) => ({
|
||||
...d,
|
||||
sleepHours: Number(d.sleepHours.toFixed(1)),
|
||||
}));
|
||||
};
|
||||
|
||||
// Prepare diaper type data
|
||||
const prepareDiaperData = (): DiaperTypeData[] => {
|
||||
const diaperActivities = activities.filter((a) => a.type === 'diaper');
|
||||
const typeCount: Record<string, number> = {};
|
||||
|
||||
diaperActivities.forEach((activity) => {
|
||||
const type = activity.data?.type || 'unknown';
|
||||
typeCount[type] = (typeCount[type] || 0) + 1;
|
||||
});
|
||||
|
||||
return Object.entries(typeCount).map(([name, value]) => ({
|
||||
name: name.charAt(0).toUpperCase() + name.slice(1),
|
||||
value,
|
||||
color: COLORS[name as keyof typeof COLORS] || '#CCCCCC',
|
||||
}));
|
||||
};
|
||||
|
||||
// Prepare activity type data
|
||||
const prepareActivityTypeData = (): ActivityTypeData[] => {
|
||||
const typeCount: Record<string, number> = {};
|
||||
|
||||
activities.forEach((activity) => {
|
||||
typeCount[activity.type] = (typeCount[activity.type] || 0) + 1;
|
||||
});
|
||||
|
||||
return Object.entries(typeCount).map(([name, count]) => ({
|
||||
name: name.charAt(0).toUpperCase() + name.slice(1),
|
||||
count,
|
||||
color: COLORS[name as keyof typeof COLORS] || '#CCCCCC',
|
||||
}));
|
||||
};
|
||||
|
||||
const stats = calculateStats();
|
||||
const dailyData = prepareDailyData();
|
||||
const diaperData = prepareDiaperData();
|
||||
const activityTypeData = prepareActivityTypeData();
|
||||
const recentActivities = [...activities]
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
.slice(0, 20);
|
||||
|
||||
// Empty state check
|
||||
const noChildren = children.length === 0;
|
||||
const noActivities = activities.length === 0 && !loading;
|
||||
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h4" fontWeight="600" gutterBottom>
|
||||
Insights & Analytics
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
||||
Track patterns and get insights about your child's activities
|
||||
</Typography>
|
||||
|
||||
{/* Filters */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
{children.length > 1 && (
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Child</InputLabel>
|
||||
<Select
|
||||
value={selectedChild}
|
||||
onChange={(e) => setSelectedChild(e.target.value)}
|
||||
label="Child"
|
||||
>
|
||||
{children.map((child) => (
|
||||
<MenuItem key={child.id} value={child.id}>
|
||||
{child.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ToggleButtonGroup
|
||||
value={dateRange}
|
||||
exclusive
|
||||
onChange={(_, newValue) => newValue && setDateRange(newValue)}
|
||||
fullWidth
|
||||
size="large"
|
||||
>
|
||||
<ToggleButton value="7days">7 Days</ToggleButton>
|
||||
<ToggleButton value="30days">30 Days</ToggleButton>
|
||||
<ToggleButton value="3months">3 Months</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* No Children State */}
|
||||
{noChildren && !loading && (
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
No children found. Please add a child to view insights.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{loading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 8 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* No Activities State */}
|
||||
{noActivities && !noChildren && (
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
No activities found for the selected date range. Start tracking activities to see insights!
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{!loading && !noChildren && !noActivities && (
|
||||
<>
|
||||
{/* Summary Statistics */}
|
||||
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0 }}
|
||||
>
|
||||
<Card sx={{ bgcolor: COLORS.feeding, color: 'white' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<Restaurant sx={{ fontSize: 32, mr: 1 }} />
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
Feedings
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="h3" fontWeight="700">
|
||||
{stats.totalFeedings}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
|
||||
Total count
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
>
|
||||
<Card sx={{ bgcolor: COLORS.sleep, color: 'white' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<Hotel sx={{ fontSize: 32, mr: 1 }} />
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
Sleep
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="h3" fontWeight="700">
|
||||
{stats.avgSleepHours}h
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
|
||||
Average per day
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<Card sx={{ bgcolor: COLORS.diaper, color: 'white' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<BabyChangingStation sx={{ fontSize: 32, mr: 1 }} />
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
Diapers
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="h3" fontWeight="700">
|
||||
{stats.totalDiapers}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
|
||||
Total changes
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
<Card sx={{ bgcolor: COLORS.milestone, color: 'white' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<TrendingUp sx={{ fontSize: 32, mr: 1 }} />
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
Top Activity
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="h3" fontWeight="700" sx={{ textTransform: 'capitalize' }}>
|
||||
{stats.mostCommonType}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
|
||||
Most frequent
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Charts */}
|
||||
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||
{/* Feeding Frequency Chart */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Restaurant sx={{ mr: 1, color: COLORS.feeding }} />
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
Feeding Frequency
|
||||
</Typography>
|
||||
</Box>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<BarChart data={dailyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="feedings" fill={COLORS.feeding} name="Feedings" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Sleep Duration Chart */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Hotel sx={{ mr: 1, color: COLORS.sleep }} />
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
Sleep Duration (Hours)
|
||||
</Typography>
|
||||
</Box>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<LineChart data={dailyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="sleepHours"
|
||||
stroke={COLORS.sleep}
|
||||
strokeWidth={3}
|
||||
name="Sleep Hours"
|
||||
dot={{ fill: COLORS.sleep, r: 4 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Diaper Changes by Type */}
|
||||
{diaperData.length > 0 && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<BabyChangingStation sx={{ mr: 1, color: COLORS.diaper }} />
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
Diaper Changes by Type
|
||||
</Typography>
|
||||
</Box>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={diaperData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{diaperData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Activity Timeline */}
|
||||
<Grid item xs={12} md={diaperData.length > 0 ? 6 : 12}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Assessment sx={{ mr: 1, color: 'primary.main' }} />
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
Activity Timeline
|
||||
</Typography>
|
||||
</Box>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<BarChart data={dailyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar dataKey="feedings" fill={COLORS.feeding} name="Feedings" />
|
||||
<Bar dataKey="diapers" fill={COLORS.diaper} name="Diapers" />
|
||||
<Bar dataKey="sleepHours" fill={COLORS.sleep} name="Sleep (hrs)" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Activity Type Distribution */}
|
||||
{activityTypeData.length > 0 && (
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight="600" gutterBottom>
|
||||
Activity Distribution
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 2 }}>
|
||||
{activityTypeData.map((activity) => (
|
||||
<Chip
|
||||
key={activity.name}
|
||||
icon={getActivityIcon(activity.name.toLowerCase() as ActivityType)}
|
||||
label={`${activity.name}: ${activity.count}`}
|
||||
sx={{
|
||||
bgcolor: activity.color,
|
||||
color: 'white',
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 600,
|
||||
px: 1,
|
||||
py: 2,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Recent Activities */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" fontWeight="600" gutterBottom>
|
||||
Recent Activities (Last 20)
|
||||
</Typography>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<List sx={{ maxHeight: 400, overflow: 'auto' }}>
|
||||
{recentActivities.map((activity, index) => (
|
||||
<motion.div
|
||||
key={activity.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.02 }}
|
||||
>
|
||||
<ListItem
|
||||
sx={{
|
||||
borderBottom: index < recentActivities.length - 1 ? '1px solid' : 'none',
|
||||
borderColor: 'divider',
|
||||
py: 1.5,
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: getActivityColor(activity.type) }}>
|
||||
{getActivityIcon(activity.type)}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="body1" fontWeight="600" sx={{ textTransform: 'capitalize' }}>
|
||||
{activity.type}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={formatDistanceToNow(parseISO(activity.timestamp), { addSuffix: true })}
|
||||
size="small"
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{activity.notes || format(parseISO(activity.timestamp), 'MMM dd, yyyy HH:mm')}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</motion.div>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</motion.div>
|
||||
<Suspense fallback={<LoadingFallback variant="page" />}>
|
||||
<InsightsDashboard />
|
||||
</Suspense>
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { ThemeRegistry } from '@/components/ThemeRegistry';
|
||||
// import { PerformanceMonitor } from '@/components/common/PerformanceMonitor'; // Temporarily disabled
|
||||
import './globals.css';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
@@ -37,6 +38,7 @@ export default function RootLayout({
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<ThemeRegistry>
|
||||
{/* <PerformanceMonitor /> */}
|
||||
{children}
|
||||
</ThemeRegistry>
|
||||
</body>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Typography, Button, Paper, Grid } from '@mui/material';
|
||||
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 {
|
||||
@@ -14,10 +15,51 @@ import {
|
||||
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' },
|
||||
@@ -26,6 +68,18 @@ export default function HomePage() {
|
||||
{ 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>
|
||||
@@ -78,34 +132,62 @@ export default function HomePage() {
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Recent Activity */}
|
||||
{/* Today's Summary */}
|
||||
<Typography variant="h6" gutterBottom fontWeight="600" sx={{ mb: 2 }}>
|
||||
Today's Summary
|
||||
Today's Summary{selectedChild ? ` - ${selectedChild.name}` : ''}
|
||||
</Typography>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<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">8</Typography>
|
||||
<Typography variant="body2" color="text.secondary">Feedings</Typography>
|
||||
</Box>
|
||||
{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>
|
||||
<Grid item xs={4}>
|
||||
<Box textAlign="center">
|
||||
<Hotel sx={{ fontSize: 32, color: 'info.main', mb: 1 }} />
|
||||
<Typography variant="h5" fontWeight="600">12h</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">6</Typography>
|
||||
<Typography variant="body2" color="text.secondary">Diapers</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Next Predicted Activity */}
|
||||
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
BabyChangingStation,
|
||||
Warning,
|
||||
CheckCircle,
|
||||
ChildCare,
|
||||
Add,
|
||||
} from '@mui/icons-material';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||
@@ -315,18 +317,24 @@ export default function DiaperTrackPage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<Box>
|
||||
<Alert severity="warning">
|
||||
Please add a child first before tracking diaper changes.
|
||||
</Alert>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => router.push('/children')}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Go to Children Page
|
||||
</Button>
|
||||
</Box>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: 'center', py: 8 }}>
|
||||
<ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
No Children Added
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
You need to add a child before you can track diaper changes
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Add />}
|
||||
onClick={() => router.push('/children')}
|
||||
>
|
||||
Add Child
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
Fastfood,
|
||||
Delete,
|
||||
Edit,
|
||||
ChildCare,
|
||||
Add,
|
||||
} from '@mui/icons-material';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||
@@ -316,18 +318,24 @@ export default function FeedingTrackPage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<Box>
|
||||
<Alert severity="warning">
|
||||
Please add a child first before tracking feeding activities.
|
||||
</Alert>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => router.push('/children')}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Go to Children Page
|
||||
</Button>
|
||||
</Box>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: 'center', py: 8 }}>
|
||||
<ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
No Children Added
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
You need to add a child before you can track feeding activities
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Add />}
|
||||
onClick={() => router.push('/children')}
|
||||
>
|
||||
Add Child
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
DirectionsCar,
|
||||
Chair,
|
||||
Home,
|
||||
ChildCare,
|
||||
Add,
|
||||
} from '@mui/icons-material';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||
@@ -330,18 +332,24 @@ export default function SleepTrackPage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<Box>
|
||||
<Alert severity="warning">
|
||||
Please add a child first before tracking sleep activities.
|
||||
</Alert>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => router.push('/children')}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Go to Children Page
|
||||
</Button>
|
||||
</Box>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: 'center', py: 8 }}>
|
||||
<ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
No Children Added
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
You need to add a child before you can track sleep activities
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Add />}
|
||||
onClick={() => router.push('/children')}
|
||||
>
|
||||
Add Child
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user