| [b62cefc] | 1 | import React, { useState } from 'react'
|
|---|
| 2 | import { useNavigate, Link } from 'react-router-dom'
|
|---|
| 3 | import { BookOpen, PenLine, ArrowLeft, Eye, EyeOff } from 'lucide-react'
|
|---|
| 4 | import logo from '../../assets/chapterX-removebg-preview.png'
|
|---|
| 5 | import { useAuthStore } from '../../store/authStore'
|
|---|
| 6 | import { useUIStore } from '../../store/uiStore'
|
|---|
| 7 | import { Button } from '../../components/ui/Button'
|
|---|
| 8 |
|
|---|
| 9 | type Role = 'regular' | 'writer'
|
|---|
| 10 | type Step = 1 | 2
|
|---|
| 11 |
|
|---|
| 12 | export const RegisterPage: React.FC = () => {
|
|---|
| 13 | const navigate = useNavigate()
|
|---|
| 14 | const { register } = useAuthStore()
|
|---|
| 15 | const { addToast } = useUIStore()
|
|---|
| 16 | const [step, setStep] = useState<Step>(1)
|
|---|
| 17 | const [role, setRole] = useState<Role>('regular')
|
|---|
| 18 | const [loading, setLoading] = useState(false)
|
|---|
| 19 | const [showPw, setShowPw] = useState(false)
|
|---|
| 20 | const [form, setForm] = useState({
|
|---|
| 21 | username: '',
|
|---|
| 22 | email: '',
|
|---|
| 23 | name: '',
|
|---|
| 24 | surname: '',
|
|---|
| 25 | password: '',
|
|---|
| 26 | confirmPassword: '',
|
|---|
| 27 | })
|
|---|
| 28 | const [errors, setErrors] = useState<Record<string, string>>({})
|
|---|
| 29 |
|
|---|
| 30 | const validate = () => {
|
|---|
| 31 | const e: Record<string, string> = {}
|
|---|
| 32 | if (!form.username.trim()) e.username = 'Username is required'
|
|---|
| 33 | if (form.username.length < 3) e.username = 'Username must be at least 3 characters'
|
|---|
| 34 | if (!form.email.includes('@')) e.email = 'Enter a valid email address'
|
|---|
| 35 | if (!form.name.trim()) e.name = 'First name is required'
|
|---|
| 36 | if (!form.surname.trim()) e.surname = 'Last name is required'
|
|---|
| 37 | if (form.password.length < 6) e.password = 'Password must be at least 6 characters'
|
|---|
| 38 | if (form.password !== form.confirmPassword) e.confirmPassword = 'Passwords do not match'
|
|---|
| 39 | setErrors(e)
|
|---|
| 40 | return Object.keys(e).length === 0
|
|---|
| 41 | }
|
|---|
| 42 |
|
|---|
| 43 | const handleSubmit = async (ev: React.FormEvent) => {
|
|---|
| 44 | ev.preventDefault()
|
|---|
| 45 | if (!validate()) return
|
|---|
| 46 | setLoading(true)
|
|---|
| 47 | try {
|
|---|
| 48 | await register(
|
|---|
| 49 | { username: form.username, email: form.email, name: form.name, surname: form.surname, password: form.password },
|
|---|
| 50 | role
|
|---|
| 51 | )
|
|---|
| 52 | addToast('Account created! Welcome to ChapterX.')
|
|---|
| 53 | navigate('/')
|
|---|
| 54 | } catch (err: any) {
|
|---|
| 55 | addToast(err.message || 'Registration failed', 'error')
|
|---|
| 56 | } finally {
|
|---|
| 57 | setLoading(false)
|
|---|
| 58 | }
|
|---|
| 59 | }
|
|---|
| 60 |
|
|---|
| 61 | const setField = (field: string, value: string) => {
|
|---|
| 62 | setForm(p => ({ ...p, [field]: value }))
|
|---|
| 63 | setErrors(p => ({ ...p, [field]: '' }))
|
|---|
| 64 | }
|
|---|
| 65 |
|
|---|
| 66 | return (
|
|---|
| 67 | <div className="min-h-[80vh] flex items-center justify-center px-4 py-12">
|
|---|
| 68 | <div className="w-full max-w-md">
|
|---|
| 69 | <div className="text-center mb-8">
|
|---|
| 70 | <Link to="/" className="inline-flex items-center gap-2 mb-6">
|
|---|
| 71 | <img src={logo} alt="ChapterX" className="h-20 w-20 object-contain" />
|
|---|
| 72 | </Link>
|
|---|
| 73 | <h1 className="font-serif text-2xl font-bold text-white">Create your account</h1>
|
|---|
| 74 | <p className="text-slate-400 text-sm mt-2">Step {step} of 2</p>
|
|---|
| 75 |
|
|---|
| 76 | {/* Progress */}
|
|---|
| 77 | <div className="flex gap-2 justify-center mt-4">
|
|---|
| 78 | <div className="h-1 w-16 rounded-full bg-indigo-600" />
|
|---|
| 79 | <div className={`h-1 w-16 rounded-full transition-colors ${step === 2 ? 'bg-indigo-600' : 'bg-slate-700'}`} />
|
|---|
| 80 | </div>
|
|---|
| 81 | </div>
|
|---|
| 82 |
|
|---|
| 83 | {step === 1 ? (
|
|---|
| 84 | <div className="space-y-4">
|
|---|
| 85 | <h2 className="text-white font-semibold text-center mb-6">I want to join as...</h2>
|
|---|
| 86 | <div className="grid grid-cols-1 gap-4">
|
|---|
| 87 | <button
|
|---|
| 88 | onClick={() => setRole('regular')}
|
|---|
| 89 | className={`flex items-start gap-4 p-5 rounded-2xl border-2 transition-all ${
|
|---|
| 90 | role === 'regular'
|
|---|
| 91 | ? 'border-indigo-500 bg-indigo-500/10'
|
|---|
| 92 | : 'border-slate-700 bg-slate-800 hover:border-slate-600'
|
|---|
| 93 | }`}
|
|---|
| 94 | >
|
|---|
| 95 | <div className={`w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
|---|
| 96 | role === 'regular' ? 'bg-indigo-500/30' : 'bg-slate-700'
|
|---|
| 97 | }`}>
|
|---|
| 98 | <BookOpen size={20} className="text-indigo-400" />
|
|---|
| 99 | </div>
|
|---|
| 100 | <div className="text-left">
|
|---|
| 101 | <p className="font-semibold text-white">Reader</p>
|
|---|
| 102 | <p className="text-slate-400 text-sm mt-1">
|
|---|
| 103 | Browse stories, leave comments, build reading lists, and follow your favorite authors.
|
|---|
| 104 | </p>
|
|---|
| 105 | </div>
|
|---|
| 106 | </button>
|
|---|
| 107 | <button
|
|---|
| 108 | onClick={() => setRole('writer')}
|
|---|
| 109 | className={`flex items-start gap-4 p-5 rounded-2xl border-2 transition-all ${
|
|---|
| 110 | role === 'writer'
|
|---|
| 111 | ? 'border-violet-500 bg-violet-500/10'
|
|---|
| 112 | : 'border-slate-700 bg-slate-800 hover:border-slate-600'
|
|---|
| 113 | }`}
|
|---|
| 114 | >
|
|---|
| 115 | <div className={`w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
|---|
| 116 | role === 'writer' ? 'bg-violet-500/30' : 'bg-slate-700'
|
|---|
| 117 | }`}>
|
|---|
| 118 | <PenLine size={20} className="text-violet-400" />
|
|---|
| 119 | </div>
|
|---|
| 120 | <div className="text-left">
|
|---|
| 121 | <p className="font-semibold text-white">Writer</p>
|
|---|
| 122 | <p className="text-slate-400 text-sm mt-1">
|
|---|
| 123 | Publish stories, write chapters, collaborate with others, and get AI writing assistance.
|
|---|
| 124 | </p>
|
|---|
| 125 | </div>
|
|---|
| 126 | </button>
|
|---|
| 127 | </div>
|
|---|
| 128 | <Button className="w-full mt-6" size="lg" onClick={() => setStep(2)}>
|
|---|
| 129 | Continue
|
|---|
| 130 | </Button>
|
|---|
| 131 | <p className="text-center text-slate-500 text-sm">
|
|---|
| 132 | Already have an account?{' '}
|
|---|
| 133 | <Link to="/login" className="text-indigo-400 hover:text-indigo-300">Sign in</Link>
|
|---|
| 134 | </p>
|
|---|
| 135 | </div>
|
|---|
| 136 | ) : (
|
|---|
| 137 | <form onSubmit={handleSubmit} className="space-y-4">
|
|---|
| 138 | <button
|
|---|
| 139 | type="button"
|
|---|
| 140 | onClick={() => setStep(1)}
|
|---|
| 141 | className="flex items-center gap-1 text-sm text-slate-400 hover:text-white mb-2"
|
|---|
| 142 | >
|
|---|
| 143 | <ArrowLeft size={14} />
|
|---|
| 144 | Back
|
|---|
| 145 | </button>
|
|---|
| 146 |
|
|---|
| 147 | <div className="grid grid-cols-2 gap-3">
|
|---|
| 148 | <div>
|
|---|
| 149 | <label className="block text-sm text-slate-400 mb-1.5">First Name</label>
|
|---|
| 150 | <input
|
|---|
| 151 | value={form.name}
|
|---|
| 152 | onChange={e => setField('name', e.target.value)}
|
|---|
| 153 | placeholder="Elena"
|
|---|
| 154 | className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 ${errors.name ? 'border-rose-500' : 'border-slate-700'}`}
|
|---|
| 155 | />
|
|---|
| 156 | {errors.name && <p className="text-rose-400 text-xs mt-1">{errors.name}</p>}
|
|---|
| 157 | </div>
|
|---|
| 158 | <div>
|
|---|
| 159 | <label className="block text-sm text-slate-400 mb-1.5">Last Name</label>
|
|---|
| 160 | <input
|
|---|
| 161 | value={form.surname}
|
|---|
| 162 | onChange={e => setField('surname', e.target.value)}
|
|---|
| 163 | placeholder="Dimitrova"
|
|---|
| 164 | className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 ${errors.surname ? 'border-rose-500' : 'border-slate-700'}`}
|
|---|
| 165 | />
|
|---|
| 166 | {errors.surname && <p className="text-rose-400 text-xs mt-1">{errors.surname}</p>}
|
|---|
| 167 | </div>
|
|---|
| 168 | </div>
|
|---|
| 169 |
|
|---|
| 170 | {[
|
|---|
| 171 | { field: 'username', label: 'Username', placeholder: 'elena_writes', type: 'text' },
|
|---|
| 172 | { field: 'email', label: 'Email', placeholder: 'you@example.com', type: 'email' },
|
|---|
| 173 | ].map(f => (
|
|---|
| 174 | <div key={f.field}>
|
|---|
| 175 | <label className="block text-sm text-slate-400 mb-1.5">{f.label}</label>
|
|---|
| 176 | <input
|
|---|
| 177 | type={f.type}
|
|---|
| 178 | value={(form as any)[f.field]}
|
|---|
| 179 | onChange={e => setField(f.field, e.target.value)}
|
|---|
| 180 | placeholder={f.placeholder}
|
|---|
| 181 | className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 ${(errors as any)[f.field] ? 'border-rose-500' : 'border-slate-700'}`}
|
|---|
| 182 | />
|
|---|
| 183 | {(errors as any)[f.field] && <p className="text-rose-400 text-xs mt-1">{(errors as any)[f.field]}</p>}
|
|---|
| 184 | </div>
|
|---|
| 185 | ))}
|
|---|
| 186 |
|
|---|
| 187 | <div>
|
|---|
| 188 | <label className="block text-sm text-slate-400 mb-1.5">Password</label>
|
|---|
| 189 | <div className="relative">
|
|---|
| 190 | <input
|
|---|
| 191 | type={showPw ? 'text' : 'password'}
|
|---|
| 192 | value={form.password}
|
|---|
| 193 | onChange={e => setField('password', e.target.value)}
|
|---|
| 194 | placeholder="Min. 6 characters"
|
|---|
| 195 | className={`w-full px-4 py-3 pr-12 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 ${errors.password ? 'border-rose-500' : 'border-slate-700'}`}
|
|---|
| 196 | />
|
|---|
| 197 | <button type="button" onClick={() => setShowPw(!showPw)} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-500 hover:text-slate-300">
|
|---|
| 198 | {showPw ? <EyeOff size={16} /> : <Eye size={16} />}
|
|---|
| 199 | </button>
|
|---|
| 200 | </div>
|
|---|
| 201 | {errors.password && <p className="text-rose-400 text-xs mt-1">{errors.password}</p>}
|
|---|
| 202 | </div>
|
|---|
| 203 |
|
|---|
| 204 | <div>
|
|---|
| 205 | <label className="block text-sm text-slate-400 mb-1.5">Confirm Password</label>
|
|---|
| 206 | <input
|
|---|
| 207 | type="password"
|
|---|
| 208 | value={form.confirmPassword}
|
|---|
| 209 | onChange={e => setField('confirmPassword', e.target.value)}
|
|---|
| 210 | placeholder="••••••••"
|
|---|
| 211 | className={`w-full px-4 py-3 bg-slate-800 border rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 ${errors.confirmPassword ? 'border-rose-500' : 'border-slate-700'}`}
|
|---|
| 212 | />
|
|---|
| 213 | {errors.confirmPassword && <p className="text-rose-400 text-xs mt-1">{errors.confirmPassword}</p>}
|
|---|
| 214 | </div>
|
|---|
| 215 |
|
|---|
| 216 | <Button type="submit" className="w-full" size="lg" loading={loading}>
|
|---|
| 217 | Create Account as {role === 'writer' ? 'Writer' : 'Reader'}
|
|---|
| 218 | </Button>
|
|---|
| 219 | </form>
|
|---|
| 220 | )}
|
|---|
| 221 | </div>
|
|---|
| 222 | </div>
|
|---|
| 223 | )
|
|---|
| 224 | }
|
|---|