745 lines
25 KiB
TypeScript
745 lines
25 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Container,
|
|
Paper,
|
|
TextField,
|
|
Button,
|
|
Typography,
|
|
Grid,
|
|
Alert,
|
|
CircularProgress,
|
|
Tabs,
|
|
Tab,
|
|
AppBar,
|
|
Toolbar,
|
|
Card,
|
|
CardContent,
|
|
Divider,
|
|
Avatar,
|
|
IconButton,
|
|
InputAdornment,
|
|
List,
|
|
ListItem,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
Collapse,
|
|
FormControl,
|
|
FormLabel,
|
|
RadioGroup,
|
|
FormControlLabel,
|
|
Radio,
|
|
Chip
|
|
} from '@mui/material';
|
|
import {
|
|
Person,
|
|
PersonAdd,
|
|
AccountCircle,
|
|
ExitToApp,
|
|
Visibility,
|
|
VisibilityOff,
|
|
CheckCircle,
|
|
Cancel,
|
|
ExpandLess,
|
|
ExpandMore,
|
|
Visibility as ViewIcon,
|
|
Work,
|
|
Business
|
|
} from '@mui/icons-material';
|
|
import 'react-phone-number-input/style.css';
|
|
import PhoneInput from 'react-phone-number-input';
|
|
import { E164Number } from 'libphonenumber-js/core';
|
|
import './LoginPage.css';
|
|
|
|
import { ApiClient } from 'services/api-client';
|
|
import { useAuth } from 'hooks/AuthContext';
|
|
import { LocationInput } from 'components/LocationInput';
|
|
import { Location } from 'types/types';
|
|
|
|
import { Candidate } from 'types/types'
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { BackstoryPageProps } from 'components/BackstoryTab';
|
|
|
|
type UserRegistrationType = 'candidate' | 'employer';
|
|
|
|
interface LoginRequest {
|
|
login: string;
|
|
password: string;
|
|
}
|
|
|
|
interface RegisterRequest {
|
|
userType: UserRegistrationType;
|
|
username: string;
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
password: string;
|
|
confirmPassword: string;
|
|
phone?: string;
|
|
// Employer specific fields (placeholder)
|
|
companyName?: string;
|
|
industry?: string;
|
|
companySize?: string;
|
|
}
|
|
|
|
interface PasswordRequirement {
|
|
label: string;
|
|
met: boolean;
|
|
}
|
|
|
|
const apiClient = new ApiClient();
|
|
|
|
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|
const { setSnack } = props;
|
|
const [tabValue, setTabValue] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [success, setSuccess] = useState<string | null>(null);
|
|
const [phone, setPhone] = useState<E164Number | null>(null);
|
|
const { createCandidateAccount, guest, user, login, isLoading, error } = useAuth();
|
|
const [passwordValidation, setPasswordValidation] = useState<{ isValid: boolean; issues: string[] }>({ isValid: true, issues: [] });
|
|
const name = (user?.userType === 'candidate') ? user.username : user?.email || '';
|
|
const [location, setLocation] = useState<Partial<Location>>({});
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
|
|
// Password visibility states
|
|
const [showLoginPassword, setShowLoginPassword] = useState(false);
|
|
const [showRegisterPassword, setShowRegisterPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [showPasswordRequirements, setShowPasswordRequirements] = useState(false);
|
|
|
|
const handleLocationChange = (location: Partial<Location>) => {
|
|
setLocation(location);
|
|
console.log('Location updated:', location);
|
|
};
|
|
|
|
// Login form state
|
|
const [loginForm, setLoginForm] = useState<LoginRequest>({
|
|
login: '',
|
|
password: ''
|
|
});
|
|
|
|
// Register form state
|
|
const [registerForm, setRegisterForm] = useState<RegisterRequest>({
|
|
userType: 'candidate',
|
|
username: '',
|
|
email: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
password: '',
|
|
confirmPassword: '',
|
|
phone: '',
|
|
companyName: '',
|
|
industry: '',
|
|
companySize: ''
|
|
});
|
|
|
|
// Password requirements validation
|
|
const getPasswordRequirements = (password: string): PasswordRequirement[] => {
|
|
return [
|
|
{
|
|
label: 'At least 8 characters long',
|
|
met: password.length >= 8
|
|
},
|
|
{
|
|
label: 'Contains uppercase letter',
|
|
met: /[A-Z]/.test(password)
|
|
},
|
|
{
|
|
label: 'Contains lowercase letter',
|
|
met: /[a-z]/.test(password)
|
|
},
|
|
{
|
|
label: 'Contains number',
|
|
met: /\d/.test(password)
|
|
},
|
|
{
|
|
label: 'Contains special character (!@#$%^&*)',
|
|
met: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)
|
|
}
|
|
];
|
|
};
|
|
|
|
const passwordRequirements = getPasswordRequirements(registerForm.password);
|
|
const passwordsMatch = registerForm.password === registerForm.confirmPassword;
|
|
const hasPasswordMatchError = registerForm.confirmPassword.length > 0 && !passwordsMatch;
|
|
|
|
useEffect(() => {
|
|
if (phone !== registerForm.phone && phone) {
|
|
console.log({ phone });
|
|
setRegisterForm({ ...registerForm, phone });
|
|
}
|
|
}, [phone, registerForm]);
|
|
|
|
useEffect(() => {
|
|
if (!loading || !error) {
|
|
return;
|
|
}
|
|
if (loading && error) {
|
|
/* Remove 'HTTP .*: ' from error string */
|
|
const jsonStr = error.replace(/^[^{]*/, '');
|
|
const data = JSON.parse(jsonStr);
|
|
setErrorMessage(data.error.message);
|
|
setSnack(data.error.message, "error");
|
|
setTimeout(() => {
|
|
setErrorMessage(null);
|
|
setLoading(false);
|
|
}, 3000);
|
|
}
|
|
}, [error, loading]);
|
|
|
|
const handleLogin = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setSuccess(null);
|
|
|
|
const success = await login(loginForm);
|
|
if (success) {
|
|
setSuccess('Login successful!');
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handlePasswordChange = (password: string) => {
|
|
setRegisterForm(prev => ({ ...prev, password }));
|
|
setPasswordValidation(apiClient.validatePasswordStrength(password));
|
|
|
|
// Show requirements if password has content and isn't valid
|
|
if (password.length > 0) {
|
|
const requirements = getPasswordRequirements(password);
|
|
const allMet = requirements.every(req => req.met);
|
|
if (!allMet && !showPasswordRequirements) {
|
|
setShowPasswordRequirements(true);
|
|
}
|
|
if (allMet && showPasswordRequirements) {
|
|
setShowPasswordRequirements(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleUserTypeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const userType = event.target.value as UserRegistrationType;
|
|
setRegisterForm(prev => ({ ...prev, userType }));
|
|
};
|
|
|
|
const handleRegister = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
// Check if employer registration is attempted
|
|
if (registerForm.userType === 'employer') {
|
|
setSnack('Employer registration is not yet supported. Please contact support for employer account setup.', "warning");
|
|
return;
|
|
}
|
|
|
|
// Validate passwords match
|
|
if (!passwordsMatch) {
|
|
return;
|
|
}
|
|
|
|
// Validate password requirements
|
|
const allRequirementsMet = passwordRequirements.every(req => req.met);
|
|
if (!allRequirementsMet) {
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setSuccess(null);
|
|
|
|
// For now, all non-employer registrations go through candidate creation
|
|
// This would need to be updated when employer APIs are available
|
|
let success;
|
|
switch (registerForm.userType) {
|
|
case 'candidate':
|
|
success = await createCandidateAccount(registerForm);
|
|
break;
|
|
}
|
|
|
|
if (success) {
|
|
// Redirect based on user type
|
|
if (registerForm.userType === 'candidate') {
|
|
window.location.href = '/candidate/dashboard';
|
|
}
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
|
setTabValue(newValue);
|
|
setSuccess(null);
|
|
};
|
|
|
|
// Toggle password visibility functions
|
|
const toggleLoginPasswordVisibility = () => setShowLoginPassword(!showLoginPassword);
|
|
const toggleRegisterPasswordVisibility = () => setShowRegisterPassword(!showRegisterPassword);
|
|
const toggleConfirmPasswordVisibility = () => setShowConfirmPassword(!showConfirmPassword);
|
|
|
|
// Get user type icon and description
|
|
const getUserTypeInfo = (userType: UserRegistrationType) => {
|
|
switch (userType) {
|
|
case 'candidate':
|
|
return {
|
|
icon: <Person />,
|
|
title: 'Candidate',
|
|
description: 'Use Backstory to generate your resume and help you manage your career. Optionally let people interact with your profile.'
|
|
};
|
|
case 'employer':
|
|
return {
|
|
icon: <Business />,
|
|
title: 'Employer',
|
|
description: 'Post jobs and find talent (Coming Soon.)'
|
|
};
|
|
}
|
|
};
|
|
|
|
// If user is logged in, show their profile
|
|
if (user) {
|
|
return (
|
|
<Container maxWidth="md" sx={{ mt: 4 }}>
|
|
<Card elevation={3}>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
|
|
<Avatar sx={{ mr: 2, bgcolor: 'primary.main' }}>
|
|
<AccountCircle />
|
|
</Avatar>
|
|
<Typography variant="h4" component="h1">
|
|
User Profile
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Divider sx={{ mb: 3 }} />
|
|
|
|
<Grid container spacing={3}>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Username:</strong> {name}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Email:</strong> {user.email}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
{/* <strong>Status:</strong> {user.status} */}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Phone:</strong> {user.phone || 'Not provided'}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Account type:</strong> {user.userType}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Last Login:</strong> {
|
|
user.lastLogin
|
|
? user.lastLogin.toLocaleString()
|
|
: 'N/A'
|
|
}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Member Since:</strong> {user.createdAt.toLocaleDateString()}
|
|
</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
const validateInput = (value: string) => {
|
|
if (!value) return 'This field is required';
|
|
|
|
// Username: alphanumeric, 3-20 characters, no @
|
|
const usernameRegex = /^[a-zA-Z0-9]{3,20}$/;
|
|
// Email: basic email format
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
if (usernameRegex.test(value)) return '';
|
|
if (emailRegex.test(value)) return '';
|
|
return 'Enter a valid username (3-20 alphanumeric characters) or email';
|
|
};
|
|
|
|
const handleLoginChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const { value } = event.target;
|
|
setLoginForm({ ...loginForm, login: value });
|
|
};
|
|
|
|
return (
|
|
<Container maxWidth="sm" sx={{ mt: 4 }}>
|
|
<Paper elevation={3} sx={{ p: 4 }}>
|
|
<Typography variant="h4" component="h1" gutterBottom align="center" color="primary">
|
|
Backstory
|
|
</Typography>
|
|
|
|
{guest && (
|
|
<Card sx={{ mb: 3, bgcolor: 'grey.50' }} elevation={1}>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom color="primary">
|
|
Guest Session Active
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
|
|
Session ID: {guest.sessionId}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Created: {guest.createdAt.toLocaleString()}
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
|
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
|
<Tab icon={<Person />} label="Login" />
|
|
<Tab icon={<PersonAdd />} label="Register" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
{errorMessage && (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{errorMessage}
|
|
</Alert>
|
|
)}
|
|
|
|
{success && (
|
|
<Alert severity="success" sx={{ mb: 2 }}>
|
|
{success}
|
|
</Alert>
|
|
)}
|
|
|
|
{tabValue === 0 && (
|
|
<Box component="form" onSubmit={handleLogin}>
|
|
<Typography variant="h5" gutterBottom>
|
|
Sign In
|
|
</Typography>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Username or Email"
|
|
type="text"
|
|
value={loginForm.login}
|
|
onChange={handleLoginChange}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
placeholder="Enter username or email"
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Password"
|
|
type={showLoginPassword ? 'text' : 'password'}
|
|
value={loginForm.password}
|
|
onChange={(e) => setLoginForm({ ...loginForm, password: e.target.value })}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
autoComplete='current-password'
|
|
slotProps={{
|
|
input: {
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
aria-label="toggle password visibility"
|
|
onClick={toggleLoginPasswordVisibility}
|
|
edge="end"
|
|
disabled={loading}
|
|
>
|
|
{showLoginPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
)
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
type="submit"
|
|
fullWidth
|
|
variant="contained"
|
|
sx={{ mt: 3, mb: 2 }}
|
|
disabled={loading}
|
|
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <Person />}
|
|
>
|
|
{loading ? 'Signing In...' : 'Sign In'}
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
|
|
{tabValue === 1 && (
|
|
<Box component="form" onSubmit={handleRegister}>
|
|
<Typography variant="h5" gutterBottom>
|
|
Create Account
|
|
</Typography>
|
|
|
|
{/* User Type Selection */}
|
|
<FormControl component="fieldset" sx={{ mb: 3, width: '100%' }}>
|
|
<FormLabel component="legend" sx={{ mb: 2 }}>
|
|
<Typography variant="h6">Select Account Type</Typography>
|
|
</FormLabel>
|
|
<RadioGroup
|
|
value={registerForm.userType}
|
|
onChange={handleUserTypeChange}
|
|
sx={{ gap: 1 }}
|
|
>
|
|
{(['candidate', 'employer'] as UserRegistrationType[]).map((userType) => {
|
|
const info = getUserTypeInfo(userType);
|
|
return (
|
|
<FormControlLabel
|
|
key={userType}
|
|
value={userType}
|
|
disabled={loading || userType === 'employer'}
|
|
control={<Radio />}
|
|
label={
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
|
|
{info.icon}
|
|
<Box>
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 'medium' }}>
|
|
{info.title}
|
|
{userType === 'employer' && (
|
|
<Chip
|
|
label="Coming Soon"
|
|
size="small"
|
|
color="warning"
|
|
sx={{ ml: 1 }}
|
|
/>
|
|
)}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{info.description}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
}
|
|
sx={{
|
|
border: '1px solid',
|
|
borderColor: registerForm.userType === userType ? 'primary.main' : 'divider',
|
|
borderRadius: 1,
|
|
p: 1,
|
|
m: 0,
|
|
bgcolor: registerForm.userType === userType ? 'primary.50' : 'transparent',
|
|
'&:hover': {
|
|
bgcolor: userType === 'employer' ? 'grey.100' : 'action.hover'
|
|
},
|
|
opacity: userType === 'employer' ? 0.6 : 1
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</RadioGroup>
|
|
</FormControl>
|
|
|
|
{/* Employer Placeholder */}
|
|
{registerForm.userType === 'employer' && (
|
|
<Alert severity="info" sx={{ mb: 3 }}>
|
|
<Typography variant="h6" gutterBottom>
|
|
Employer Registration Coming Soon
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
We're currently building our employer features. If you're interested in posting jobs
|
|
and finding talent, please contact our support team at support@backstory.com for
|
|
early access.
|
|
</Typography>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Basic Information Fields */}
|
|
{registerForm.userType !== 'employer' && (
|
|
<>
|
|
<Grid container spacing={2} sx={{ mb: 2 }}>
|
|
<Grid size={{ xs: 12, sm: 6 }}>
|
|
<TextField
|
|
fullWidth
|
|
label="First Name"
|
|
value={registerForm.firstName}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, firstName: e.target.value })}
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid size={{ xs: 12, sm: 6 }}>
|
|
<TextField
|
|
fullWidth
|
|
label="Last Name"
|
|
value={registerForm.lastName}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, lastName: e.target.value })}
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Username"
|
|
value={registerForm.username}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, username: e.target.value })}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Email"
|
|
type="email"
|
|
value={registerForm.email}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, email: e.target.value })}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
/>
|
|
|
|
{/* Conditional fields based on user type */}
|
|
{registerForm.userType === 'candidate' && (
|
|
<>
|
|
<PhoneInput
|
|
label="Phone (Optional)"
|
|
placeholder="Enter phone number"
|
|
defaultCountry='US'
|
|
value={registerForm.phone}
|
|
disabled={loading}
|
|
onChange={(v) => setPhone(v as E164Number)}
|
|
/>
|
|
|
|
<LocationInput
|
|
value={location}
|
|
onChange={handleLocationChange}
|
|
showCity
|
|
helperText="Include your city for more specific job matches"
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Password"
|
|
type={showRegisterPassword ? 'text' : 'password'}
|
|
value={registerForm.password}
|
|
onChange={(e) => handlePasswordChange(e.target.value)}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
slotProps={{
|
|
input: {
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
aria-label="toggle password visibility"
|
|
onClick={toggleRegisterPasswordVisibility}
|
|
edge="end"
|
|
disabled={loading}
|
|
>
|
|
{showRegisterPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
)
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{/* Password Requirements */}
|
|
{registerForm.password.length > 0 && (
|
|
<Box sx={{ mt: 1, mb: 1 }}>
|
|
<Button
|
|
onClick={() => setShowPasswordRequirements(!showPasswordRequirements)}
|
|
startIcon={showPasswordRequirements ? <ExpandLess /> : <ExpandMore />}
|
|
size="small"
|
|
sx={{ mb: 1 }}
|
|
>
|
|
Password Requirements
|
|
</Button>
|
|
<Collapse in={showPasswordRequirements}>
|
|
<Paper variant="outlined" sx={{ p: 2 }}>
|
|
<List dense>
|
|
{passwordRequirements.map((requirement, index) => (
|
|
<ListItem key={index} sx={{ py: 0.5 }}>
|
|
<ListItemIcon sx={{ minWidth: 36 }}>
|
|
{requirement.met ? (
|
|
<CheckCircle color="success" fontSize="small" />
|
|
) : (
|
|
<Cancel color="error" fontSize="small" />
|
|
)}
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary={requirement.label}
|
|
sx={{
|
|
'& .MuiListItemText-primary': {
|
|
fontSize: '0.875rem',
|
|
color: requirement.met ? 'success.main' : 'error.main'
|
|
}
|
|
}}
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</Paper>
|
|
</Collapse>
|
|
</Box>
|
|
)}
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Confirm Password"
|
|
type={showConfirmPassword ? 'text' : 'password'}
|
|
value={registerForm.confirmPassword}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, confirmPassword: e.target.value })}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
error={hasPasswordMatchError}
|
|
helperText={hasPasswordMatchError ? 'Passwords do not match' : ''}
|
|
slotProps={{
|
|
input: {
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
aria-label="toggle confirm password visibility"
|
|
onClick={toggleConfirmPasswordVisibility}
|
|
edge="end"
|
|
disabled={loading}
|
|
>
|
|
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
)
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
type="submit"
|
|
fullWidth
|
|
variant="contained"
|
|
sx={{ mt: 3, mb: 2 }}
|
|
disabled={loading || hasPasswordMatchError || !passwordRequirements.every(req => req.met)}
|
|
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <PersonAdd />}
|
|
>
|
|
{loading ? 'Creating Account...' : `Create ${getUserTypeInfo(registerForm.userType).title} Account`}
|
|
</Button>
|
|
</>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Paper>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export { LoginPage }; |