Add comprehensive .gitignore
This commit is contained in:
221
maternal-web/app/(auth)/login/page.tsx
Normal file
221
maternal-web/app/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
315
maternal-web/app/(auth)/onboarding/page.tsx
Normal file
315
maternal-web/app/(auth)/onboarding/page.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Stepper,
|
||||
Step,
|
||||
StepLabel,
|
||||
Button,
|
||||
Typography,
|
||||
Paper,
|
||||
TextField,
|
||||
Avatar,
|
||||
IconButton,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import { ArrowBack, ArrowForward, Check } from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { childrenApi } from '@/lib/api/children';
|
||||
|
||||
const steps = ['Welcome', 'Add Child', 'Invite Family', 'Notifications'];
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [childName, setChildName] = useState('');
|
||||
const [childBirthDate, setChildBirthDate] = useState('');
|
||||
const [childGender, setChildGender] = useState<'male' | 'female' | 'other'>('other');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
|
||||
const handleNext = async () => {
|
||||
// Validate and save child data on step 1 (Add Child)
|
||||
if (activeStep === 1) {
|
||||
if (!childName.trim() || !childBirthDate) {
|
||||
setError('Please enter child name and birth date');
|
||||
return;
|
||||
}
|
||||
|
||||
const familyId = user?.families?.[0]?.familyId;
|
||||
if (!familyId) {
|
||||
setError('No family found. Please try logging out and back in.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
await childrenApi.createChild(familyId, {
|
||||
name: childName.trim(),
|
||||
birthDate: childBirthDate,
|
||||
gender: childGender,
|
||||
});
|
||||
setActiveStep((prevActiveStep) => prevActiveStep + 1);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to create child:', err);
|
||||
setError(err.response?.data?.message || 'Failed to save child. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeStep === steps.length - 1) {
|
||||
// Complete onboarding
|
||||
router.push('/');
|
||||
} else {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
px: 3,
|
||||
py: 4,
|
||||
background: 'linear-gradient(135deg, #FFE4E1 0%, #FFDAB9 100%)',
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
maxWidth: 600,
|
||||
mx: 'auto',
|
||||
width: '100%',
|
||||
p: 4,
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
}}
|
||||
>
|
||||
<Stepper activeStep={activeStep} sx={{ mb: 4 }}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeStep}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{activeStep === 0 && (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="h4" gutterBottom fontWeight="600" color="primary.main">
|
||||
Welcome to Maternal! 🎉
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mt: 2, mb: 4 }}>
|
||||
We're excited to help you track and understand your child's development, sleep patterns, feeding schedules, and more.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<Paper sx={{ p: 2, flex: 1, minWidth: 150 }}>
|
||||
<Typography variant="h6" fontWeight="600">📊</Typography>
|
||||
<Typography variant="body2">Track Activities</Typography>
|
||||
</Paper>
|
||||
<Paper sx={{ p: 2, flex: 1, minWidth: 150 }}>
|
||||
<Typography variant="h6" fontWeight="600">🤖</Typography>
|
||||
<Typography variant="body2">AI Insights</Typography>
|
||||
</Paper>
|
||||
<Paper sx={{ p: 2, flex: 1, minWidth: 150 }}>
|
||||
<Typography variant="h6" fontWeight="600">👨👩👧</Typography>
|
||||
<Typography variant="body2">Family Sharing</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{activeStep === 1 && (
|
||||
<Box sx={{ py: 4 }}>
|
||||
<Typography variant="h5" gutterBottom fontWeight="600">
|
||||
Add Your First Child
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Let's start by adding some basic information about your child.
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2, borderRadius: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Child's Name"
|
||||
value={childName}
|
||||
onChange={(e) => setChildName(e.target.value)}
|
||||
margin="normal"
|
||||
required
|
||||
disabled={loading}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Birth Date"
|
||||
type="date"
|
||||
value={childBirthDate}
|
||||
onChange={(e) => setChildBirthDate(e.target.value)}
|
||||
margin="normal"
|
||||
required
|
||||
disabled={loading}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
label="Gender"
|
||||
value={childGender}
|
||||
onChange={(e) => setChildGender(e.target.value as 'male' | 'female' | 'other')}
|
||||
margin="normal"
|
||||
disabled={loading}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
>
|
||||
<MenuItem value="male">Male</MenuItem>
|
||||
<MenuItem value="female">Female</MenuItem>
|
||||
<MenuItem value="other">Prefer not to say</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<Alert severity="info" sx={{ mt: 3, borderRadius: 2 }}>
|
||||
You can add more children and details later from settings.
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{activeStep === 2 && (
|
||||
<Box sx={{ py: 4 }}>
|
||||
<Typography variant="h5" gutterBottom fontWeight="600">
|
||||
Invite Family Members
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Share your child's progress with family members. They can view activities and add their own entries.
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email Address"
|
||||
type="email"
|
||||
margin="normal"
|
||||
placeholder="partner@example.com"
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Send Invitation
|
||||
</Button>
|
||||
|
||||
<Alert severity="info" sx={{ mt: 3, borderRadius: 2 }}>
|
||||
You can skip this step and invite family members later.
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{activeStep === 3 && (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
bgcolor: 'primary.main',
|
||||
mx: 'auto',
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<Check sx={{ fontSize: 48 }} />
|
||||
</Avatar>
|
||||
<Typography variant="h5" gutterBottom fontWeight="600">
|
||||
You're All Set! 🎉
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
||||
Start tracking your child's activities and get personalized insights.
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ p: 3, bgcolor: 'primary.light', mb: 3 }}>
|
||||
<Typography variant="body2" fontWeight="600" gutterBottom>
|
||||
Next Steps:
|
||||
</Typography>
|
||||
<Typography variant="body2" align="left" component="div">
|
||||
• Track your first feeding, sleep, or diaper change<br />
|
||||
• Chat with our AI assistant for parenting tips<br />
|
||||
• Explore insights and predictions based on your data
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 4 }}>
|
||||
<Button
|
||||
onClick={handleBack}
|
||||
disabled={activeStep === 0}
|
||||
startIcon={<ArrowBack />}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Box sx={{ flex: 1 }} />
|
||||
|
||||
{activeStep < steps.length - 1 && activeStep > 0 && (
|
||||
<Button onClick={handleSkip} sx={{ mr: 2 }}>
|
||||
Skip
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleNext}
|
||||
disabled={loading}
|
||||
endIcon={loading ? <CircularProgress size={20} /> : (activeStep === steps.length - 1 ? <Check /> : <ArrowForward />)}
|
||||
>
|
||||
{activeStep === steps.length - 1 ? 'Get Started' : 'Next'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
268
maternal-web/app/(auth)/register/page.tsx
Normal file
268
maternal-web/app/(auth)/register/page.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user