Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
Updated 4 additional pages to reach 100% Phase 4 completion: 1. Reset Password Page (auth/reset-password) - Added extractError() for password reset failures - Improved error messaging for expired tokens 2. Children Page (children/page) - Updated fetch, save, and delete operations - All 3 error handlers now use extractError() 3. Analytics Page (analytics/page) - Updated children loading, insights, and predictions - All 3 API calls now have consistent error handling 4. Advanced Analytics Page (analytics/advanced/page) - Updated 6 error handlers (children, circadian, anomalies, growth, correlations, trends) - Consistent error extraction across all analytics features Phase 4 Status: 100% COMPLETE ✅ - Total forms updated: 21/21 (100%) - Auth forms: 4/4 ✅ - Family & child management: 3/3 ✅ - Activity tracking: 6/6 ✅ - Settings & onboarding: 2/2 ✅ - Analytics & children pages: 4/4 ✅ (NEW) - Other pages: 2/2 ✅ (PhotoUpload, components) Error Improvement Plan: ~90% complete - Phase 1-4: 100% ✅ - Phase 5-6: Backend improvements (pending) All frontend forms now use centralized error handling with user-friendly, multilingual error messages from the errorHandler utility. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
335 lines
10 KiB
TypeScript
335 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useSearchParams, useRouter } from 'next/navigation';
|
|
import {
|
|
Box,
|
|
TextField,
|
|
Button,
|
|
Typography,
|
|
Paper,
|
|
Alert,
|
|
CircularProgress,
|
|
InputAdornment,
|
|
IconButton,
|
|
Link as MuiLink,
|
|
} from '@mui/material';
|
|
import { LockReset, Visibility, VisibilityOff, CheckCircle } from '@mui/icons-material';
|
|
import { motion } from 'framer-motion';
|
|
import Link from 'next/link';
|
|
import apiClient from '@/lib/api/client';
|
|
import { extractError } from '@/lib/utils/errorHandler';
|
|
|
|
export default function ResetPasswordPage() {
|
|
const searchParams = useSearchParams();
|
|
const router = useRouter();
|
|
const [token, setToken] = useState('');
|
|
const [newPassword, setNewPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const tokenParam = searchParams.get('token');
|
|
if (tokenParam) {
|
|
setToken(tokenParam);
|
|
} else {
|
|
setError('Invalid or missing reset token');
|
|
}
|
|
}, [searchParams]);
|
|
|
|
const validatePassword = (password: string): string | null => {
|
|
if (password.length < 8) {
|
|
return 'Password must be at least 8 characters long';
|
|
}
|
|
if (!/[a-z]/.test(password)) {
|
|
return 'Password must contain at least one lowercase letter';
|
|
}
|
|
if (!/[A-Z]/.test(password)) {
|
|
return 'Password must contain at least one uppercase letter';
|
|
}
|
|
if (!/\d/.test(password)) {
|
|
return 'Password must contain at least one number';
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
// Validation
|
|
if (!newPassword.trim()) {
|
|
setError('Please enter a new password');
|
|
return;
|
|
}
|
|
|
|
const passwordError = validatePassword(newPassword);
|
|
if (passwordError) {
|
|
setError(passwordError);
|
|
return;
|
|
}
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
setError('Passwords do not match');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError('');
|
|
|
|
try {
|
|
await apiClient.post('/api/v1/auth/password/reset', {
|
|
token,
|
|
newPassword,
|
|
});
|
|
setSuccess(true);
|
|
|
|
// Redirect to login after 3 seconds
|
|
setTimeout(() => {
|
|
router.push('/login');
|
|
}, 3000);
|
|
} catch (err: any) {
|
|
console.error('Reset password error:', err);
|
|
const errorData = extractError(err);
|
|
setError(errorData.userMessage || errorData.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (!token && !error) {
|
|
return (
|
|
<Box
|
|
sx={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<CircularProgress />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'linear-gradient(135deg, #FFE4E1 0%, #FFDAB9 100%)',
|
|
px: 2,
|
|
}}
|
|
>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
maxWidth: 480,
|
|
width: '100%',
|
|
p: 4,
|
|
borderRadius: 4,
|
|
background: 'rgba(255, 255, 255, 0.95)',
|
|
backdropFilter: 'blur(10px)',
|
|
}}
|
|
>
|
|
{!success ? (
|
|
<>
|
|
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
|
<Box
|
|
sx={{
|
|
width: 64,
|
|
height: 64,
|
|
borderRadius: '50%',
|
|
bgcolor: 'primary.light',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
mx: 'auto',
|
|
mb: 2,
|
|
}}
|
|
>
|
|
<LockReset sx={{ fontSize: 32, color: 'primary.main' }} />
|
|
</Box>
|
|
<Typography variant="h4" fontWeight="600" gutterBottom>
|
|
Reset Password
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Enter your new password below
|
|
</Typography>
|
|
</Box>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 3, borderRadius: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<TextField
|
|
fullWidth
|
|
label="New Password"
|
|
type={showPassword ? 'text' : 'password'}
|
|
value={newPassword}
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
|
disabled={loading}
|
|
required
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
edge="end"
|
|
>
|
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
sx={{
|
|
mb: 2,
|
|
'& .MuiOutlinedInput-root': {
|
|
borderRadius: 3,
|
|
},
|
|
}}
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Confirm New Password"
|
|
type={showConfirmPassword ? 'text' : 'password'}
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
disabled={loading}
|
|
required
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
edge="end"
|
|
>
|
|
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
sx={{
|
|
mb: 2,
|
|
'& .MuiOutlinedInput-root': {
|
|
borderRadius: 3,
|
|
},
|
|
}}
|
|
/>
|
|
|
|
<Alert severity="info" sx={{ mb: 3, borderRadius: 2 }}>
|
|
<Typography variant="body2" fontWeight="600" gutterBottom>
|
|
Password Requirements:
|
|
</Typography>
|
|
<Typography variant="body2" component="div">
|
|
• At least 8 characters long
|
|
<br />
|
|
• One uppercase letter (A-Z)
|
|
<br />
|
|
• One lowercase letter (a-z)
|
|
<br />• One number (0-9)
|
|
</Typography>
|
|
</Alert>
|
|
|
|
<Button
|
|
fullWidth
|
|
type="submit"
|
|
variant="contained"
|
|
size="large"
|
|
disabled={loading || !token}
|
|
sx={{
|
|
borderRadius: 3,
|
|
py: 1.5,
|
|
textTransform: 'none',
|
|
fontSize: 16,
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
{loading ? <CircularProgress size={24} color="inherit" /> : 'Reset Password'}
|
|
</Button>
|
|
</form>
|
|
|
|
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
|
<MuiLink
|
|
component={Link}
|
|
href="/login"
|
|
sx={{
|
|
color: 'primary.main',
|
|
textDecoration: 'none',
|
|
fontWeight: 500,
|
|
'&:hover': {
|
|
textDecoration: 'underline',
|
|
},
|
|
}}
|
|
>
|
|
Back to Login
|
|
</MuiLink>
|
|
</Box>
|
|
</>
|
|
) : (
|
|
<Box sx={{ textAlign: 'center' }}>
|
|
<Box
|
|
sx={{
|
|
width: 80,
|
|
height: 80,
|
|
borderRadius: '50%',
|
|
bgcolor: 'success.light',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
mx: 'auto',
|
|
mb: 3,
|
|
}}
|
|
>
|
|
<CheckCircle sx={{ fontSize: 48, color: 'success.main' }} />
|
|
</Box>
|
|
|
|
<Typography variant="h5" fontWeight="600" gutterBottom>
|
|
Password Reset Successful! 🎉
|
|
</Typography>
|
|
|
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
|
Your password has been reset successfully. You can now log in with your new password.
|
|
</Typography>
|
|
|
|
<Alert severity="success" sx={{ mb: 3, borderRadius: 2 }}>
|
|
Redirecting to login page...
|
|
</Alert>
|
|
|
|
<Button
|
|
fullWidth
|
|
variant="contained"
|
|
component={Link}
|
|
href="/login"
|
|
sx={{
|
|
borderRadius: 3,
|
|
py: 1.5,
|
|
textTransform: 'none',
|
|
fontSize: 16,
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
Go to Login
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
</Paper>
|
|
</motion.div>
|
|
</Box>
|
|
);
|
|
}
|