source: frontend/src/components/Register.tsx

main
Last change on this file was 700e2f9, checked in by 186079 <matej.milevski@…>, 5 days ago

Init

  • Property mode set to 100644
File size: 12.4 KB
Line 
1import { type Component, createSignal, Show } from "solid-js";
2import { useNavigate } from "@solidjs/router";
3import { useAuth } from "@/context/AuthContext";
4import { validatePassword, validateUsername } from "@/utils/userValidators";
5import {
6 authApi,
7 type RegisterPatientRequest,
8 type RegisterTherapistRequest,
9} from "@/api/auth";
10
11type UserType = "patient" | "therapist";
12
13interface RegisterProps {
14 onSwitchToLogin: () => void;
15}
16
17const Register: Component<RegisterProps> = (props) => {
18 const [userType, setUserType] = createSignal<UserType>("patient");
19 const [error, setError] = createSignal("");
20 const [isLoading, setIsLoading] = createSignal(false);
21 const navigate = useNavigate();
22 const { login } = useAuth();
23
24 const [regUsername, setRegUsername] = createSignal("");
25 const [regPassword, setRegPassword] = createSignal("");
26 const [regConfirmPassword, setRegConfirmPassword] = createSignal("");
27 const [regName, setRegName] = createSignal("");
28 const [regSurname, setRegSurname] = createSignal("");
29 const [regEmail, setRegEmail] = createSignal("");
30
31 const [regOfficeLocation, setRegOfficeLocation] = createSignal("");
32 const [regDegree, setRegDegree] = createSignal("");
33 const [regYearsExp, setRegYearsExp] = createSignal("");
34 const [regPhoneNumber, setRegPhoneNumber] = createSignal("");
35
36 const handleRegister = async (e: Event) => {
37 e.preventDefault();
38 setError("");
39
40 const trimmedUsername = regUsername().trim();
41 const trimmedEmail = regEmail().trim();
42 const trimmedName = regName().trim();
43 const trimmedSurname = regSurname().trim();
44
45 const usernameValidation = validateUsername(trimmedUsername);
46 if (!usernameValidation.isValid) {
47 setError(usernameValidation.errors.join("\n"));
48 return;
49 }
50
51 if (regPassword() !== regConfirmPassword()) {
52 setError("Passwords do not match");
53 return;
54 }
55
56 const passwordValidation = validatePassword(regPassword());
57 if (!passwordValidation.isValid) {
58 setError(passwordValidation.errors.join("\n"));
59 return;
60 }
61
62 setIsLoading(true);
63
64 try {
65 let response;
66
67 if (userType() === "patient") {
68 const data: RegisterPatientRequest = {
69 username: trimmedUsername,
70 password: regPassword(),
71 name: trimmedName,
72 surname: trimmedSurname,
73 email: trimmedEmail,
74 };
75 response = await authApi.registerPatient(data);
76 } else {
77 const yearsExpNum = Number.parseInt(regYearsExp());
78 if (Number.isNaN(yearsExpNum) || yearsExpNum < 0) {
79 setError("Please enter a valid number of years of experience");
80 setIsLoading(false);
81 return;
82 }
83
84 const trimmedOfficeLocation = regOfficeLocation().trim();
85 const trimmedDegree = regDegree().trim();
86 const trimmedPhoneNumber = regPhoneNumber().trim();
87
88 const data: RegisterTherapistRequest = {
89 username: trimmedUsername,
90 password: regPassword(),
91 name: trimmedName,
92 surname: trimmedSurname,
93 email: trimmedEmail,
94 officeLocation: trimmedOfficeLocation,
95 degree: trimmedDegree,
96 yearsExp: yearsExpNum,
97 phoneNumber: trimmedPhoneNumber,
98 };
99 response = await authApi.registerTherapist(data);
100 }
101
102 login(response);
103 navigate("/");
104 } catch (err) {
105 setError(
106 err instanceof Error
107 ? err.message
108 : "Error has occurred during registration. Please try again.",
109 );
110 } finally {
111 setIsLoading(false);
112 }
113 };
114
115 return (
116 <div class="space-y-6">
117 <div class="flex rounded-lg border border-gray-300 p-1 bg-gray-50">
118 <button
119 type="button"
120 onClick={() => setUserType("patient")}
121 class={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
122 userType() === "patient"
123 ? "bg-white text-blue-600 shadow-sm"
124 : "text-gray-600 hover:text-gray-900"
125 }`}
126 >
127 Patient
128 </button>
129 <button
130 type="button"
131 onClick={() => setUserType("therapist")}
132 class={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
133 userType() === "therapist"
134 ? "bg-white text-blue-600 shadow-sm"
135 : "text-gray-600 hover:text-gray-900"
136 }`}
137 >
138 Therapist
139 </button>
140 </div>
141
142 <form class="space-y-4" onSubmit={handleRegister}>
143 {error() && (
144 <div class="rounded-md bg-red-50 p-4">
145 <div class="text-sm text-red-700 whitespace-pre-line">
146 {error()}
147 </div>
148 </div>
149 )}
150
151 <div class="grid grid-cols-2 gap-4">
152 <div>
153 <label
154 for="reg-name"
155 class="block text-sm font-medium text-gray-700 mb-1"
156 >
157 First Name
158 </label>
159 <input
160 id="reg-name"
161 type="text"
162 required
163 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
164 value={regName()}
165 onInput={(e) => setRegName(e.currentTarget.value)}
166 />
167 </div>
168 <div>
169 <label
170 for="reg-surname"
171 class="block text-sm font-medium text-gray-700 mb-1"
172 >
173 Last Name
174 </label>
175 <input
176 id="reg-surname"
177 type="text"
178 required
179 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
180 value={regSurname()}
181 onInput={(e) => setRegSurname(e.currentTarget.value)}
182 />
183 </div>
184 </div>
185
186 <div>
187 <label
188 for="reg-username"
189 class="block text-sm font-medium text-gray-700 mb-1"
190 >
191 Username
192 </label>
193 <input
194 id="reg-username"
195 type="text"
196 required
197 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
198 value={regUsername()}
199 onInput={(e) => setRegUsername(e.currentTarget.value)}
200 />
201 <p class="mt-1 text-xs text-gray-600">
202 3-50 characters. Letters, numbers, dots, hyphens, and underscores
203 only.
204 </p>
205 </div>
206
207 <div>
208 <label
209 for="reg-email"
210 class="block text-sm font-medium text-gray-700 mb-1"
211 >
212 Email
213 </label>
214 <input
215 id="reg-email"
216 type="email"
217 required
218 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
219 value={regEmail()}
220 onInput={(e) => setRegEmail(e.currentTarget.value)}
221 />
222 </div>
223
224 <div>
225 <label
226 for="reg-password"
227 class="block text-sm font-medium text-gray-700 mb-1"
228 >
229 Password
230 </label>
231 <input
232 id="reg-password"
233 type="password"
234 required
235 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
236 placeholder="Enter your password"
237 value={regPassword()}
238 onInput={(e) => setRegPassword(e.currentTarget.value)}
239 />
240 <div class="mt-2 text-xs text-gray-600 space-y-1">
241 <p class="font-medium">Password must contain:</p>
242 <ul class="list-disc list-inside space-y-0.5 ml-1">
243 <li>At least 8 characters (max 128)</li>
244 <li>One uppercase letter (A-Z)</li>
245 <li>One lowercase letter (a-z)</li>
246 <li>One number (0-9)</li>
247 <li>One special character (!@#$%^&*...)</li>
248 </ul>
249 </div>
250 </div>
251
252 <div>
253 <label
254 for="reg-confirm-password"
255 class="block text-sm font-medium text-gray-700 mb-1"
256 >
257 Confirm Password
258 </label>
259 <input
260 id="reg-confirm-password"
261 type="password"
262 required
263 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
264 value={regConfirmPassword()}
265 onInput={(e) => setRegConfirmPassword(e.currentTarget.value)}
266 />
267 </div>
268
269 <Show when={userType() === "therapist"}>
270 <div class="border-t border-gray-200 pt-4 space-y-4">
271 <h3 class="text-sm font-semibold text-gray-900">
272 Professional Information
273 </h3>
274
275 <div>
276 <label
277 for="reg-degree"
278 class="block text-sm font-medium text-gray-700 mb-1"
279 >
280 Degree / Qualification
281 </label>
282 <input
283 id="reg-degree"
284 type="text"
285 required
286 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
287 placeholder="e.g., PhD in Clinical Psychology"
288 value={regDegree()}
289 onInput={(e) => setRegDegree(e.currentTarget.value)}
290 />
291 </div>
292
293 <div>
294 <label
295 for="reg-years-exp"
296 class="block text-sm font-medium text-gray-700 mb-1"
297 >
298 Years of Experience
299 </label>
300 <input
301 id="reg-years-exp"
302 type="number"
303 required
304 min="0"
305 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
306 value={regYearsExp()}
307 onInput={(e) => setRegYearsExp(e.currentTarget.value)}
308 />
309 </div>
310
311 <div>
312 <label
313 for="reg-office"
314 class="block text-sm font-medium text-gray-700 mb-1"
315 >
316 Office Location
317 </label>
318 <input
319 id="reg-office"
320 type="text"
321 required
322 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
323 placeholder="e.g., 123 Main St, City"
324 value={regOfficeLocation()}
325 onInput={(e) => setRegOfficeLocation(e.currentTarget.value)}
326 />
327 </div>
328
329 <div>
330 <label
331 for="reg-phone"
332 class="block text-sm font-medium text-gray-700 mb-1"
333 >
334 Phone Number
335 </label>
336 <input
337 id="reg-phone"
338 type="tel"
339 required
340 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
341 placeholder="+1 (555) 123-4567"
342 value={regPhoneNumber()}
343 onInput={(e) => setRegPhoneNumber(e.currentTarget.value)}
344 />
345 </div>
346 </div>
347 </Show>
348
349 <div>
350 <button
351 type="submit"
352 disabled={isLoading()}
353 class="w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
354 >
355 {isLoading() ? "Creating account..." : "Create account"}
356 </button>
357 </div>
358
359 <div class="text-center text-sm">
360 <span class="text-gray-600">Already have an account? </span>
361 <button
362 type="button"
363 onClick={props.onSwitchToLogin}
364 class="font-medium text-blue-600 hover:text-blue-500"
365 >
366 Sign in
367 </button>
368 </div>
369 </form>
370 </div>
371 );
372};
373
374export default Register;
Note: See TracBrowser for help on using the repository browser.