- 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>
269 lines
8.0 KiB
TypeScript
269 lines
8.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
Box,
|
|
TextField,
|
|
Button,
|
|
Typography,
|
|
Paper,
|
|
InputAdornment,
|
|
IconButton,
|
|
Alert,
|
|
CircularProgress,
|
|
Link as MuiLink,
|
|
Checkbox,
|
|
FormControlLabel,
|
|
} from '@mui/material';
|
|
import { Visibility, VisibilityOff } 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 registerSchema = z.object({
|
|
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
email: z.string().email('Invalid email address'),
|
|
password: z.string()
|
|
.min(8, 'Password must be at least 8 characters')
|
|
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
|
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
|
.regex(/[0-9]/, 'Password must contain at least one number'),
|
|
confirmPassword: z.string(),
|
|
agreeToTerms: z.boolean().refine(val => val === true, {
|
|
message: 'You must agree to the terms and conditions',
|
|
}),
|
|
}).refine((data) => data.password === data.confirmPassword, {
|
|
message: 'Passwords do not match',
|
|
path: ['confirmPassword'],
|
|
});
|
|
|
|
type RegisterFormData = z.infer<typeof registerSchema>;
|
|
|
|
export default function RegisterPage() {
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const { register: registerUser } = useAuth();
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
} = useForm<RegisterFormData>({
|
|
resolver: zodResolver(registerSchema),
|
|
});
|
|
|
|
const onSubmit = async (data: RegisterFormData) => {
|
|
setError(null);
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
await registerUser({
|
|
name: data.name,
|
|
email: data.email,
|
|
password: data.password,
|
|
});
|
|
// Navigation to onboarding is handled in the register function
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to register. Please try again.');
|
|
} 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"
|
|
>
|
|
Create Account ✨
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
align="center"
|
|
color="text.secondary"
|
|
sx={{ mb: 3 }}
|
|
>
|
|
Start your journey to organized parenting
|
|
</Typography>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 3, borderRadius: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<Box component="form" onSubmit={handleSubmit(onSubmit)}>
|
|
<TextField
|
|
fullWidth
|
|
label="Full Name"
|
|
margin="normal"
|
|
error={!!errors.name}
|
|
helperText={errors.name?.message}
|
|
{...register('name')}
|
|
disabled={isLoading}
|
|
InputProps={{
|
|
sx: { borderRadius: 3 },
|
|
}}
|
|
/>
|
|
|
|
<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: 'new-password' }}
|
|
InputProps={{
|
|
sx: { borderRadius: 3 },
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
edge="end"
|
|
disabled={isLoading}
|
|
>
|
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Confirm Password"
|
|
type={showConfirmPassword ? 'text' : 'password'}
|
|
margin="normal"
|
|
error={!!errors.confirmPassword}
|
|
helperText={errors.confirmPassword?.message}
|
|
{...register('confirmPassword')}
|
|
disabled={isLoading}
|
|
inputProps={{ autoComplete: 'new-password' }}
|
|
InputProps={{
|
|
sx: { borderRadius: 3 },
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
edge="end"
|
|
disabled={isLoading}
|
|
>
|
|
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
{...register('agreeToTerms')}
|
|
disabled={isLoading}
|
|
/>
|
|
}
|
|
label={
|
|
<Typography variant="body2" color="text.secondary">
|
|
I agree to the{' '}
|
|
<MuiLink href="/terms" target="_blank">
|
|
Terms of Service
|
|
</MuiLink>{' '}
|
|
and{' '}
|
|
<MuiLink href="/privacy" target="_blank">
|
|
Privacy Policy
|
|
</MuiLink>
|
|
</Typography>
|
|
}
|
|
sx={{ mt: 2 }}
|
|
/>
|
|
{errors.agreeToTerms && (
|
|
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 1 }}>
|
|
{errors.agreeToTerms.message}
|
|
</Typography>
|
|
)}
|
|
|
|
<Button
|
|
fullWidth
|
|
type="submit"
|
|
variant="contained"
|
|
size="large"
|
|
disabled={isLoading}
|
|
sx={{ mt: 3, mb: 2 }}
|
|
>
|
|
{isLoading ? (
|
|
<CircularProgress size={24} color="inherit" />
|
|
) : (
|
|
'Create Account'
|
|
)}
|
|
</Button>
|
|
</Box>
|
|
|
|
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Already have an account?{' '}
|
|
<Link href="/login" passHref legacyBehavior>
|
|
<MuiLink sx={{ cursor: 'pointer', fontWeight: 600 }}>
|
|
Sign in
|
|
</MuiLink>
|
|
</Link>
|
|
</Typography>
|
|
</Box>
|
|
</Paper>
|
|
</motion.div>
|
|
</Box>
|
|
);
|
|
}
|