source: chapterx-frontend/src/pages/auth/RegisterPage.tsx@ b62cefc

main
Last change on this file since b62cefc was b62cefc, checked in by kikisrbinoska <srbinoskakristina07@…>, 4 months ago

Added frontend and imporoved AI Suggestions service

  • Property mode set to 100644
File size: 10.2 KB
RevLine 
[b62cefc]1import React, { useState } from 'react'
2import { useNavigate, Link } from 'react-router-dom'
3import { BookOpen, PenLine, ArrowLeft, Eye, EyeOff } from 'lucide-react'
4import logo from '../../assets/chapterX-removebg-preview.png'
5import { useAuthStore } from '../../store/authStore'
6import { useUIStore } from '../../store/uiStore'
7import { Button } from '../../components/ui/Button'
8
9type Role = 'regular' | 'writer'
10type Step = 1 | 2
11
12export 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}
Note: See TracBrowser for help on using the repository browser.