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 = (props: BackstoryPageProps) => { const { setSnack } = props; const [tabValue, setTabValue] = useState(0); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(null); const [phone, setPhone] = useState(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>({}); const [errorMessage, setErrorMessage] = useState(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) => { setLocation(location); console.log('Location updated:', location); }; // Login form state const [loginForm, setLoginForm] = useState({ login: '', password: '' }); // Register form state const [registerForm, setRegisterForm] = useState({ 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) => { 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: , 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: , title: 'Employer', description: 'Post jobs and find talent (Coming Soon.)' }; } }; // If user is logged in, show their profile if (user) { return ( User Profile Username: {name} Email: {user.email} {/* Status: {user.status} */} Phone: {user.phone || 'Not provided'} Account type: {user.userType} Last Login: { user.lastLogin ? user.lastLogin.toLocaleString() : 'N/A' } Member Since: {user.createdAt.toLocaleDateString()} ); } 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) => { const { value } = event.target; setLoginForm({ ...loginForm, login: value }); }; return ( Backstory {guest && ( Guest Session Active Session ID: {guest.sessionId} Created: {guest.createdAt.toLocaleString()} )} } label="Login" /> } label="Register" /> {errorMessage && ( {errorMessage} )} {success && ( {success} )} {tabValue === 0 && ( Sign In setLoginForm({ ...loginForm, password: e.target.value })} margin="normal" required disabled={loading} variant="outlined" autoComplete='current-password' slotProps={{ input: { endAdornment: ( {showLoginPassword ? : } ) } }} /> )} {tabValue === 1 && ( Create Account {/* User Type Selection */} Select Account Type {(['candidate', 'employer'] as UserRegistrationType[]).map((userType) => { const info = getUserTypeInfo(userType); return ( } label={ {info.icon} {info.title} {userType === 'employer' && ( )} {info.description} } 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 }} /> ); })} {/* Employer Placeholder */} {registerForm.userType === 'employer' && ( Employer Registration Coming Soon 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. )} {/* Basic Information Fields */} {registerForm.userType !== 'employer' && ( <> setRegisterForm({ ...registerForm, firstName: e.target.value })} required disabled={loading} variant="outlined" /> setRegisterForm({ ...registerForm, lastName: e.target.value })} required disabled={loading} variant="outlined" /> setRegisterForm({ ...registerForm, username: e.target.value })} margin="normal" required disabled={loading} variant="outlined" /> setRegisterForm({ ...registerForm, email: e.target.value })} margin="normal" required disabled={loading} variant="outlined" /> {/* Conditional fields based on user type */} {registerForm.userType === 'candidate' && ( <> setPhone(v as E164Number)} /> )} handlePasswordChange(e.target.value)} margin="normal" required disabled={loading} variant="outlined" slotProps={{ input: { endAdornment: ( {showRegisterPassword ? : } ) } }} /> {/* Password Requirements */} {registerForm.password.length > 0 && ( {passwordRequirements.map((requirement, index) => ( {requirement.met ? ( ) : ( )} ))} )} setRegisterForm({ ...registerForm, confirmPassword: e.target.value })} margin="normal" required disabled={loading} variant="outlined" error={hasPasswordMatchError} helperText={hasPasswordMatchError ? 'Passwords do not match' : ''} slotProps={{ input: { endAdornment: ( {showConfirmPassword ? : } ) } }} /> )} )} ); }; export { LoginPage };