backstory/frontend/src/pages/candidate/RegistrationForms.tsx
2025-06-18 14:26:07 -07:00

939 lines
29 KiB
TypeScript

import React, { useState } from "react";
import {
Paper,
Box,
Typography,
TextField,
Button,
Stack,
Alert,
Select,
MenuItem,
FormControl,
InputLabel,
FormHelperText,
LinearProgress,
CircularProgress,
Link,
Card,
CardContent,
CardActions,
useMediaQuery,
useTheme,
IconButton,
InputAdornment,
} from "@mui/material";
import { Visibility, VisibilityOff } from "@mui/icons-material";
import { ApiClient } from "services/api-client";
import { RegistrationSuccessDialog } from "components/EmailVerificationComponents";
import { useAuth } from "hooks/AuthContext";
import { useNavigate } from "react-router-dom";
// Candidate Registration Form
const CandidateRegistrationForm = () => {
const { apiClient } = useAuth();
const navigate = useNavigate();
const [formData, setFormData] = useState({
email: "",
username: "",
password: "",
confirmPassword: "",
firstName: "",
lastName: "",
phone: "",
});
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [showSuccess, setShowSuccess] = useState(false);
const [registrationResult, setRegistrationResult] = useState<any>(null);
// Password visibility states
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const validateForm = () => {
const newErrors: Record<string, string> = {};
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email) {
newErrors.email = "Email is required";
} else if (!emailRegex.test(formData.email)) {
newErrors.email = "Please enter a valid email address";
}
// Username validation
if (!formData.username) {
newErrors.username = "Username is required";
} else if (formData.username.length < 3) {
newErrors.username = "Username must be at least 3 characters";
} else if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) {
newErrors.username =
"Username can only contain letters, numbers, and underscores";
}
// Password validation
if (!formData.password) {
newErrors.password = "Password is required";
} else {
const passwordErrors = validatePassword(formData.password);
if (passwordErrors.length > 0) {
newErrors.password = passwordErrors.join(", ");
}
}
// Confirm password
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = "Passwords do not match";
}
// Name validation
if (!formData.firstName.trim()) {
newErrors.firstName = "First name is required";
}
if (!formData.lastName.trim()) {
newErrors.lastName = "Last name is required";
}
// Phone validation (optional but must be valid if provided)
if (
formData.phone &&
!/^[+]?[1-9][\d]{0,15}$/.test(formData.phone.replace(/\s/g, ""))
) {
newErrors.phone = "Please enter a valid phone number";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const validatePassword = (password: string): string[] => {
const errors: string[] = [];
if (password.length < 8) {
errors.push("at least 8 characters");
}
if (!/[a-z]/.test(password)) {
errors.push("one lowercase letter");
}
if (!/[A-Z]/.test(password)) {
errors.push("one uppercase letter");
}
if (!/\d/.test(password)) {
errors.push("one number");
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push("one special character");
}
return errors.length > 0
? [`Password must contain ${errors.join(", ")}`]
: [];
};
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" }));
}
};
const handleSubmit = async () => {
if (!validateForm()) {
return;
}
setLoading(true);
try {
const result = await apiClient.createCandidate({
email: formData.email,
username: formData.username,
password: formData.password,
firstName: formData.firstName,
lastName: formData.lastName,
phone: formData.phone || undefined,
});
// Set pending verification
apiClient.setPendingEmailVerification(formData.email);
setRegistrationResult(result);
setShowSuccess(true);
} catch (error: any) {
if (error.message.includes("already exists")) {
if (error.message.includes("email")) {
setErrors({ email: "An account with this email already exists" });
} else if (error.message.includes("username")) {
setErrors({ username: "This username is already taken" });
}
} else {
setErrors({
general: error.message || "Registration failed. Please try again.",
});
}
} finally {
setLoading(false);
}
};
const getPasswordStrength = (password: string) => {
const validations = [
password.length >= 8,
/[a-z]/.test(password),
/[A-Z]/.test(password),
/\d/.test(password),
/[!@#$%^&*(),.?":{}|<>]/.test(password),
];
const strength = validations.filter(Boolean).length;
if (strength < 2) return { level: "weak", color: "error", value: 20 };
if (strength < 4) return { level: "medium", color: "warning", value: 60 };
return { level: "strong", color: "success", value: 100 };
};
const passwordStrength = formData.password
? getPasswordStrength(formData.password)
: null;
return (
<Box sx={{ p: isMobile ? 1 : 5 }}>
<Box sx={{ textAlign: "center", mb: 4 }}>
<Typography variant="h4" component="h1" sx={{ mb: 1 }}>
Join as a Candidate
</Typography>
<Typography variant="body1" color="text.secondary">
Create your account to start finding your dream job
</Typography>
</Box>
<Stack spacing={3}>
<TextField
fullWidth
label="Email Address"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="your.email@example.com"
error={!!errors.email}
helperText={errors.email}
required
/>
<TextField
fullWidth
label="Username"
value={formData.username}
onChange={(e) =>
handleInputChange("username", e.target.value.toLowerCase())
}
placeholder="johndoe123"
error={!!errors.username}
helperText={errors.username}
required
/>
<Stack direction="row" spacing={2}>
<TextField
fullWidth
label="First Name"
value={formData.firstName}
onChange={(e) => handleInputChange("firstName", e.target.value)}
placeholder="John"
error={!!errors.firstName}
helperText={errors.firstName}
required
/>
<TextField
fullWidth
label="Last Name"
value={formData.lastName}
onChange={(e) => handleInputChange("lastName", e.target.value)}
placeholder="Doe"
error={!!errors.lastName}
helperText={errors.lastName}
required
/>
</Stack>
<TextField
fullWidth
label="Phone Number"
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)}
placeholder="+1 (555) 123-4567"
error={!!errors.phone}
helperText={errors.phone || "Optional"}
/>
<Box>
<TextField
fullWidth
label="Password"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => handleInputChange("password", e.target.value)}
placeholder="Create a strong password"
error={!!errors.password}
helperText={errors.password}
required
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)}
onMouseDown={(e) => e.preventDefault()}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
{formData.password && passwordStrength && (
<Box sx={{ mt: 1 }}>
<LinearProgress
variant="determinate"
value={passwordStrength.value}
color={passwordStrength.color as any}
sx={{ height: 6, borderRadius: 3 }}
/>
<Typography
variant="caption"
color={`${passwordStrength.color}.main`}
sx={{ mt: 0.5, display: "block", textTransform: "capitalize" }}
>
Password strength: {passwordStrength.level}
</Typography>
</Box>
)}
</Box>
<TextField
fullWidth
label="Confirm Password"
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmPassword}
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
placeholder="Confirm your password"
error={!!errors.confirmPassword}
helperText={errors.confirmPassword}
required
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle confirm password visibility"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
onMouseDown={(e) => e.preventDefault()}
edge="end"
>
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
{errors.general && <Alert severity="error">{errors.general}</Alert>}
<Button
fullWidth
variant="contained"
size="large"
onClick={handleSubmit}
disabled={loading}
sx={{ py: 2 }}
>
{loading ? (
<Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} color="inherit" />
<Typography>Creating Account...</Typography>
</Stack>
) : (
"Create Account"
)}
</Button>
<Box sx={{ textAlign: "center" }}>
<Typography variant="body2" color="text.secondary">
Already have an account?{" "}
<Link
component="button"
onClick={(e) => {
e.preventDefault();
navigate("/login");
}}
sx={{ fontWeight: 600 }}
>
Sign in here
</Link>
</Typography>
</Box>
</Stack>
{showSuccess && registrationResult && (
<RegistrationSuccessDialog
open={showSuccess}
onClose={() => setShowSuccess(false)}
email={registrationResult.email}
userType="candidate"
/>
)}
</Box>
);
};
// Employer Registration Form
const EmployerRegistrationForm = () => {
const [formData, setFormData] = useState({
email: "",
username: "",
password: "",
confirmPassword: "",
companyName: "",
industry: "",
companySize: "",
companyDescription: "",
websiteUrl: "",
phone: "",
});
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [showSuccess, setShowSuccess] = useState(false);
const [registrationResult, setRegistrationResult] = useState<any>(null);
// Password visibility states
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const apiClient = new ApiClient();
const industryOptions = [
"Technology",
"Healthcare",
"Finance",
"Education",
"Manufacturing",
"Retail",
"Consulting",
"Media",
"Non-profit",
"Government",
"Other",
];
const companySizeOptions = [
"1-10 employees",
"11-50 employees",
"51-200 employees",
"201-500 employees",
"501-1000 employees",
"1000+ employees",
];
const validateForm = () => {
const newErrors: Record<string, string> = {};
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email) {
newErrors.email = "Email is required";
} else if (!emailRegex.test(formData.email)) {
newErrors.email = "Please enter a valid email address";
}
// Username validation
if (!formData.username) {
newErrors.username = "Username is required";
} else if (formData.username.length < 3) {
newErrors.username = "Username must be at least 3 characters";
}
// Password validation
if (!formData.password) {
newErrors.password = "Password is required";
} else {
const passwordErrors = validatePassword(formData.password);
if (passwordErrors.length > 0) {
newErrors.password = passwordErrors.join(", ");
}
}
// Confirm password
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = "Passwords do not match";
}
// Company validation
if (!formData.companyName.trim()) {
newErrors.companyName = "Company name is required";
}
if (!formData.industry) {
newErrors.industry = "Industry is required";
}
if (!formData.companySize) {
newErrors.companySize = "Company size is required";
}
if (!formData.companyDescription.trim()) {
newErrors.companyDescription = "Company description is required";
} else if (formData.companyDescription.length < 50) {
newErrors.companyDescription =
"Company description must be at least 50 characters";
}
// Website URL validation (optional but must be valid if provided)
if (formData.websiteUrl) {
try {
new URL(formData.websiteUrl);
} catch {
newErrors.websiteUrl = "Please enter a valid website URL";
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const validatePassword = (password: string): string[] => {
const errors: string[] = [];
if (password.length < 8) {
errors.push("at least 8 characters");
}
if (!/[a-z]/.test(password)) {
errors.push("one lowercase letter");
}
if (!/[A-Z]/.test(password)) {
errors.push("one uppercase letter");
}
if (!/\d/.test(password)) {
errors.push("one number");
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push("one special character");
}
return errors.length > 0
? [`Password must contain ${errors.join(", ")}`]
: [];
};
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" }));
}
};
const handleSubmit = async () => {
if (!validateForm()) {
return;
}
setLoading(true);
try {
const result = await apiClient.createEmployerWithVerification({
email: formData.email,
username: formData.username,
password: formData.password,
companyName: formData.companyName,
industry: formData.industry,
companySize: formData.companySize,
companyDescription: formData.companyDescription,
websiteUrl: formData.websiteUrl || undefined,
phone: formData.phone || undefined,
});
// Set pending verification
apiClient.setPendingEmailVerification(formData.email);
setRegistrationResult(result);
setShowSuccess(true);
} catch (error: any) {
if (error.message.includes("already exists")) {
if (error.message.includes("email")) {
setErrors({ email: "An account with this email already exists" });
} else if (error.message.includes("username")) {
setErrors({ username: "This username is already taken" });
}
} else {
setErrors({
general: error.message || "Registration failed. Please try again.",
});
}
} finally {
setLoading(false);
}
};
return (
<Paper elevation={3}>
<Box sx={{ p: 5 }}>
<Box sx={{ textAlign: "center", mb: 4 }}>
<Typography variant="h4" component="h1" sx={{ mb: 1 }}>
Join as an Employer
</Typography>
<Typography variant="body1" color="text.secondary">
Create your company account to start hiring top talent
</Typography>
</Box>
<Stack spacing={4}>
{/* Account Information Section */}
<Box sx={{ bgcolor: "grey.50", p: 3, borderRadius: 2 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
Account Information
</Typography>
<Stack spacing={3}>
<Stack direction="row" spacing={2}>
<TextField
fullWidth
label="Email Address"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="company@example.com"
error={!!errors.email}
helperText={errors.email}
required
/>
<TextField
fullWidth
label="Username"
value={formData.username}
onChange={(e) =>
handleInputChange("username", e.target.value.toLowerCase())
}
placeholder="company123"
error={!!errors.username}
helperText={errors.username}
required
/>
</Stack>
<Stack direction="row" spacing={2}>
<TextField
fullWidth
label="Password"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) =>
handleInputChange("password", e.target.value)
}
placeholder="Create a strong password"
error={!!errors.password}
helperText={errors.password}
required
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)}
onMouseDown={(e) => e.preventDefault()}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
<TextField
fullWidth
label="Confirm Password"
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmPassword}
onChange={(e) =>
handleInputChange("confirmPassword", e.target.value)
}
placeholder="Confirm your password"
error={!!errors.confirmPassword}
helperText={errors.confirmPassword}
required
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle confirm password visibility"
onClick={() =>
setShowConfirmPassword(!showConfirmPassword)
}
onMouseDown={(e) => e.preventDefault()}
edge="end"
>
{showConfirmPassword ? (
<VisibilityOff />
) : (
<Visibility />
)}
</IconButton>
</InputAdornment>
),
}}
/>
</Stack>
</Stack>
</Box>
{/* Company Information Section */}
<Box sx={{ bgcolor: "primary.50", p: 3, borderRadius: 2 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
Company Information
</Typography>
<Stack spacing={3}>
<TextField
fullWidth
label="Company Name"
value={formData.companyName}
onChange={(e) =>
handleInputChange("companyName", e.target.value)
}
placeholder="Your Company Inc."
error={!!errors.companyName}
helperText={errors.companyName}
required
/>
<Stack direction="row" spacing={2}>
<FormControl fullWidth error={!!errors.industry} required>
<InputLabel>Industry</InputLabel>
<Select
value={formData.industry}
onChange={(e) =>
handleInputChange("industry", e.target.value)
}
label="Industry"
>
{industryOptions.map((industry) => (
<MenuItem key={industry} value={industry}>
{industry}
</MenuItem>
))}
</Select>
{errors.industry && (
<FormHelperText>{errors.industry}</FormHelperText>
)}
</FormControl>
<FormControl fullWidth error={!!errors.companySize} required>
<InputLabel>Company Size</InputLabel>
<Select
value={formData.companySize}
onChange={(e) =>
handleInputChange("companySize", e.target.value)
}
label="Company Size"
>
{companySizeOptions.map((size) => (
<MenuItem key={size} value={size}>
{size}
</MenuItem>
))}
</Select>
{errors.companySize && (
<FormHelperText>{errors.companySize}</FormHelperText>
)}
</FormControl>
</Stack>
<Box>
<TextField
fullWidth
label="Company Description"
multiline
rows={4}
value={formData.companyDescription}
onChange={(e) =>
handleInputChange("companyDescription", e.target.value)
}
placeholder="Tell us about your company, what you do, your mission, and what makes you unique..."
error={!!errors.companyDescription}
helperText={
errors.companyDescription ||
`${formData.companyDescription.length}/50 characters minimum`
}
required
/>
</Box>
<Stack direction="row" spacing={2}>
<TextField
fullWidth
label="Website URL"
type="url"
value={formData.websiteUrl}
onChange={(e) =>
handleInputChange("websiteUrl", e.target.value)
}
placeholder="https://www.yourcompany.com"
error={!!errors.websiteUrl}
helperText={errors.websiteUrl || "Optional"}
/>
<TextField
fullWidth
label="Phone Number"
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)}
placeholder="+1 (555) 123-4567"
error={!!errors.phone}
helperText={errors.phone || "Optional"}
/>
</Stack>
</Stack>
</Box>
{errors.general && <Alert severity="error">{errors.general}</Alert>}
<Button
fullWidth
variant="contained"
size="large"
onClick={handleSubmit}
disabled={loading}
sx={{ py: 2 }}
>
{loading ? (
<Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} color="inherit" />
<Typography>Creating Company Account...</Typography>
</Stack>
) : (
"Create Company Account"
)}
</Button>
<Box sx={{ textAlign: "center" }}>
<Typography variant="body2" color="text.secondary">
Already have an account?{" "}
<Link href="/login" sx={{ fontWeight: 600 }}>
Sign in here
</Link>
</Typography>
</Box>
</Stack>
</Box>
{showSuccess && registrationResult && (
<RegistrationSuccessDialog
open={showSuccess}
onClose={() => setShowSuccess(false)}
email={registrationResult.email}
userType="employer"
/>
)}
</Paper>
);
};
// Registration Type Selector Component
export function RegistrationTypeSelector() {
return (
<Paper elevation={3}>
<Box sx={{ p: 5 }}>
<Box sx={{ textAlign: "center", mb: 5 }}>
<Typography variant="h3" component="h1" sx={{ mb: 2 }}>
Join Backstory
</Typography>
<Typography variant="h6" color="text.secondary">
Choose how you'd like to get started
</Typography>
</Box>
<Stack direction="row" spacing={3}>
{/* Candidate Option */}
<Card
sx={{
flex: 1,
cursor: "pointer",
transition: "all 0.3s ease",
border: "2px solid transparent",
"&:hover": {
transform: "translateY(-4px)",
boxShadow: 6,
borderColor: "primary.main",
},
}}
onClick={() => (window.location.href = "/register/candidate")}
>
<CardContent sx={{ textAlign: "center", py: 4 }}>
<Typography variant="h1" sx={{ mb: 2 }}>
👤
</Typography>
<Typography variant="h5" component="h3" sx={{ mb: 1.5 }}>
I'm looking for work
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Create a candidate profile to find your next opportunity
</Typography>
</CardContent>
<CardActions sx={{ justifyContent: "center", pb: 3 }}>
<Button variant="contained" size="large">
Join as Candidate
</Button>
</CardActions>
</Card>
{/* Employer Option */}
<Card
sx={{
flex: 1,
cursor: "pointer",
transition: "all 0.3s ease",
border: "2px solid transparent",
"&:hover": {
transform: "translateY(-4px)",
boxShadow: 6,
borderColor: "primary.main",
},
}}
onClick={() => (window.location.href = "/register/employer")}
>
<CardContent sx={{ textAlign: "center", py: 4 }}>
<Typography variant="h1" sx={{ mb: 2 }}>
🏢
</Typography>
<Typography variant="h5" component="h3" sx={{ mb: 1.5 }}>
I'm hiring
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Create a company account to find and hire talent
</Typography>
</CardContent>
<CardActions sx={{ justifyContent: "center", pb: 3 }}>
<Button variant="contained" size="large">
Join as Employer
</Button>
</CardActions>
</Card>
</Stack>
<Box sx={{ textAlign: "center", mt: 4 }}>
<Typography variant="body2" color="text.secondary">
Already have an account?{" "}
<Link href="/login" sx={{ fontWeight: 600 }}>
Sign in here
</Link>
</Typography>
</Box>
</Box>
</Paper>
);
}
export { CandidateRegistrationForm, EmployerRegistrationForm };