Files
andupetcu 37227369d3 Add Phase 2 & 3: Web frontend with authentication and tracking features
- Initialize Next.js 14 web application with Material UI and TypeScript
- Implement authentication (login/register) with device fingerprint
- Create mobile-first responsive layout with app shell pattern
- Add tracking pages for feeding, sleep, and diaper changes
- Implement activity history with filtering
- Configure backend CORS for web frontend (port 3030)
- Update backend port to 3020, frontend to 3030
- Fix API response handling for auth endpoints

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 21:21:22 +03:00

221 lines
7.1 KiB
TypeScript

'use client';
import { useState } from 'react';
import {
Box,
Typography,
Paper,
List,
ListItem,
ListItemAvatar,
ListItemText,
Avatar,
Chip,
IconButton,
Tabs,
Tab,
Button,
} from '@mui/material';
import {
Restaurant,
Hotel,
BabyChangingStation,
Delete,
Edit,
FilterList,
} from '@mui/icons-material';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { motion } from 'framer-motion';
import { formatDistanceToNow } from 'date-fns';
// Mock data - will be replaced with API calls
const mockActivities = [
{
id: '1',
type: 'feeding',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
details: 'Breast feeding - Left, 15 minutes',
icon: <Restaurant />,
color: '#FFB6C1',
},
{
id: '2',
type: 'diaper',
timestamp: new Date(Date.now() - 3 * 60 * 60 * 1000),
details: 'Diaper change - Wet',
icon: <BabyChangingStation />,
color: '#FFE4B5',
},
{
id: '3',
type: 'sleep',
timestamp: new Date(Date.now() - 5 * 60 * 60 * 1000),
details: 'Sleep - 2h 30m, Good quality',
icon: <Hotel />,
color: '#B6D7FF',
},
{
id: '4',
type: 'feeding',
timestamp: new Date(Date.now() - 6 * 60 * 60 * 1000),
details: 'Bottle - 120ml',
icon: <Restaurant />,
color: '#FFB6C1',
},
{
id: '5',
type: 'diaper',
timestamp: new Date(Date.now() - 7 * 60 * 60 * 1000),
details: 'Diaper change - Both',
icon: <BabyChangingStation />,
color: '#FFE4B5',
},
];
export default function HistoryPage() {
const [filter, setFilter] = useState<string>('all');
const [activities, setActivities] = useState(mockActivities);
const filteredActivities =
filter === 'all'
? activities
: activities.filter((activity) => activity.type === filter);
const handleDelete = (id: string) => {
// TODO: Call API to delete activity
setActivities(activities.filter((activity) => activity.id !== id));
};
return (
<ProtectedRoute>
<AppShell>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h4" fontWeight="600">
Activity History
</Typography>
<IconButton>
<FilterList />
</IconButton>
</Box>
{/* Filter Tabs */}
<Paper sx={{ mb: 3 }}>
<Tabs
value={filter}
onChange={(_, newValue) => setFilter(newValue)}
variant="scrollable"
scrollButtons="auto"
>
<Tab label="All" value="all" />
<Tab label="Feeding" value="feeding" icon={<Restaurant />} iconPosition="start" />
<Tab label="Sleep" value="sleep" icon={<Hotel />} iconPosition="start" />
<Tab label="Diaper" value="diaper" icon={<BabyChangingStation />} iconPosition="start" />
</Tabs>
</Paper>
{/* Activity Timeline */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<Paper>
<List>
{filteredActivities.length === 0 ? (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body1" color="text.secondary">
No activities found
</Typography>
</Box>
) : (
filteredActivities.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.05 }}
>
<ListItem
sx={{
borderBottom: index < filteredActivities.length - 1 ? '1px solid' : 'none',
borderColor: 'divider',
py: 2,
}}
secondaryAction={
<Box>
<IconButton edge="end" aria-label="edit" sx={{ mr: 1 }}>
<Edit />
</IconButton>
<IconButton
edge="end"
aria-label="delete"
onClick={() => handleDelete(activity.id)}
>
<Delete />
</IconButton>
</Box>
}
>
<ListItemAvatar>
<Avatar sx={{ bgcolor: activity.color }}>
{activity.icon}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={activity.details}
secondary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
<Typography variant="caption" color="text.secondary">
{formatDistanceToNow(activity.timestamp, { addSuffix: true })}
</Typography>
<Chip
label={activity.type}
size="small"
sx={{
height: 20,
fontSize: '0.7rem',
textTransform: 'capitalize',
}}
/>
</Box>
}
/>
</ListItem>
</motion.div>
))
)}
</List>
</Paper>
</motion.div>
{/* Daily Summary */}
<Paper sx={{ p: 3, mt: 3 }}>
<Typography variant="h6" fontWeight="600" gutterBottom>
Today's Summary
</Typography>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 2 }}>
<Chip
icon={<Restaurant />}
label={`${activities.filter((a) => a.type === 'feeding').length} Feedings`}
sx={{ bgcolor: '#FFB6C1', color: 'white' }}
/>
<Chip
icon={<Hotel />}
label={`${activities.filter((a) => a.type === 'sleep').length} Sleep Sessions`}
sx={{ bgcolor: '#B6D7FF', color: 'white' }}
/>
<Chip
icon={<BabyChangingStation />}
label={`${activities.filter((a) => a.type === 'diaper').length} Diaper Changes`}
sx={{ bgcolor: '#FFE4B5', color: 'white' }}
/>
</Box>
</Paper>
</Box>
</AppShell>
</ProtectedRoute>
);
}