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:
andupetcu
2025-10-01 09:40:21 +03:00
parent 688f9bd57a
commit 0a2e28b5ee
16 changed files with 1725 additions and 1032 deletions

View File

@@ -0,0 +1,182 @@
import { Box, Skeleton, Paper, Container } from '@mui/material';
interface LoadingFallbackProps {
variant?: 'page' | 'card' | 'list' | 'chart' | 'chat';
}
export const LoadingFallback: React.FC<LoadingFallbackProps> = ({ variant = 'page' }) => {
if (variant === 'chat') {
return (
<Box
sx={{
height: 'calc(100vh - 200px)',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(135deg, #FFF5F5 0%, #FFE4E1 100%)',
borderRadius: 2,
overflow: 'hidden',
}}
>
{/* Header Skeleton */}
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: 0,
background: 'rgba(255, 255, 255, 0.95)',
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Skeleton variant="circular" width={40} height={40} />
<Box sx={{ flex: 1 }}>
<Skeleton variant="text" width={200} height={28} />
<Skeleton variant="text" width={300} height={20} />
</Box>
</Box>
</Paper>
{/* Messages Skeleton */}
<Box sx={{ flex: 1, overflowY: 'auto', p: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Suggested questions */}
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', justifyContent: 'center' }}>
<Skeleton variant="rounded" width={180} height={32} sx={{ borderRadius: 3 }} />
<Skeleton variant="rounded" width={200} height={32} sx={{ borderRadius: 3 }} />
<Skeleton variant="rounded" width={160} height={32} sx={{ borderRadius: 3 }} />
<Skeleton variant="rounded" width={190} height={32} sx={{ borderRadius: 3 }} />
</Box>
</Box>
</Box>
{/* Input Skeleton */}
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: 0,
background: 'rgba(255, 255, 255, 0.95)',
borderTop: '1px solid',
borderColor: 'divider',
}}
>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
<Skeleton variant="rounded" sx={{ flex: 1, height: 48, borderRadius: 3 }} />
<Skeleton variant="circular" width={48} height={48} />
</Box>
</Paper>
</Box>
);
}
if (variant === 'chart') {
return (
<Paper sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Skeleton variant="text" width={180} height={32} />
<Skeleton variant="rounded" width={120} height={36} />
</Box>
<Skeleton variant="rectangular" width="100%" height={250} sx={{ borderRadius: 2 }} />
</Paper>
);
}
if (variant === 'list') {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{[1, 2, 3, 4, 5].map((i) => (
<Paper key={i} sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Skeleton variant="circular" width={48} height={48} />
<Box sx={{ flex: 1 }}>
<Skeleton variant="text" width="60%" height={24} />
<Skeleton variant="text" width="40%" height={20} />
</Box>
<Skeleton variant="rounded" width={80} height={32} />
</Box>
</Paper>
))}
</Box>
);
}
if (variant === 'card') {
return (
<Paper sx={{ p: 3, borderRadius: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Skeleton variant="circular" width={40} height={40} sx={{ mr: 2 }} />
<Skeleton variant="text" width={150} height={32} />
</Box>
<Skeleton variant="text" width="100%" height={24} />
<Skeleton variant="text" width="90%" height={24} />
<Skeleton variant="text" width="70%" height={24} />
<Box sx={{ mt: 3, display: 'flex', gap: 1 }}>
<Skeleton variant="rounded" width={100} height={36} />
<Skeleton variant="rounded" width={100} height={36} />
</Box>
</Paper>
);
}
// Default: full page skeleton
return (
<Container maxWidth="lg" sx={{ py: 3 }}>
<Box sx={{ mb: 4 }}>
<Skeleton variant="text" width={300} height={48} />
<Skeleton variant="text" width={500} height={24} sx={{ mt: 1 }} />
</Box>
{/* Filter section */}
<Paper sx={{ p: 3, mb: 3 }}>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<Skeleton variant="rounded" width={200} height={56} />
<Skeleton variant="rounded" width={300} height={56} />
</Box>
</Paper>
{/* Stats cards */}
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr', md: '1fr 1fr 1fr 1fr' }, gap: 3, mb: 3 }}>
{[1, 2, 3, 4].map((i) => (
<Paper key={i} sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Skeleton variant="circular" width={32} height={32} sx={{ mr: 1 }} />
<Skeleton variant="text" width={100} height={28} />
</Box>
<Skeleton variant="text" width={80} height={48} />
<Skeleton variant="text" width={120} height={20} />
</Paper>
))}
</Box>
{/* Charts */}
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3, mb: 3 }}>
{[1, 2].map((i) => (
<Paper key={i} sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Skeleton variant="circular" width={24} height={24} sx={{ mr: 1 }} />
<Skeleton variant="text" width={150} height={28} />
</Box>
<Skeleton variant="rectangular" width="100%" height={250} sx={{ borderRadius: 1 }} />
</Paper>
))}
</Box>
{/* Activity list */}
<Paper sx={{ p: 3 }}>
<Skeleton variant="text" width={200} height={32} sx={{ mb: 2 }} />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{[1, 2, 3].map((i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 2, borderBottom: '1px solid', borderColor: 'divider', pb: 2 }}>
<Skeleton variant="circular" width={40} height={40} />
<Box sx={{ flex: 1 }}>
<Skeleton variant="text" width="40%" height={24} />
<Skeleton variant="text" width="60%" height={20} />
</Box>
</Box>
))}
</Box>
</Paper>
</Container>
);
};

View File

@@ -0,0 +1,77 @@
import { useState } from 'react';
import Image, { ImageProps } from 'next/image';
import { Box, Skeleton } from '@mui/material';
interface OptimizedImageProps extends Omit<ImageProps, 'onLoadingComplete'> {
onLoadComplete?: () => void;
}
/**
* OptimizedImage Component
*
* Wraps Next.js Image component with:
* - Loading states with MUI Skeleton
* - Blur placeholder support
* - Loading completion events
* - Automatic optimization
*/
export const OptimizedImage: React.FC<OptimizedImageProps> = ({
src,
alt,
width,
height,
onLoadComplete,
style,
...props
}) => {
const [isLoading, setIsLoading] = useState(true);
const handleLoadingComplete = () => {
setIsLoading(false);
onLoadComplete?.();
};
// Generate a simple blur data URL for placeholder
const blurDataURL = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2YwZjBmMCIvPjwvc3ZnPg==';
return (
<Box
sx={{
position: 'relative',
width: typeof width === 'number' ? `${width}px` : width,
height: typeof height === 'number' ? `${height}px` : height,
...style,
}}
>
{isLoading && (
<Skeleton
variant="rectangular"
width={width}
height={height}
sx={{
position: 'absolute',
top: 0,
left: 0,
borderRadius: 'inherit',
}}
animation="wave"
/>
)}
<Image
src={src}
alt={alt}
width={width}
height={height}
onLoadingComplete={handleLoadingComplete}
placeholder="blur"
blurDataURL={blurDataURL}
style={{
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
...style,
}}
{...props}
/>
</Box>
);
};

View File

@@ -0,0 +1,20 @@
'use client';
import { useEffect } from 'react';
import { initPerformanceMonitoring } from '@/lib/performance/monitoring';
/**
* PerformanceMonitor Component
*
* Client-side component that initializes web vitals monitoring
* Should be included once in the root layout
*/
export const PerformanceMonitor: React.FC = () => {
useEffect(() => {
// Initialize performance monitoring on client side
initPerformanceMonitoring();
}, []);
// This component doesn't render anything
return null;
};