- 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>
265 lines
8.5 KiB
TypeScript
265 lines
8.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Button,
|
|
Paper,
|
|
TextField,
|
|
IconButton,
|
|
Alert,
|
|
Chip,
|
|
FormControl,
|
|
FormLabel,
|
|
RadioGroup,
|
|
FormControlLabel,
|
|
Radio,
|
|
ToggleButtonGroup,
|
|
ToggleButton,
|
|
} from '@mui/material';
|
|
import {
|
|
ArrowBack,
|
|
Save,
|
|
Mic,
|
|
BabyChangingStation,
|
|
} from '@mui/icons-material';
|
|
import { useRouter } from 'next/navigation';
|
|
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
|
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
|
import { motion } from 'framer-motion';
|
|
import { useForm, Controller } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import * as z from 'zod';
|
|
import { format } from 'date-fns';
|
|
|
|
const diaperSchema = z.object({
|
|
type: z.enum(['wet', 'dirty', 'both', 'clean']),
|
|
timestamp: z.string(),
|
|
rash: z.boolean(),
|
|
notes: z.string().optional(),
|
|
});
|
|
|
|
type DiaperFormData = z.infer<typeof diaperSchema>;
|
|
|
|
export default function DiaperTrackPage() {
|
|
const router = useRouter();
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
setValue,
|
|
watch,
|
|
control,
|
|
formState: { errors },
|
|
} = useForm<DiaperFormData>({
|
|
resolver: zodResolver(diaperSchema),
|
|
defaultValues: {
|
|
type: 'wet',
|
|
rash: false,
|
|
timestamp: format(new Date(), "yyyy-MM-dd'T'HH:mm"),
|
|
},
|
|
});
|
|
|
|
const diaperType = watch('type');
|
|
const rash = watch('rash');
|
|
|
|
const setTimeNow = () => {
|
|
const now = format(new Date(), "yyyy-MM-dd'T'HH:mm");
|
|
setValue('timestamp', now);
|
|
};
|
|
|
|
const onSubmit = async (data: DiaperFormData) => {
|
|
setError(null);
|
|
|
|
try {
|
|
// TODO: Call API to save diaper data
|
|
console.log('Diaper data:', data);
|
|
setSuccess(true);
|
|
setTimeout(() => router.push('/'), 2000);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to log diaper change');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ProtectedRoute>
|
|
<AppShell>
|
|
<Box>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
|
|
<IconButton onClick={() => router.back()} sx={{ mr: 2 }}>
|
|
<ArrowBack />
|
|
</IconButton>
|
|
<Typography variant="h4" fontWeight="600">
|
|
Track Diaper Change
|
|
</Typography>
|
|
</Box>
|
|
|
|
{success && (
|
|
<Alert severity="success" sx={{ mb: 3 }}>
|
|
Diaper change logged successfully!
|
|
</Alert>
|
|
)}
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 3 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<Paper sx={{ p: 3, mb: 3 }}>
|
|
<Box component="form" onSubmit={handleSubmit(onSubmit)}>
|
|
{/* Icon Header */}
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 3 }}>
|
|
<BabyChangingStation sx={{ fontSize: 64, color: 'primary.main' }} />
|
|
</Box>
|
|
|
|
{/* Time */}
|
|
<Box sx={{ mb: 3 }}>
|
|
<FormLabel sx={{ mb: 1, display: 'block' }}>Time</FormLabel>
|
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
|
<TextField
|
|
fullWidth
|
|
type="datetime-local"
|
|
{...register('timestamp')}
|
|
error={!!errors.timestamp}
|
|
helperText={errors.timestamp?.message}
|
|
InputLabelProps={{ shrink: true }}
|
|
/>
|
|
<Button variant="outlined" onClick={setTimeNow} sx={{ minWidth: 100 }}>
|
|
Now
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Diaper Type */}
|
|
<FormControl component="fieldset" sx={{ mb: 3, width: '100%' }}>
|
|
<FormLabel component="legend" sx={{ mb: 2 }}>
|
|
Diaper Type
|
|
</FormLabel>
|
|
<Controller
|
|
name="type"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<ToggleButtonGroup
|
|
{...field}
|
|
exclusive
|
|
fullWidth
|
|
onChange={(_, value) => {
|
|
if (value !== null) {
|
|
field.onChange(value);
|
|
}
|
|
}}
|
|
>
|
|
<ToggleButton value="wet" sx={{ py: 2 }}>
|
|
<Box sx={{ textAlign: 'center' }}>
|
|
<Typography variant="h5">💧</Typography>
|
|
<Typography variant="body2">Wet</Typography>
|
|
</Box>
|
|
</ToggleButton>
|
|
<ToggleButton value="dirty" sx={{ py: 2 }}>
|
|
<Box sx={{ textAlign: 'center' }}>
|
|
<Typography variant="h5">💩</Typography>
|
|
<Typography variant="body2">Dirty</Typography>
|
|
</Box>
|
|
</ToggleButton>
|
|
<ToggleButton value="both" sx={{ py: 2 }}>
|
|
<Box sx={{ textAlign: 'center' }}>
|
|
<Typography variant="h5">💧💩</Typography>
|
|
<Typography variant="body2">Both</Typography>
|
|
</Box>
|
|
</ToggleButton>
|
|
<ToggleButton value="clean" sx={{ py: 2 }}>
|
|
<Box sx={{ textAlign: 'center' }}>
|
|
<Typography variant="h5">✨</Typography>
|
|
<Typography variant="body2">Clean</Typography>
|
|
</Box>
|
|
</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
)}
|
|
/>
|
|
</FormControl>
|
|
|
|
{/* Rash Indicator */}
|
|
<FormControl component="fieldset" sx={{ mb: 3, width: '100%' }}>
|
|
<FormLabel component="legend" sx={{ mb: 2 }}>
|
|
Diaper Rash?
|
|
</FormLabel>
|
|
<RadioGroup row>
|
|
<FormControlLabel
|
|
value="no"
|
|
control={
|
|
<Radio
|
|
checked={!rash}
|
|
onChange={() => setValue('rash', false)}
|
|
/>
|
|
}
|
|
label="No"
|
|
/>
|
|
<FormControlLabel
|
|
value="yes"
|
|
control={
|
|
<Radio
|
|
checked={rash}
|
|
onChange={() => setValue('rash', true)}
|
|
/>
|
|
}
|
|
label="Yes"
|
|
/>
|
|
</RadioGroup>
|
|
</FormControl>
|
|
|
|
{/* Rash Warning */}
|
|
{rash && (
|
|
<Alert severity="warning" sx={{ mb: 3 }}>
|
|
Consider applying diaper rash cream and consulting your pediatrician if it persists.
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Notes */}
|
|
<TextField
|
|
fullWidth
|
|
label="Notes (optional)"
|
|
multiline
|
|
rows={3}
|
|
{...register('notes')}
|
|
sx={{ mb: 3 }}
|
|
placeholder="Color, consistency, or any concerns..."
|
|
/>
|
|
|
|
{/* Voice Input Button */}
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 3 }}>
|
|
<Chip
|
|
icon={<Mic />}
|
|
label="Use Voice Input"
|
|
onClick={() => {/* TODO: Implement voice input */}}
|
|
sx={{ cursor: 'pointer' }}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Submit Button */}
|
|
<Button
|
|
fullWidth
|
|
type="submit"
|
|
variant="contained"
|
|
size="large"
|
|
startIcon={<Save />}
|
|
>
|
|
Save Diaper Change
|
|
</Button>
|
|
</Box>
|
|
</Paper>
|
|
</motion.div>
|
|
</Box>
|
|
</AppShell>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|