| [700e2f9] | 1 | import { type Component, createSignal, Show } from "solid-js";
|
|---|
| 2 | import { useNavigate } from "@solidjs/router";
|
|---|
| 3 | import { useAuth } from "@/context/AuthContext";
|
|---|
| 4 | import { validatePassword, validateUsername } from "@/utils/userValidators";
|
|---|
| 5 | import {
|
|---|
| 6 | authApi,
|
|---|
| 7 | type RegisterPatientRequest,
|
|---|
| 8 | type RegisterTherapistRequest,
|
|---|
| 9 | } from "@/api/auth";
|
|---|
| 10 |
|
|---|
| 11 | type UserType = "patient" | "therapist";
|
|---|
| 12 |
|
|---|
| 13 | interface RegisterProps {
|
|---|
| 14 | onSwitchToLogin: () => void;
|
|---|
| 15 | }
|
|---|
| 16 |
|
|---|
| 17 | const 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 |
|
|---|
| 374 | export default Register;
|
|---|