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:
264
maternal-web/app/track/diaper/page.tsx
Normal file
264
maternal-web/app/track/diaper/page.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
254
maternal-web/app/track/feeding/page.tsx
Normal file
254
maternal-web/app/track/feeding/page.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
IconButton,
|
||||
Alert,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack,
|
||||
PlayArrow,
|
||||
Stop,
|
||||
Save,
|
||||
Mic,
|
||||
} 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 } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
const feedingSchema = z.object({
|
||||
type: z.enum(['breast_left', 'breast_right', 'breast_both', 'bottle', 'solid']),
|
||||
amount: z.number().min(0).optional(),
|
||||
unit: z.enum(['ml', 'oz']).optional(),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
type FeedingFormData = z.infer<typeof feedingSchema>;
|
||||
|
||||
export default function FeedingTrackPage() {
|
||||
const router = useRouter();
|
||||
const [isTimerRunning, setIsTimerRunning] = useState(false);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<FeedingFormData>({
|
||||
resolver: zodResolver(feedingSchema),
|
||||
defaultValues: {
|
||||
type: 'breast_left',
|
||||
unit: 'ml',
|
||||
},
|
||||
});
|
||||
|
||||
const feedingType = watch('type');
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (isTimerRunning) {
|
||||
interval = setInterval(() => {
|
||||
setDuration((prev) => prev + 1);
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [isTimerRunning]);
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const onSubmit = async (data: FeedingFormData) => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// TODO: Call API to save feeding data
|
||||
console.log('Feeding data:', { ...data, duration });
|
||||
setSuccess(true);
|
||||
setTimeout(() => router.push('/'), 2000);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to log feeding');
|
||||
}
|
||||
};
|
||||
|
||||
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 Feeding
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{success && (
|
||||
<Alert severity="success" sx={{ mb: 3 }}>
|
||||
Feeding 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 }}>
|
||||
{/* Timer Section */}
|
||||
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
||||
<Typography variant="h2" fontWeight="600" sx={{ mb: 2 }}>
|
||||
{formatDuration(duration)}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
||||
{!isTimerRunning ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<PlayArrow />}
|
||||
onClick={() => setIsTimerRunning(true)}
|
||||
>
|
||||
Start Timer
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
size="large"
|
||||
startIcon={<Stop />}
|
||||
onClick={() => setIsTimerRunning(false)}
|
||||
>
|
||||
Stop Timer
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* Feeding Type */}
|
||||
<FormControl component="fieldset" sx={{ mb: 3, width: '100%' }}>
|
||||
<FormLabel component="legend" sx={{ mb: 2 }}>
|
||||
Feeding Type
|
||||
</FormLabel>
|
||||
<RadioGroup row>
|
||||
<FormControlLabel
|
||||
value="breast_left"
|
||||
control={<Radio {...register('type')} />}
|
||||
label="Left Breast"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="breast_right"
|
||||
control={<Radio {...register('type')} />}
|
||||
label="Right Breast"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="breast_both"
|
||||
control={<Radio {...register('type')} />}
|
||||
label="Both"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="bottle"
|
||||
control={<Radio {...register('type')} />}
|
||||
label="Bottle"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="solid"
|
||||
control={<Radio {...register('type')} />}
|
||||
label="Solid Food"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
{/* Amount (for bottle/solid) */}
|
||||
{(feedingType === 'bottle' || feedingType === 'solid') && (
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Amount"
|
||||
type="number"
|
||||
{...register('amount', { valueAsNumber: true })}
|
||||
error={!!errors.amount}
|
||||
helperText={errors.amount?.message}
|
||||
/>
|
||||
<FormControl sx={{ minWidth: 120 }}>
|
||||
<RadioGroup row>
|
||||
<FormControlLabel
|
||||
value="ml"
|
||||
control={<Radio {...register('unit')} />}
|
||||
label="ml"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="oz"
|
||||
control={<Radio {...register('unit')} />}
|
||||
label="oz"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Notes (optional)"
|
||||
multiline
|
||||
rows={3}
|
||||
{...register('notes')}
|
||||
sx={{ mb: 3 }}
|
||||
/>
|
||||
|
||||
{/* 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 Feeding
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
259
maternal-web/app/track/sleep/page.tsx
Normal file
259
maternal-web/app/track/sleep/page.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
IconButton,
|
||||
Alert,
|
||||
Chip,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack,
|
||||
Bedtime,
|
||||
WbSunny,
|
||||
Save,
|
||||
Mic,
|
||||
} 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 } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const sleepSchema = z.object({
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
quality: z.enum(['excellent', 'good', 'fair', 'poor']),
|
||||
notes: z.string().optional(),
|
||||
}).refine((data) => new Date(data.endTime) > new Date(data.startTime), {
|
||||
message: 'End time must be after start time',
|
||||
path: ['endTime'],
|
||||
});
|
||||
|
||||
type SleepFormData = z.infer<typeof sleepSchema>;
|
||||
|
||||
export default function SleepTrackPage() {
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<SleepFormData>({
|
||||
resolver: zodResolver(sleepSchema),
|
||||
defaultValues: {
|
||||
quality: 'good',
|
||||
},
|
||||
});
|
||||
|
||||
const startTime = watch('startTime');
|
||||
const endTime = watch('endTime');
|
||||
|
||||
const calculateDuration = () => {
|
||||
if (!startTime || !endTime) return null;
|
||||
const start = new Date(startTime);
|
||||
const end = new Date(endTime);
|
||||
const diff = end.getTime() - start.getTime();
|
||||
if (diff < 0) return null;
|
||||
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
return `${hours}h ${minutes}m`;
|
||||
};
|
||||
|
||||
const setStartNow = () => {
|
||||
const now = format(new Date(), "yyyy-MM-dd'T'HH:mm");
|
||||
setValue('startTime', now);
|
||||
};
|
||||
|
||||
const setEndNow = () => {
|
||||
const now = format(new Date(), "yyyy-MM-dd'T'HH:mm");
|
||||
setValue('endTime', now);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: SleepFormData) => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// TODO: Call API to save sleep data
|
||||
console.log('Sleep data:', data);
|
||||
setSuccess(true);
|
||||
setTimeout(() => router.push('/'), 2000);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to log sleep');
|
||||
}
|
||||
};
|
||||
|
||||
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 Sleep
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{success && (
|
||||
<Alert severity="success" sx={{ mb: 3 }}>
|
||||
Sleep 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)}>
|
||||
{/* Start Time */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
|
||||
<Bedtime color="primary" />
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
Sleep Start
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="datetime-local"
|
||||
{...register('startTime')}
|
||||
error={!!errors.startTime}
|
||||
helperText={errors.startTime?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<Button variant="outlined" onClick={setStartNow} sx={{ minWidth: 100 }}>
|
||||
Now
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* End Time */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
|
||||
<WbSunny color="warning" />
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
Wake Up
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="datetime-local"
|
||||
{...register('endTime')}
|
||||
error={!!errors.endTime}
|
||||
helperText={errors.endTime?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<Button variant="outlined" onClick={setEndNow} sx={{ minWidth: 100 }}>
|
||||
Now
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Duration Display */}
|
||||
{calculateDuration() && (
|
||||
<Box sx={{ mb: 3, textAlign: 'center' }}>
|
||||
<Chip
|
||||
label={`Duration: ${calculateDuration()}`}
|
||||
color="primary"
|
||||
sx={{ fontSize: '1rem', py: 3 }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Sleep Quality */}
|
||||
<FormControl component="fieldset" sx={{ mb: 3, width: '100%' }}>
|
||||
<FormLabel component="legend" sx={{ mb: 2 }}>
|
||||
Sleep Quality
|
||||
</FormLabel>
|
||||
<RadioGroup row>
|
||||
<FormControlLabel
|
||||
value="excellent"
|
||||
control={<Radio {...register('quality')} />}
|
||||
label="Excellent"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="good"
|
||||
control={<Radio {...register('quality')} />}
|
||||
label="Good"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="fair"
|
||||
control={<Radio {...register('quality')} />}
|
||||
label="Fair"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="poor"
|
||||
control={<Radio {...register('quality')} />}
|
||||
label="Poor"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
{/* Notes */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Notes (optional)"
|
||||
multiline
|
||||
rows={3}
|
||||
{...register('notes')}
|
||||
sx={{ mb: 3 }}
|
||||
placeholder="Any disruptions, dreams, or observations..."
|
||||
/>
|
||||
|
||||
{/* 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 Sleep Session
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user