Spaces:
Sleeping
Sleeping
| 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; |