NeonClary
Restore cybersecurity user profile UX and personalize advisor responses
6004480
Raw
History Blame Contribute Delete
15.5 kB
import React, { useState } from 'react';
import { Eye, EyeOff, Mail, Lock, User, ArrowRight, Shield, Phone, Globe } from 'lucide-react';
import { useAppConfig } from '../contexts/AppConfigContext';
import '../styles/Signup.css';
const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
const { config } = useAppConfig();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
academicStage: '',
researchArea: ''
});
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState({});
const knowledgeLevels = config?.login?.knowledge_levels?.length
? config.login.knowledge_levels
: config?.login?.academic_stages?.length
? config.login.academic_stages
: [
{ value: '', label: 'Select your cybersecurity knowledge level' },
{ value: 'newcomer', label: 'New to cybersecurity' },
{ value: 'foundational', label: 'Foundational' },
{ value: 'practitioner', label: 'Practitioner' },
{ value: 'experienced', label: 'Experienced' },
{ value: 'expert', label: 'Expert / specialist' },
];
const timezones = config?.login?.timezones?.length
? config.login.timezones
: [{ value: '', label: 'Select timezone (optional)' }];
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
const validateForm = () => {
const newErrors = {};
if (!formData.firstName.trim()) {
newErrors.firstName = 'First name is required';
}
if (!formData.lastName.trim()) {
newErrors.lastName = 'Last name is required';
}
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Please enter a valid email address';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(formData.password)) {
newErrors.password = 'Password must contain uppercase, lowercase, and number';
}
if (!formData.confirmPassword) {
newErrors.confirmPassword = 'Please confirm your password';
} else if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
if (!formData.academicStage) {
newErrors.academicStage = 'Please select your cybersecurity knowledge level';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
try {
const response = await fetch(`${process.env.REACT_APP_API_URL}/auth/signup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
password: formData.password,
academicStage: formData.academicStage,
researchArea: formData.researchArea
}),
});
const data = await response.json();
if (response.ok) {
// Store the token and user info
localStorage.setItem('authToken', data.access_token);
localStorage.setItem('user', JSON.stringify(data.user));
onNavigateToHome?.(data.user, data.access_token);
} else {
setErrors({ submit: data.detail || 'Signup failed. Please try again.' });
}
} catch (error) {
console.error('Signup error:', error);
setErrors({ submit: 'Signup failed. Please try again.' });
} finally {
setIsLoading(false);
}
};
const handleGoogleSignUp = () => {
console.log('Google Sign Up clicked');
// Future Google Auth integration will go here
};
const handlePhoneSignUp = () => {
console.log('Phone Sign Up clicked');
// Future Phone Auth integration will go here
};
return (
<div className="signup-page">
<div className="signup-container">
{/* Header */}
<div className="signup-header">
<div className="logo-container">
<Shield className="logo-icon" />
</div>
<h1 className="signup-title">Join Our Community</h1>
<p className="signup-subtitle">
{config?.login?.signup_subtitle || 'Create your account to get personalized guidance from expert advisors'}
</p>
</div>
{/* Main Signup Form */}
<div className="signup-form-container">
<form onSubmit={handleSubmit} className="signup-form">
{/* Name Fields Row */}
<div className="form-row">
<div className="form-group">
<label htmlFor="firstName" className="form-label">
First Name
</label>
<div className="input-container">
<User className="input-icon" />
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
className={`form-input ${errors.firstName ? 'error' : ''}`}
placeholder="First name"
disabled={isLoading}
/>
</div>
{errors.firstName && (
<span className="error-message">{errors.firstName}</span>
)}
</div>
<div className="form-group">
<label htmlFor="lastName" className="form-label">
Last Name
</label>
<div className="input-container">
<User className="input-icon" />
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
className={`form-input ${errors.lastName ? 'error' : ''}`}
placeholder="Last name"
disabled={isLoading}
/>
</div>
{errors.lastName && (
<span className="error-message">{errors.lastName}</span>
)}
</div>
</div>
{/* Email Field */}
<div className="form-group">
<label htmlFor="email" className="form-label">
Email Address
</label>
<div className="input-container">
<Mail className="input-icon" />
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className={`form-input ${errors.email ? 'error' : ''}`}
placeholder="Enter your email"
disabled={isLoading}
/>
</div>
{errors.email && (
<span className="error-message">{errors.email}</span>
)}
</div>
{/* Password Fields Row */}
<div className="form-row">
<div className="form-group">
<label htmlFor="password" className="form-label">
Password
</label>
<div className="input-container">
<Lock className="input-icon" />
<input
type={showPassword ? 'text' : 'password'}
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
className={`form-input ${errors.password ? 'error' : ''}`}
placeholder="Create password"
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="password-toggle"
disabled={isLoading}
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
{errors.password && (
<span className="error-message">{errors.password}</span>
)}
</div>
<div className="form-group">
<label htmlFor="confirmPassword" className="form-label">
Confirm Password
</label>
<div className="input-container">
<Lock className="input-icon" />
<input
type={showConfirmPassword ? 'text' : 'password'}
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
className={`form-input ${errors.confirmPassword ? 'error' : ''}`}
placeholder="Confirm password"
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="password-toggle"
disabled={isLoading}
>
{showConfirmPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
{errors.confirmPassword && (
<span className="error-message">{errors.confirmPassword}</span>
)}
</div>
</div>
{/* Cybersecurity knowledge level */}
<div className="form-group">
<label htmlFor="academicStage" className="form-label">
Cybersecurity knowledge level
</label>
<div className="input-container">
<Shield className="input-icon" />
<select
id="academicStage"
name="academicStage"
value={formData.academicStage}
onChange={handleInputChange}
className={`form-select ${errors.academicStage ? 'error' : ''}`}
disabled={isLoading}
>
{knowledgeLevels.map(stage => (
<option key={stage.value} value={stage.value}>
{stage.label}
</option>
))}
</select>
</div>
{errors.academicStage && (
<span className="error-message">{errors.academicStage}</span>
)}
</div>
{/* Time zone (optional) */}
<div className="form-group">
<label htmlFor="researchArea" className="form-label">
Time zone <span className="optional">(Optional)</span>
</label>
<div className="input-container">
<Globe className="input-icon" />
<select
id="researchArea"
name="researchArea"
value={formData.researchArea}
onChange={handleInputChange}
className="form-select"
disabled={isLoading}
>
{timezones.map(tz => (
<option key={tz.value} value={tz.value}>
{tz.label}
</option>
))}
</select>
</div>
</div>
{/* Terms and Privacy */}
<div className="terms-section">
<p className="terms-text">
By creating an account, you agree to our{' '}
<button type="button" className="link-btn">Terms of Service</button>
{' '}and{' '}
<button type="button" className="link-btn">Privacy Policy</button>
</p>
</div>
{/* Submit Error */}
{errors.submit && (
<div className="submit-error">
{errors.submit}
</div>
)}
{/* Submit Button */}
<button
type="submit"
className={`submit-btn ${isLoading ? 'loading' : ''}`}
disabled={isLoading}
>
{isLoading ? (
<>
<div className="loading-spinner"></div>
Creating Account...
</>
) : (
<>
Create Account
<ArrowRight size={16} />
</>
)}
</button>
</form>
{/* Divider */}
<div className="divider">
<span>or sign up with</span>
</div>
{/* Social Signup Options */}
<div className="social-signup">
<button
type="button"
className="google-btn"
onClick={handleGoogleSignUp}
disabled={isLoading}
>
<svg className="google-icon" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Continue with Google
</button>
<button
type="button"
className="phone-btn"
onClick={handlePhoneSignUp}
disabled={isLoading}
>
<Phone size={16} />
Continue with Phone
</button>
</div>
</div>
{/* Footer */}
<div className="signup-footer">
<p>
Already have an account?{' '}
<button
type="button"
className="link-btn"
onClick={onNavigateToLogin}
>
Sign in here
</button>
</p>
</div>
</div>
</div>
);
};
export default Signup;