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>
This commit is contained in:
220
maternal-web/app/history/page.tsx
Normal file
220
maternal-web/app/history/page.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user