- 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>
222 lines
6.0 KiB
TypeScript
222 lines
6.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
Box,
|
|
TextField,
|
|
Button,
|
|
Typography,
|
|
Paper,
|
|
InputAdornment,
|
|
IconButton,
|
|
Divider,
|
|
Alert,
|
|
CircularProgress,
|
|
Link as MuiLink,
|
|
} from '@mui/material';
|
|
import { Visibility, VisibilityOff, Google, Apple } from '@mui/icons-material';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { motion } from 'framer-motion';
|
|
import * as z from 'zod';
|
|
import { useAuth } from '@/lib/auth/AuthContext';
|
|
import Link from 'next/link';
|
|
|
|
const loginSchema = z.object({
|
|
email: z.string().email('Invalid email address'),
|
|
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
});
|
|
|
|
type LoginFormData = z.infer<typeof loginSchema>;
|
|
|
|
export default function LoginPage() {
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const { login } = useAuth();
|
|
const router = useRouter();
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
} = useForm<LoginFormData>({
|
|
resolver: zodResolver(loginSchema),
|
|
});
|
|
|
|
const onSubmit = async (data: LoginFormData) => {
|
|
setError(null);
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
await login(data);
|
|
// Navigation is handled in the login function
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to login. Please check your credentials.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'center',
|
|
px: 3,
|
|
py: 6,
|
|
background: 'linear-gradient(135deg, #FFE4E1 0%, #FFDAB9 100%)',
|
|
}}
|
|
>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 4,
|
|
borderRadius: 4,
|
|
maxWidth: 440,
|
|
mx: 'auto',
|
|
background: 'rgba(255, 255, 255, 0.95)',
|
|
backdropFilter: 'blur(10px)',
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="h4"
|
|
gutterBottom
|
|
align="center"
|
|
fontWeight="600"
|
|
color="primary.main"
|
|
>
|
|
Welcome Back 👋
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
align="center"
|
|
color="text.secondary"
|
|
sx={{ mb: 3 }}
|
|
>
|
|
Sign in to continue tracking your child's journey
|
|
</Typography>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 3, borderRadius: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<Box component="form" onSubmit={handleSubmit(onSubmit)}>
|
|
<TextField
|
|
fullWidth
|
|
label="Email"
|
|
type="email"
|
|
margin="normal"
|
|
error={!!errors.email}
|
|
helperText={errors.email?.message}
|
|
{...register('email')}
|
|
disabled={isLoading}
|
|
inputProps={{ autoComplete: 'username' }}
|
|
InputProps={{
|
|
sx: { borderRadius: 3 },
|
|
}}
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Password"
|
|
type={showPassword ? 'text' : 'password'}
|
|
margin="normal"
|
|
error={!!errors.password}
|
|
helperText={errors.password?.message}
|
|
{...register('password')}
|
|
disabled={isLoading}
|
|
inputProps={{ autoComplete: 'current-password' }}
|
|
InputProps={{
|
|
sx: { borderRadius: 3 },
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
edge="end"
|
|
disabled={isLoading}
|
|
>
|
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
|
|
<Box sx={{ textAlign: 'right', mt: 1 }}>
|
|
<Link href="/forgot-password" passHref legacyBehavior>
|
|
<MuiLink variant="body2" sx={{ cursor: 'pointer' }}>
|
|
Forgot password?
|
|
</MuiLink>
|
|
</Link>
|
|
</Box>
|
|
|
|
<Button
|
|
fullWidth
|
|
type="submit"
|
|
variant="contained"
|
|
size="large"
|
|
disabled={isLoading}
|
|
sx={{ mt: 3, mb: 2 }}
|
|
>
|
|
{isLoading ? (
|
|
<CircularProgress size={24} color="inherit" />
|
|
) : (
|
|
'Sign In'
|
|
)}
|
|
</Button>
|
|
</Box>
|
|
|
|
<Divider sx={{ my: 3 }}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
OR
|
|
</Typography>
|
|
</Divider>
|
|
|
|
<Button
|
|
fullWidth
|
|
variant="outlined"
|
|
startIcon={<Google />}
|
|
size="large"
|
|
disabled={isLoading}
|
|
sx={{ mb: 2 }}
|
|
>
|
|
Continue with Google
|
|
</Button>
|
|
|
|
<Button
|
|
fullWidth
|
|
variant="outlined"
|
|
startIcon={<Apple />}
|
|
size="large"
|
|
disabled={isLoading}
|
|
>
|
|
Continue with Apple
|
|
</Button>
|
|
|
|
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Don't have an account?{' '}
|
|
<Link href="/register" passHref legacyBehavior>
|
|
<MuiLink sx={{ cursor: 'pointer', fontWeight: 600 }}>
|
|
Sign up
|
|
</MuiLink>
|
|
</Link>
|
|
</Typography>
|
|
</Box>
|
|
</Paper>
|
|
</motion.div>
|
|
</Box>
|
|
);
|
|
}
|