From bdf51ba3da2d48b4311d09ab360ca266a9b48314 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Fri, 3 Jul 2026 17:51:16 +0530 Subject: [PATCH] feat(signup): create separate signup pages for each role - /signup/company - dedicated company registration page - /signup/job-seeker - dedicated job seeker registration page - /signup/professional - dedicated professional registration page - /signup - redirects to appropriate role-specific page - Removed tabs, each role has its own clean registration flow Fixes role assignment issues by having dedicated pages per role type. --- src/routes/signup.tsx | 860 ++--------------------------- src/routes/signup/company.tsx | 548 ++++++++++++++++++ src/routes/signup/job-seeker.tsx | 547 ++++++++++++++++++ src/routes/signup/professional.tsx | 586 ++++++++++++++++++++ 4 files changed, 1718 insertions(+), 823 deletions(-) create mode 100644 src/routes/signup/company.tsx create mode 100644 src/routes/signup/job-seeker.tsx create mode 100644 src/routes/signup/professional.tsx diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index 5a18224..b99638b 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -1,832 +1,46 @@ -import { A, useNavigate, useSearchParams } from "@solidjs/router"; -import { createMemo, createSignal, For, onMount, onCleanup, Show } from "solid-js"; -import PublicBackground from "~/components/PublicBackground"; -import PublicHeader from "~/components/PublicHeader"; -import CaptchaCanvas from "~/components/CaptchaCanvas"; -import { - checkPasswordStrength, - isPasswordStrong, - isValidCaptcha, - isValidEmail, - isValidName, - validateRegisterForm, -} from "~/lib/form-validation"; +import { useNavigate, useSearchParams } from "@solidjs/router"; +import { createEffect } from "solid-js"; -type RoleKey = "company" | "job_seeker" | "professional" | "customer"; - -type RegisterErrors = Record; - -function normalizeIntent(intent: string | null | undefined): RoleKey { - const v = String(intent || "").toLowerCase(); - if (v.includes("company")) return "company"; - if (v.includes("professional")) return "professional"; - if ( - v.includes("developer") || - v.includes("photographer") || - v.includes("makeup") || - v.includes("tutor") || - v.includes("video") || - v.includes("graphic") || - v.includes("social") || - v.includes("fitness") || - v.includes("catering") || - v.includes("ugc") - ) - return "professional"; - if (v.includes("customer")) return "customer"; - return "job_seeker"; -} - -function randomCaptcha(length = 6): string { - const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; - let out = ""; - for (let i = 0; i < length; i += 1) { - out += alphabet[Math.floor(Math.random() * alphabet.length)]; - } - return out; -} - -function PasswordVisibilityIcon(props: { visible: boolean }) { - if (props.visible) { - return ( - - ); - } - return ( - - ); -} - -export default function SignupRoute() { +/** + * Legacy signup route - redirects to role-specific signup pages + * + * This maintains backwards compatibility with old links: + * - /signup?intent=company → /signup/company + * - /signup?intent=job_seeker → /signup/job-seeker + * - /signup?intent=professional → /signup/professional + * - /signup?intent=customer → /signup/customer + * - /signup (no intent) → /signup/job-seeker (default) + */ +export default function SignupRedirectRoute() { const navigate = useNavigate(); const [search] = useSearchParams(); - - const [step, setStep] = createSignal<"register" | "verify">("register"); - const [firstName, setFirstName] = createSignal(""); - const [lastName, setLastName] = createSignal(""); - const [email, setEmail] = createSignal(""); - const [password, setPassword] = createSignal(""); - const [confirmPassword, setConfirmPassword] = createSignal(""); - const [selectedRole, setSelectedRole] = createSignal(normalizeIntent(search.intent || search.role)); - const role = createMemo(() => selectedRole()); - const selectedProfessionalRole = createMemo(() => - String(search.profession || search.role || "") - .trim() - .toUpperCase() - ); - const [termsAccepted, setTermsAccepted] = createSignal(false); - let termsRef: HTMLButtonElement | undefined; - const [companyName, setCompanyName] = createSignal(""); - const [captcha, setCaptcha] = createSignal(""); - const [captchaCode, setCaptchaCode] = createSignal(randomCaptcha()); - const [otp, setOtp] = createSignal(["", "", "", "", "", ""]); - const [errors, setErrors] = createSignal({}); - const [serverError, setServerError] = createSignal(""); - const [emailExists, setEmailExists] = createSignal(false); - const [submitting, setSubmitting] = createSignal(false); - const [pendingEmail, setPendingEmail] = createSignal(""); - const [verifiedSuccess, setVerifiedSuccess] = createSignal(false); - const [showPassword, setShowPassword] = createSignal(false); - const [showConfirmPassword, setShowConfirmPassword] = createSignal(false); - - const passwordChecks = createMemo(() => checkPasswordStrength(password(), confirmPassword())); - const otpCode = createMemo(() => otp().join("")); - const firstNameValid = createMemo(() => !firstName().trim() || isValidName(firstName())); - const lastNameValid = createMemo(() => !lastName().trim() || isValidName(lastName())); - const companyNameValid = createMemo(() => !companyName().trim() || companyName().trim().length >= 2); - const emailValid = createMemo(() => !email().trim() || isValidEmail(email())); - const canSubmit = createMemo( - () => - firstName().trim().length > 0 && - firstNameValid() && - (role() === "company" - ? companyName().trim().length > 0 && companyNameValid() - : lastName().trim().length > 0 && lastNameValid()) && - emailValid() && - isValidEmail(email()) && - isPasswordStrong(passwordChecks()) && - passwordChecks().match && - isValidCaptcha(captcha(), captchaCode()) && - termsAccepted() && - (!emailExists()) - ); - - const refreshCaptcha = () => { - setCaptcha(""); - setCaptchaCode(randomCaptcha()); - }; - - const checkEmailExists = async (emailValue: string) => { - const normalized = emailValue.trim().toLowerCase(); - if (!normalized || !isValidEmail(normalized)) { - setEmailExists(false); - return false; + createEffect(() => { + const intent = search.intent?.toLowerCase() || ""; + + if (intent.includes("company")) { + navigate("/signup/company", { replace: true }); + } else if (intent.includes("professional")) { + navigate("/signup/professional", { replace: true }); + } else if (intent.includes("customer")) { + navigate("/signup/customer", { replace: true }); + } else if (intent.includes("job_seeker") || intent.includes("jobseeker")) { + navigate("/signup/job-seeker", { replace: true }); + } else { + // Default to job seeker if no intent specified + navigate("/signup/job-seeker", { replace: true }); } - - try { - const response = await fetch("/api/gateway/auth/check-email", { - method: "POST", - headers: { "Content-Type": "application/json", Accept: "application/json" }, - credentials: "include", - body: JSON.stringify({ email: normalized }), - }); - const payload = await response.json().catch(() => ({})); - const exists = Boolean(response.ok && payload?.exists); - setEmailExists(exists); - return exists; - } catch { - setEmailExists(false); - return false; - } - }; - - const setOtpDigit = (index: number, value: string) => { - const clean = value.replace(/\D/g, "").slice(0, 1); - setOtp((prev) => { - const next = prev.slice(); - next[index] = clean; - return next; - }); - // Defer focus until after SolidJS reactive flush so the next input exists in DOM - queueMicrotask(() => { - if (clean && index < 5) { - const nextEl = document.querySelector(`#otp-${index + 1}`); - if (nextEl) nextEl.focus(); - } - }); - }; - - const saveUserForDashboard = (input: { - firstName: string; - lastName: string; - email: string; - roleKey: RoleKey; - user?: any; - companyName?: string; - }) => { - const isCompany = input.roleKey === "company"; - const displayName = isCompany ? (input.companyName || input.firstName) : `${input.firstName} ${input.lastName}`.trim(); - const payload = { - firstName: input.firstName, - lastName: isCompany ? "" : input.lastName, - fullName: displayName, - name: displayName, - displayName, - email: input.email.toLowerCase(), - roleKey: input.roleKey, - role: input.roleKey, - selectedProfessionalRole: selectedProfessionalRole() || null, - user: input.user || null, - ...(isCompany ? { companyName: input.companyName } : {}), - }; - if (typeof window !== "undefined") { - window.sessionStorage.setItem("nxtgauge_signup_profile_v1", JSON.stringify(payload)); - window.sessionStorage.setItem("nxtgauge_auth_user", JSON.stringify(payload)); - window.sessionStorage.setItem("nxtgauge_user", JSON.stringify(payload)); - } - }; - - const register = async () => { - console.log('[register] START'); - console.log('[register] canSubmit():', canSubmit()); - console.log('[register] testMode:', typeof window !== 'undefined' && window.__testMode === true); - setServerError(""); - const validation = validateRegisterForm({ - firstName: firstName(), - lastName: lastName(), - email: email(), - password: password(), - confirmPassword: confirmPassword(), - captcha: captcha(), - expectedCaptcha: captchaCode(), - termsAccepted: termsAccepted(), - }); - setErrors(validation.errors); - if (!validation.isValid) return; - - const isTestMode = typeof window !== "undefined" && window.__testMode === true; - - setSubmitting(true); - try { - console.log('[register] after canSubmit guard, calling API...'); - const res = await fetch("/api/gateway/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json", Accept: "application/json" }, - credentials: "include", - body: JSON.stringify({ - first_name: firstName().trim(), - last_name: role() === "company" ? companyName().trim() : lastName().trim(), - email: email().trim().toLowerCase(), - password: password(), - phone: "", - intent: role(), - role_key: selectedProfessionalRole() || undefined, - profession: selectedProfessionalRole() || undefined, - ...(role() === "company" ? { company_name: companyName().trim() } : {}), - ...(isTestMode ? { test_mode: true } : {}), - }), - }); - - const data = await res.json().catch(() => ({})); - console.log('[register] API response:', res.ok, res.status); - console.log('[register] data:', JSON.stringify(data)); - if (!res.ok) { - setServerError(String(data?.error || data?.message || "Unable to create account.")); - refreshCaptcha(); - return; - } - - // Check if account was created but email (SMTP) failed - const isSmtpError = data?.error === "SMTP_ERROR" || data?.code === "SMTP_ERROR"; - if (isSmtpError || isTestMode) { - const cleanEmail = email().trim().toLowerCase(); - setPendingEmail(cleanEmail); - setVerifiedSuccess(false); - saveUserForDashboard({ - firstName: firstName().trim(), - lastName: role() === "company" ? companyName().trim() : lastName().trim(), - email: cleanEmail, - roleKey: role(), - user: data?.user, - ...(role() === "company" ? { companyName: companyName().trim() } : {}), - }); - - setServerError( - isTestMode - ? "Test mode: Account created. Use OTP from Redis." - : "Email could not be sent. Your account was created — use the OTP stored in Redis for testing." - ); - setStep("verify"); - // Populate otp signal with digits from backend response (test_mode) - if (isTestMode && data?.otp) { - const digits = data.otp.split(""); - setOtp(digits); - } else { - setOtp(["", "", "", "", "", ""]); - } - console.log('[register] END - returning:', data); - return; - } - - const cleanEmail = email().trim().toLowerCase(); - setPendingEmail(cleanEmail); - setVerifiedSuccess(false); - saveUserForDashboard({ - firstName: firstName().trim(), - lastName: role() === "company" ? companyName().trim() : lastName().trim(), - email: cleanEmail, - roleKey: role(), - ...(role() === "company" ? { companyName: companyName().trim() } : {}), - }); - setStep("verify"); - // Populate otp signal with digits from backend response (test_mode) - if (isTestMode && data?.otp) { - const digits = data.otp.split(""); - setOtp(digits); - } else { - setOtp(["", "", "", "", "", ""]); - } - } catch (err) { - console.error("[register] fetch error:", err); - setServerError("Network error — please check your connection and try again."); - refreshCaptcha(); - } finally { - setSubmitting(false); - } - }; - - const verifyOtp = async () => { - setServerError(""); - if (otpCode().length !== 6) { - setServerError("Enter the 6-digit code sent to your email."); - return; - } - setSubmitting(true); - try { - const verifyRes = await fetch("/api/gateway/auth/verify-email", { - method: "POST", - headers: { "Content-Type": "application/json", Accept: "application/json" }, - credentials: "include", - body: JSON.stringify({ otp: otpCode() }), - }); - const verifyData = await verifyRes.json().catch(() => ({})); - if (!verifyRes.ok) { - setServerError(String(verifyData?.error || verifyData?.message || "Verification failed.")); - return; - } - setVerifiedSuccess(true); - // Redirect to role-specific dashboard instead of login page - try { - const stored = typeof window !== 'undefined' - ? JSON.parse(sessionStorage.getItem('nxtgauge_signup_profile_v1') || localStorage.getItem('nxtgauge_signup_profile_v1') || '{}') - : {}; - const roleKey = String(stored?.roleKey || stored?.role || 'JOB_SEEKER').toUpperCase(); - let dashRoute = '/dashboard?role=JOB_SEEKER'; - if (roleKey === 'COMPANY') dashRoute = '/dashboard?role=COMPANY'; - else if (roleKey === 'CUSTOMER') dashRoute = '/dashboard?role=CUSTOMER'; - else if (roleKey !== 'JOB_SEEKER') dashRoute = `/dashboard?role=${roleKey}`; - setTimeout(() => navigate(dashRoute, { replace: true }), 1400); - } catch { - setTimeout(() => navigate('/dashboard?role=JOB_SEEKER', { replace: true }), 1400); - } - } catch (err) { - console.error("[verifyOtp] fetch error:", err); - setServerError("Network error — please try again."); - } finally { - setSubmitting(false); - } - }; - - // Expose for testing - if (typeof window !== 'undefined') { - (window as any).__role = role; - (window as any).__setRole = setSelectedRole; - - (window as any).__companyName = companyName; - (window as any).__signupRegister = register; - (window as any).__signupVerifyOtp = verifyOtp; - (window as any).__setTermsAccepted = setTermsAccepted; - (window as any).__termsAccepted = termsAccepted; - // Add these: - (window as any).__setFirstName = setFirstName; - (window as any).__setLastName = setLastName; - (window as any).__setCompanyName = setCompanyName; - (window as any).__setEmail = setEmail; - (window as any).__setPassword = setPassword; - (window as any).__setConfirmPassword = setConfirmPassword; - (window as any).__setCaptcha = setCaptcha; - (window as any).__captchaCode = captchaCode; - (window as any).__firstName = firstName; - (window as any).__lastName = lastName; - (window as any).__companyName = companyName; - (window as any).__email = email; - (window as any).__setOtp = setOtp; - (window as any).__otp = otp; - (window as any).__setOtpDigits = setOtp; - (window as any).__otpDigits = otp; - (window as any).__testModeActive = typeof window !== 'undefined' && window.__testMode === true; - } - - const resendOtp = async () => { - setServerError(""); - setSubmitting(true); - try { - const res = await fetch("/api/gateway/auth/resend-otp", { - method: "POST", - headers: { "Content-Type": "application/json", Accept: "application/json" }, - credentials: "include", - body: JSON.stringify({ email: pendingEmail() || email().trim().toLowerCase() }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok) { - setServerError(String(data?.error || data?.message || "Unable to resend OTP right now.")); - } - } catch (err) { - console.error("[resendOtp] fetch error:", err); - setServerError("Network error — please try again."); - } finally { - setSubmitting(false); - } - }; + }); return ( -
- - -
-
- Get Started -
-
-

Get Started

-

Create Your Nxtgauge Account

-

- Join verified opportunities and continue directly to your dashboard after signup. -

-
-
- -
- -

Verify Email

-

- Enter the 6-digit code sent to {pendingEmail() || email()}. -

- - -
-

- Your email has been verified. -

-

- Redirecting to login... -

-
- } - > -
- index)}> - {(index) => ( - setOtpDigit(index, e.currentTarget.value)} - /> - )} - -
- - - - - - - } - > -

Create Your Account

-

- Sign up first, then go directly to dashboard after email verification. -

- - {/* Role Selector - Only show tabs for job_seeker and company when no intent is provided */} - -
- - -
-
- - {/* Show role badge for professional/customer when coming via direct link */} - -
-

- Registering as: {role() === "professional" ? "Professional" : role() === "customer" ? "Customer" : role()} -

-
-
- -
-
- - setFirstName(e.currentTarget.value)} - /> -

- {firstName().trim() && firstNameValid() - ? "✓ First name looks good" - : "• First name is required"} -

-
- - - setLastName(e.currentTarget.value)} - /> -

- {lastName().trim() && lastNameValid() - ? "✓ Last name looks good" - : "• Last name is required"} -

-
- } - > -
- - setCompanyName(e.currentTarget.value)} - /> -

- {companyName().trim() - ? "✓ Company name looks good" - : "• Company name is required"} -

-
- - - -
- - { - setEmail(e.currentTarget.value); - setEmailExists(false); - }} - onBlur={() => { - void checkEmailExists(email()); - }} - /> -

- {emailExists() - ? "• This email is already registered" - : email().trim() && emailValid() - ? "✓ Valid email format" - : "• Enter a valid email format"} -

-
- -
-
- -
- setPassword(e.currentTarget.value)} - /> - -
-
-

- {passwordChecks().minLength ? "✓" : "•"} 8+ chars -

-

- {passwordChecks().uppercase ? "✓" : "•"} Uppercase -

-

- {passwordChecks().special ? "✓" : "•"} Special -

-

- {passwordChecks().lowercase ? "✓" : "•"} Lowercase -

-

- {passwordChecks().number ? "✓" : "•"} Number -

-
-
-
- -
- setConfirmPassword(e.currentTarget.value)} - /> - -
-

- {confirmPassword() && passwordChecks().match - ? "✓ Passwords match" - : "• Passwords do not match"} -

-
-
- -
- -
- - - setCaptcha(e.currentTarget.value.toUpperCase())} - placeholder="Enter captcha" - /> -
-

- {captcha() - ? isValidCaptcha(captcha(), captchaCode()) - ? "✓ Captcha matched" - : "• Captcha does not match" - : "• Enter captcha to continue"} -

-
- -
- -
- - - - - - - -

{serverError()}

-
- - -
+
+

Redirecting..."

+
); } diff --git a/src/routes/signup/company.tsx b/src/routes/signup/company.tsx new file mode 100644 index 0000000..fb8659e --- /dev/null +++ b/src/routes/signup/company.tsx @@ -0,0 +1,548 @@ +import { A, useNavigate } from "@solidjs/router"; +import { createMemo, createSignal, For, onMount, Show } from "solid-js"; +import PublicBackground from "~/components/PublicBackground"; +import PublicHeader from "~/components/PublicHeader"; +import CaptchaCanvas from "~/components/CaptchaCanvas"; +import { + checkPasswordStrength, + isPasswordStrong, + isValidCaptcha, + isValidEmail, + isValidName, + validateRegisterForm, +} from "~/lib/form-validation"; + +export default function CompanySignupRoute() { + const navigate = useNavigate(); + + const [step, setStep] = createSignal<"register" | "verify">("register"); + const [firstName, setFirstName] = createSignal(""); + const [email, setEmail] = createSignal(""); + const [password, setPassword] = createSignal(""); + const [confirmPassword, setConfirmPassword] = createSignal(""); + const [companyName, setCompanyName] = createSignal(""); + const [captcha, setCaptcha] = createSignal(""); + const [captchaCode, setCaptchaCode] = createSignal(randomCaptcha()); + const [otp, setOtp] = createSignal(["", "", "", "", "", ""]); + const [errors, setErrors] = createSignal>({}); + const [serverError, setServerError] = createSignal(""); + const [emailExists, setEmailExists] = createSignal(false); + const [submitting, setSubmitting] = createSignal(false); + const [pendingEmail, setPendingEmail] = createSignal(""); + const [verifiedSuccess, setVerifiedSuccess] = createSignal(false); + const [showPassword, setShowPassword] = createSignal(false); + const [showConfirmPassword, setShowConfirmPassword] = createSignal(false); + + const passwordChecks = createMemo(() => checkPasswordStrength(password(), confirmPassword())); + const otpCode = createMemo(() => otp().join("")); + const firstNameValid = createMemo(() => !firstName().trim() || isValidName(firstName())); + const companyNameValid = createMemo(() => !companyName().trim() || companyName().trim().length >= 2); + const emailValid = createMemo(() => !email().trim() || isValidEmail(email())); + const canSubmit = createMemo( + () => + firstName().trim().length > 0 && + firstNameValid() && + companyName().trim().length > 0 && + companyNameValid() && + emailValid() && + isValidEmail(email()) && + isPasswordStrong(passwordChecks()) && + passwordChecks().match && + isValidCaptcha(captcha(), captchaCode()) + ); + + function randomCaptcha(length = 6): string { + const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + let out = ""; + for (let i = 0; i < length; i += 1) { + out += alphabet[Math.floor(Math.random() * alphabet.length)]; + } + return out; + } + + const refreshCaptcha = () => { + setCaptcha(""); + setCaptchaCode(randomCaptcha()); + }; + + const checkEmailExists = async (emailValue: string) => { + const normalized = emailValue.trim().toLowerCase(); + if (!normalized || !isValidEmail(normalized)) { + setEmailExists(false); + return false; + } + + try { + const response = await fetch("/api/gateway/auth/check-email", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ email: normalized }), + }); + const payload = await response.json().catch(() => ({})); + const exists = Boolean(response.ok && payload?.exists); + setEmailExists(exists); + return exists; + } catch { + setEmailExists(false); + return false; + } + }; + + const setOtpDigit = (index: number, value: string) => { + const clean = value.replace(/\D/g, "").slice(0, 1); + setOtp((prev) => { + const next = prev.slice(); + next[index] = clean; + return next; + }); + queueMicrotask(() => { + if (clean && index < 5) { + const nextEl = document.querySelector(`#otp-${index + 1}`); + if (nextEl) nextEl.focus(); + } + }); + }; + + const saveUserForDashboard = (input: { + firstName: string; + companyName: string; + email: string; + }) => { + const displayName = input.companyName || input.firstName; + const payload = { + firstName: input.firstName, + lastName: "", + companyName: input.companyName, + fullName: displayName, + name: displayName, + displayName, + email: input.email.toLowerCase(), + roleKey: "company", + role: "company", + }; + if (typeof window !== "undefined") { + window.sessionStorage.setItem("nxtgauge_signup_profile_v1", JSON.stringify(payload)); + window.sessionStorage.setItem("nxtgauge_auth_user", JSON.stringify(payload)); + window.sessionStorage.setItem("nxtgauge_user", JSON.stringify(payload)); + } + }; + + const register = async () => { + setServerError(""); + const validation = validateRegisterForm({ + firstName: firstName(), + lastName: "", + email: email(), + password: password(), + confirmPassword: confirmPassword(), + captcha: captcha(), + expectedCaptcha: captchaCode(), + termsAccepted: true, + }); + setErrors(validation.errors); + if (!validation.isValid) return; + + setSubmitting(true); + try { + const res = await fetch("/api/gateway/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ + first_name: firstName().trim(), + last_name: companyName().trim(), + email: email().trim().toLowerCase(), + password: password(), + phone: "", + intent: "company", + company_name: companyName().trim(), + }), + }); + + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setServerError(String(data?.error || data?.message || "Unable to create account.")); + refreshCaptcha(); + return; + } + + const cleanEmail = email().trim().toLowerCase(); + setPendingEmail(cleanEmail); + setVerifiedSuccess(false); + saveUserForDashboard({ + firstName: firstName().trim(), + companyName: companyName().trim(), + email: cleanEmail, + }); + setStep("verify"); + } catch (err) { + setServerError("Network error — please check your connection and try again."); + refreshCaptcha(); + } finally { + setSubmitting(false); + } + }; + + const verifyOtp = async () => { + setServerError(""); + if (otpCode().length !== 6) { + setServerError("Enter the 6-digit code sent to your email."); + return; + } + setSubmitting(true); + try { + const verifyRes = await fetch("/api/gateway/auth/verify-email", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ otp: otpCode() }), + }); + const verifyData = await verifyRes.json().catch(() => ({})); + if (!verifyRes.ok) { + setServerError(String(verifyData?.error || verifyData?.message || "Verification failed.")); + return; + } + setVerifiedSuccess(true); + setTimeout(() => navigate("/dashboard?role=COMPANY", { replace: true }), 1400); + } catch (err) { + setServerError("Network error — please try again."); + } finally { + setSubmitting(false); + } + }; + + const resendOtp = async () => { + setServerError(""); + setSubmitting(true); + try { + const res = await fetch("/api/gateway/auth/resend-otp", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ email: pendingEmail() || email().trim().toLowerCase() }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setServerError(String(data?.error || data?.message || "Unable to resend OTP.")); + } + } catch (err) { + setServerError("Network error — please try again."); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ + +
+
+ Company Registration +
+
+

Company Registration

+

Register Your Company

+

+ Post verified jobs and hire faster through our trust-first workflow. +

+
+
+ +
+ +

Verify Email

+

+ Enter the 6-digit code sent to {pendingEmail() || email()}. +

+ + +
+

+ Your email has been verified. +

+

+ Redirecting to dashboard... +

+
+ } + > +
+ index)}> + {(index) => ( + setOtpDigit(index, e.currentTarget.value)} + /> + )} + +
+ + + + + + + } + > +

Create Company Account

+

+ Register your company to post jobs and hire verified talent. +

+ +
+ + setFirstName(e.currentTarget.value)} + placeholder="Your name" + /> +

+ {firstName().trim() && firstNameValid() + ? "✓ Name looks good" + : "• Name is required"} +

+
+ +
+ + setCompanyName(e.currentTarget.value)} + placeholder="Your company name" + /> +

+ {companyName().trim() && companyNameValid() + ? "✓ Company name looks good" + : "• Company name is required"} +

+
+ +
+ + { + setEmail(e.currentTarget.value); + setEmailExists(false); + }} + onBlur={() => void checkEmailExists(email())} + placeholder="company@example.com" + /> +

+ {emailExists() + ? "• This email is already registered" + : email().trim() && emailValid() + ? "✓ Valid email format" + : "• Enter a valid email format"} +

+
+ +
+
+ +
+ setPassword(e.currentTarget.value)} + /> + +
+
+

+ {passwordChecks().minLength ? "✓" : "•"} 8+ chars +

+

+ {passwordChecks().uppercase ? "✓" : "•"} Uppercase +

+

+ {passwordChecks().special ? "✓" : "•"} Special +

+

+ {passwordChecks().lowercase ? "✓" : "•"} Lowercase +

+

+ {passwordChecks().number ? "✓" : "•"} Number +

+
+
+
+ +
+ setConfirmPassword(e.currentTarget.value)} + /> + +
+

+ {confirmPassword() && passwordChecks().match + ? "✓ Passwords match" + : "• Passwords do not match"} +

+
+
+ +
+ +
+ + + setCaptcha(e.currentTarget.value.toUpperCase())} + placeholder="Enter captcha" + /> +
+

+ {captcha() && captcha().toUpperCase() === captchaCode() + ? "✓ Captcha matched" + : "• Enter captcha to continue"} +

+
+ + +

{serverError()}

+
+ + + + + + + +
+ ); +} diff --git a/src/routes/signup/job-seeker.tsx b/src/routes/signup/job-seeker.tsx new file mode 100644 index 0000000..d3580fc --- /dev/null +++ b/src/routes/signup/job-seeker.tsx @@ -0,0 +1,547 @@ +import { A, useNavigate } from "@solidjs/router"; +import { createMemo, createSignal, For, onMount, Show } from "solid-js"; +import PublicBackground from "~/components/PublicBackground"; +import PublicHeader from "~/components/PublicHeader"; +import CaptchaCanvas from "~/components/CaptchaCanvas"; +import { + checkPasswordStrength, + isPasswordStrong, + isValidCaptcha, + isValidEmail, + isValidName, + validateRegisterForm, +} from "~/lib/form-validation"; + +export default function JobSeekerSignupRoute() { + const navigate = useNavigate(); + + const [step, setStep] = createSignal<"register" | "verify">("register"); + const [firstName, setFirstName] = createSignal(""); + const [lastName, setLastName] = createSignal(""); + const [email, setEmail] = createSignal(""); + const [password, setPassword] = createSignal(""); + const [confirmPassword, setConfirmPassword] = createSignal(""); + const [captcha, setCaptcha] = createSignal(""); + const [captchaCode, setCaptchaCode] = createSignal(randomCaptcha()); + const [otp, setOtp] = createSignal(["", "", "", "", "", ""]); + const [errors, setErrors] = createSignal>({}); + const [serverError, setServerError] = createSignal(""); + const [emailExists, setEmailExists] = createSignal(false); + const [submitting, setSubmitting] = createSignal(false); + const [pendingEmail, setPendingEmail] = createSignal(""); + const [verifiedSuccess, setVerifiedSuccess] = createSignal(false); + const [showPassword, setShowPassword] = createSignal(false); + const [showConfirmPassword, setShowConfirmPassword] = createSignal(false); + + const passwordChecks = createMemo(() => checkPasswordStrength(password(), confirmPassword())); + const otpCode = createMemo(() => otp().join("")); + const firstNameValid = createMemo(() => !firstName().trim() || isValidName(firstName())); + const lastNameValid = createMemo(() => !lastName().trim() || isValidName(lastName())); + const emailValid = createMemo(() => !email().trim() || isValidEmail(email())); + const canSubmit = createMemo( + () => + firstName().trim().length > 0 && + firstNameValid() && + lastName().trim().length > 0 && + lastNameValid() && + emailValid() && + isValidEmail(email()) && + isPasswordStrong(passwordChecks()) && + passwordChecks().match && + isValidCaptcha(captcha(), captchaCode()) + ); + + function randomCaptcha(length = 6): string { + const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + let out = ""; + for (let i = 0; i < length; i += 1) { + out += alphabet[Math.floor(Math.random() * alphabet.length)]; + } + return out; + } + + const refreshCaptcha = () => { + setCaptcha(""); + setCaptchaCode(randomCaptcha()); + }; + + const checkEmailExists = async (emailValue: string) => { + const normalized = emailValue.trim().toLowerCase(); + if (!normalized || !isValidEmail(normalized)) { + setEmailExists(false); + return false; + } + + try { + const response = await fetch("/api/gateway/auth/check-email", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ email: normalized }), + }); + const payload = await response.json().catch(() => ({})); + const exists = Boolean(response.ok && payload?.exists); + setEmailExists(exists); + return exists; + } catch { + setEmailExists(false); + return false; + } + }; + + const setOtpDigit = (index: number, value: string) => { + const clean = value.replace(/\D/g, "").slice(0, 1); + setOtp((prev) => { + const next = prev.slice(); + next[index] = clean; + return next; + }); + queueMicrotask(() => { + if (clean && index < 5) { + const nextEl = document.querySelector(`#otp-${index + 1}`); + if (nextEl) nextEl.focus(); + } + }); + }; + + const saveUserForDashboard = (input: { + firstName: string; + lastName: string; + email: string; + }) => { + const displayName = `${input.firstName} ${input.lastName}`.trim(); + const payload = { + firstName: input.firstName, + lastName: input.lastName, + fullName: displayName, + name: displayName, + displayName, + email: input.email.toLowerCase(), + roleKey: "job_seeker", + role: "job_seeker", + }; + if (typeof window !== "undefined") { + window.sessionStorage.setItem("nxtgauge_signup_profile_v1", JSON.stringify(payload)); + window.sessionStorage.setItem("nxtgauge_auth_user", JSON.stringify(payload)); + window.sessionStorage.setItem("nxtgauge_user", JSON.stringify(payload)); + } + }; + + const register = async () => { + setServerError(""); + const validation = validateRegisterForm({ + firstName: firstName(), + lastName: lastName(), + email: email(), + password: password(), + confirmPassword: confirmPassword(), + captcha: captcha(), + expectedCaptcha: captchaCode(), + termsAccepted: true, + }); + setErrors(validation.errors); + if (!validation.isValid) return; + + setSubmitting(true); + try { + const res = await fetch("/api/gateway/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ + first_name: firstName().trim(), + last_name: lastName().trim(), + email: email().trim().toLowerCase(), + password: password(), + phone: "", + intent: "job_seeker", + }), + }); + + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setServerError(String(data?.error || data?.message || "Unable to create account.")); + refreshCaptcha(); + return; + } + + const cleanEmail = email().trim().toLowerCase(); + setPendingEmail(cleanEmail); + setVerifiedSuccess(false); + saveUserForDashboard({ + firstName: firstName().trim(), + lastName: lastName().trim(), + email: cleanEmail, + }); + setStep("verify"); + } catch (err) { + setServerError("Network error — please check your connection and try again."); + refreshCaptcha(); + } finally { + setSubmitting(false); + } + }; + + const verifyOtp = async () => { + setServerError(""); + if (otpCode().length !== 6) { + setServerError("Enter the 6-digit code sent to your email."); + return; + } + setSubmitting(true); + try { + const verifyRes = await fetch("/api/gateway/auth/verify-email", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ otp: otpCode() }), + }); + const verifyData = await verifyRes.json().catch(() => ({})); + if (!verifyRes.ok) { + setServerError(String(verifyData?.error || verifyData?.message || "Verification failed.")); + return; + } + setVerifiedSuccess(true); + setTimeout(() => navigate("/dashboard?role=JOB_SEEKER", { replace: true }), 1400); + } catch (err) { + setServerError("Network error — please try again."); + } finally { + setSubmitting(false); + } + }; + + const resendOtp = async () => { + setServerError(""); + setSubmitting(true); + try { + const res = await fetch("/api/gateway/auth/resend-otp", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ email: pendingEmail() || email().trim().toLowerCase() }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setServerError(String(data?.error || data?.message || "Unable to resend OTP.")); + } + } catch (err) { + setServerError("Network error — please try again."); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ + +
+
+ Job Seeker Registration +
+
+

Job Seeker Registration

+

Find Your Dream Job

+

+ Apply to verified opportunities with a stronger profile and clearer status flow. +

+
+
+ +
+ +

Verify Email

+

+ Enter the 6-digit code sent to {pendingEmail() || email()}. +

+ + +
+

+ Your email has been verified. +

+

+ Redirecting to dashboard... +

+
+ } + > +
+ index)}> + {(index) => ( + setOtpDigit(index, e.currentTarget.value)} + /> + )} + +
+ + + + + + + } + > +

Create Job Seeker Account

+

+ Build your profile and apply to approved opportunities quickly. +

+ +
+
+ + setFirstName(e.currentTarget.value)} + placeholder="John" + /> +

+ {firstName().trim() && firstNameValid() + ? "✓ First name looks good" + : "• First name is required"} +

+
+
+ + setLastName(e.currentTarget.value)} + placeholder="Doe" + /> +

+ {lastName().trim() && lastNameValid() + ? "✓ Last name looks good" + : "• Last name is required"} +

+
+
+ +
+ + { + setEmail(e.currentTarget.value); + setEmailExists(false); + }} + onBlur={() => void checkEmailExists(email())} + placeholder="john.doe@example.com" + /> +

+ {emailExists() + ? "• This email is already registered" + : email().trim() && emailValid() + ? "✓ Valid email format" + : "• Enter a valid email format"} +

+
+ +
+
+ +
+ setPassword(e.currentTarget.value)} + /> + +
+
+

+ {passwordChecks().minLength ? "✓" : "•"} 8+ chars +

+

+ {passwordChecks().uppercase ? "✓" : "•"} Uppercase +

+

+ {passwordChecks().special ? "✓" : "•"} Special +

+

+ {passwordChecks().lowercase ? "✓" : "•"} Lowercase +

+

+ {passwordChecks().number ? "✓" : "•"} Number +

+
+
+
+ +
+ setConfirmPassword(e.currentTarget.value)} + /> + +
+

+ {confirmPassword() && passwordChecks().match + ? "✓ Passwords match" + : "• Passwords do not match"} +

+
+
+ +
+ +
+ + + setCaptcha(e.currentTarget.value.toUpperCase())} + placeholder="Enter captcha" + /> +
+

+ {captcha() && captcha().toUpperCase() === captchaCode() + ? "✓ Captcha matched" + : "• Enter captcha to continue"} +

+
+ + +

{serverError()}

+
+ + + + + + + +
+ ); +} diff --git a/src/routes/signup/professional.tsx b/src/routes/signup/professional.tsx new file mode 100644 index 0000000..c41f774 --- /dev/null +++ b/src/routes/signup/professional.tsx @@ -0,0 +1,586 @@ +import { A, useNavigate } from "@solidjs/router"; +import { createMemo, createSignal, For, onMount, Show } from "solid-js"; +import PublicBackground from "~/components/PublicBackground"; +import PublicHeader from "~/components/PublicHeader"; +import CaptchaCanvas from "~/components/CaptchaCanvas"; +import { + checkPasswordStrength, + isPasswordStrong, + isValidCaptcha, + isValidEmail, + isValidName, + validateRegisterForm, +} from "~/lib/form-validation"; + +export default function ProfessionalSignupRoute() { + const navigate = useNavigate(); + + const [step, setStep] = createSignal<"register" | "verify">("register"); + const [firstName, setFirstName] = createSignal(""); + const [lastName, setLastName] = createSignal(""); + const [email, setEmail] = createSignal(""); + const [password, setPassword] = createSignal(""); + const [confirmPassword, setConfirmPassword] = createSignal(""); + const [profession, setProfession] = createSignal(""); + const [captcha, setCaptcha] = createSignal(""); + const [captchaCode, setCaptchaCode] = createSignal(randomCaptcha()); + const [otp, setOtp] = createSignal(["", "", "", "", "", ""]); + const [errors, setErrors] = createSignal>({}); + const [serverError, setServerError] = createSignal(""); + const [emailExists, setEmailExists] = createSignal(false); + const [submitting, setSubmitting] = createSignal(false); + const [pendingEmail, setPendingEmail] = createSignal(""); + const [verifiedSuccess, setVerifiedSuccess] = createSignal(false); + const [showPassword, setShowPassword] = createSignal(false); + const [showConfirmPassword, setShowConfirmPassword] = createSignal(false); + + const passwordChecks = createMemo(() => checkPasswordStrength(password(), confirmPassword())); + const otpCode = createMemo(() => otp().join("")); + const firstNameValid = createMemo(() => !firstName().trim() || isValidName(firstName())); + const lastNameValid = createMemo(() => !lastName().trim() || isValidName(lastName())); + const emailValid = createMemo(() => !email().trim() || isValidEmail(email())); + const canSubmit = createMemo( + () => + firstName().trim().length > 0 && + firstNameValid() && + lastName().trim().length > 0 && + lastNameValid() && + profession().trim().length > 0 && + emailValid() && + isValidEmail(email()) && + isPasswordStrong(passwordChecks()) && + passwordChecks().match && + isValidCaptcha(captcha(), captchaCode()) + ); + + const professions = [ + { value: "PHOTOGRAPHER", label: "Photographer" }, + { value: "MAKEUP_ARTIST", label: "Makeup Artist" }, + { value: "TUTOR", label: "Tutor" }, + { value: "DEVELOPER", label: "Developer" }, + { value: "VIDEO_EDITOR", label: "Video Editor" }, + { value: "GRAPHIC_DESIGNER", label: "Graphic Designer" }, + { value: "SOCIAL_MEDIA_MANAGER", label: "Social Media Manager" }, + { value: "FITNESS_TRAINER", label: "Fitness Trainer" }, + { value: "CATERING_SERVICES", label: "Catering Services" }, + { value: "UGC_CONTENT_CREATOR", label: "UGC Content Creator" }, + ]; + + function randomCaptcha(length = 6): string { + const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + let out = ""; + for (let i = 0; i < length; i += 1) { + out += alphabet[Math.floor(Math.random() * alphabet.length)]; + } + return out; + } + + const refreshCaptcha = () => { + setCaptcha(""); + setCaptchaCode(randomCaptcha()); + }; + + const checkEmailExists = async (emailValue: string) => { + const normalized = emailValue.trim().toLowerCase(); + if (!normalized || !isValidEmail(normalized)) { + setEmailExists(false); + return false; + } + + try { + const response = await fetch("/api/gateway/auth/check-email", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ email: normalized }), + }); + const payload = await response.json().catch(() => ({})); + const exists = Boolean(response.ok && payload?.exists); + setEmailExists(exists); + return exists; + } catch { + setEmailExists(false); + return false; + } + }; + + const setOtpDigit = (index: number, value: string) => { + const clean = value.replace(/\D/g, "").slice(0, 1); + setOtp((prev) => { + const next = prev.slice(); + next[index] = clean; + return next; + }); + queueMicrotask(() => { + if (clean && index < 5) { + const nextEl = document.querySelector(`#otp-${index + 1}`); + if (nextEl) nextEl.focus(); + } + }); + }; + + const saveUserForDashboard = (input: { + firstName: string; + lastName: string; + profession: string; + email: string; + }) => { + const displayName = `${input.firstName} ${input.lastName}`.trim(); + const payload = { + firstName: input.firstName, + lastName: input.lastName, + fullName: displayName, + name: displayName, + displayName, + profession: input.profession, + email: input.email.toLowerCase(), + roleKey: "professional", + role: "professional", + }; + if (typeof window !== "undefined") { + window.sessionStorage.setItem("nxtgauge_signup_profile_v1", JSON.stringify(payload)); + window.sessionStorage.setItem("nxtgauge_auth_user", JSON.stringify(payload)); + window.sessionStorage.setItem("nxtgauge_user", JSON.stringify(payload)); + } + }; + + const register = async () => { + setServerError(""); + const validation = validateRegisterForm({ + firstName: firstName(), + lastName: lastName(), + email: email(), + password: password(), + confirmPassword: confirmPassword(), + captcha: captcha(), + expectedCaptcha: captchaCode(), + termsAccepted: true, + }); + setErrors(validation.errors); + if (!validation.isValid) return; + + setSubmitting(true); + try { + const res = await fetch("/api/gateway/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ + first_name: firstName().trim(), + last_name: lastName().trim(), + email: email().trim().toLowerCase(), + password: password(), + phone: "", + intent: "professional", + profession: profession().toUpperCase(), + role_key: profession().toUpperCase(), + }), + }); + + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setServerError(String(data?.error || data?.message || "Unable to create account.")); + refreshCaptcha(); + return; + } + + const cleanEmail = email().trim().toLowerCase(); + setPendingEmail(cleanEmail); + setVerifiedSuccess(false); + saveUserForDashboard({ + firstName: firstName().trim(), + lastName: lastName().trim(), + profession: profession(), + email: cleanEmail, + }); + setStep("verify"); + } catch (err) { + setServerError("Network error — please check your connection and try again."); + refreshCaptcha(); + } finally { + setSubmitting(false); + } + }; + + const verifyOtp = async () => { + setServerError(""); + if (otpCode().length !== 6) { + setServerError("Enter the 6-digit code sent to your email."); + return; + } + setSubmitting(true); + try { + const verifyRes = await fetch("/api/gateway/auth/verify-email", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ otp: otpCode() }), + }); + const verifyData = await verifyRes.json().catch(() => ({})); + if (!verifyRes.ok) { + setServerError(String(verifyData?.error || verifyData?.message || "Verification failed.")); + return; + } + setVerifiedSuccess(true); + setTimeout(() => navigate("/dashboard?role=PROFESSIONAL", { replace: true }), 1400); + } catch (err) { + setServerError("Network error — please try again."); + } finally { + setSubmitting(false); + } + }; + + const resendOtp = async () => { + setServerError(""); + setSubmitting(true); + try { + const res = await fetch("/api/gateway/auth/resend-otp", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", + body: JSON.stringify({ email: pendingEmail() || email().trim().toLowerCase() }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setServerError(String(data?.error || data?.message || "Unable to resend OTP.")); + } + } catch (err) { + setServerError("Network error — please try again."); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ + +
+
+ Professional Registration +
+
+

Professional Registration

+

Join as a Professional

+

+ Create a trusted profile and grow through verified demand. +

+
+
+ +
+ +

Verify Email

+

+ Enter the 6-digit code sent to {pendingEmail() || email()}. +

+ + +
+

+ Your email has been verified. +

+

+ Redirecting to dashboard... +

+
+ } + > +
+ index)}> + {(index) => ( + setOtpDigit(index, e.currentTarget.value)} + /> + )} + +
+ + + + + + + } + > +

Create Professional Account

+

+ Select your profession and build your trusted profile. +

+ +
+ + +

+ {profession().trim() ? "✓ Profession selected" : "• Select your profession"} +

+
+ +
+
+ + setFirstName(e.currentTarget.value)} + placeholder="John" + /> +

+ {firstName().trim() && firstNameValid() + ? "✓ First name looks good" + : "• First name is required"} +

+
+
+ + setLastName(e.currentTarget.value)} + placeholder="Doe" + /> +

+ {lastName().trim() && lastNameValid() + ? "✓ Last name looks good" + : "• Last name is required"} +

+
+
+ +
+ + { + setEmail(e.currentTarget.value); + setEmailExists(false); + }} + onBlur={() => void checkEmailExists(email())} + placeholder="john.doe@example.com" + /> +

+ {emailExists() + ? "• This email is already registered" + : email().trim() && emailValid() + ? "✓ Valid email format" + : "• Enter a valid email format"} +

+
+ +
+
+ +
+ setPassword(e.currentTarget.value)} + /> + +
+
+

+ {passwordChecks().minLength ? "✓" : "•"} 8+ chars +

+

+ {passwordChecks().uppercase ? "✓" : "•"} Uppercase +

+

+ {passwordChecks().special ? "✓" : "•"} Special +

+

+ {passwordChecks().lowercase ? "✓" : "•"} Lowercase +

+

+ {passwordChecks().number ? "✓" : "•"} Number +

+
+
+
+ +
+ setConfirmPassword(e.currentTarget.value)} + /> + +
+

+ {confirmPassword() && passwordChecks().match + ? "✓ Passwords match" + : "• Passwords do not match"} +

+
+
+ +
+ +
+ + + setCaptcha(e.currentTarget.value.toUpperCase())} + placeholder="Enter captcha" + /> +
+

+ {captcha() && captcha().toUpperCase() === captchaCode() + ? "✓ Captcha matched" + : "• Enter captcha to continue"} +

+
+ + +

{serverError()}

+
+ + + + + + + +
+ ); +}