From cbe49ff5add8048b800ee9c293506403c43c3365 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Fri, 3 Jul 2026 17:55:45 +0530 Subject: [PATCH] fix: update CTAs and fix company signup validation - Update homepage CTAs to point to new signup URLs - Fix company signup to not validate lastName - Add customer signup page - Fix professionals page CTA --- src/components/PublicLanding.tsx | 6 +- src/routes/professionals/index.tsx | 2 +- src/routes/signup/company.tsx | 35 +- src/routes/signup/customer.tsx | 554 +++++++++++++++++++++++++++++ 4 files changed, 581 insertions(+), 16 deletions(-) create mode 100644 src/routes/signup/customer.tsx diff --git a/src/components/PublicLanding.tsx b/src/components/PublicLanding.tsx index a651b02..137f128 100644 --- a/src/components/PublicLanding.tsx +++ b/src/components/PublicLanding.tsx @@ -374,7 +374,7 @@ export default function PublicLanding() { Hire trusted professionals, post verified jobs, and apply faster in one platform.

- Get Started + Get Started How It Works

Most profile and listing reviews are completed within 24-48 hours.

@@ -589,9 +589,9 @@ export default function PublicLanding() {

Hire, post, apply, or join as a professional from stable role-specific pages.

- Get Started + Get Started Explore Professionals - Post a Job + Post a Job
diff --git a/src/routes/professionals/index.tsx b/src/routes/professionals/index.tsx index 2f535ee..6b8a377 100644 --- a/src/routes/professionals/index.tsx +++ b/src/routes/professionals/index.tsx @@ -74,7 +74,7 @@ export default function ProfessionalsIndexPage() { Choose your category, understand what Nxtgauge offers for your role, and register with a trust-first workflow.

- Get Started + Get Started View categories

Dedicated role pages are optimized for campaign traffic and search visibility.

diff --git a/src/routes/signup/company.tsx b/src/routes/signup/company.tsx index fb8659e..8b75ac7 100644 --- a/src/routes/signup/company.tsx +++ b/src/routes/signup/company.tsx @@ -130,18 +130,29 @@ export default function CompanySignupRoute() { 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; + + // Custom validation for company (no lastName required) + const validationErrors: Record = {}; + + if (!firstName().trim()) { + validationErrors.firstName = "First name is required"; + } + if (!companyName().trim()) { + validationErrors.companyName = "Company name is required"; + } + if (!email().trim() || !isValidEmail(email())) { + validationErrors.email = "Enter a valid email format"; + } + const checks = checkPasswordStrength(password(), confirmPassword()); + if (!isPasswordStrong(checks)) { + validationErrors.password = "Password must be strong"; + } + if (!isValidCaptcha(captcha(), captchaCode())) { + validationErrors.captcha = "Enter captcha to continue"; + } + + setErrors(validationErrors); + if (Object.keys(validationErrors).length > 0) return; setSubmitting(true); try { diff --git a/src/routes/signup/customer.tsx b/src/routes/signup/customer.tsx new file mode 100644 index 0000000..4e90f1f --- /dev/null +++ b/src/routes/signup/customer.tsx @@ -0,0 +1,554 @@ +import { A, useNavigate } from "@solidjs/router"; +import { createMemo, createSignal, For, 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, +} from "~/lib/form-validation"; + +export default function CustomerSignupRoute() { + 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: "customer", + role: "customer", + }; + 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(""); + + // Custom validation for customer + const validationErrors: Record = {}; + + if (!firstName().trim()) { + validationErrors.firstName = "First name is required"; + } + if (!lastName().trim()) { + validationErrors.lastName = "Last name is required"; + } + if (!email().trim() || !isValidEmail(email())) { + validationErrors.email = "Enter a valid email format"; + } + const checks = checkPasswordStrength(password(), confirmPassword()); + if (!isPasswordStrong(checks)) { + validationErrors.password = "Password must be strong"; + } + if (!isValidCaptcha(captcha(), captchaCode())) { + validationErrors.captcha = "Enter captcha to continue"; + } + + setErrors(validationErrors); + if (Object.keys(validationErrors).length > 0) 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: "customer", + }), + }); + + 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=CUSTOMER", { 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 ( +
+ + +
+
+ Customer Registration +
+
+

Customer Registration

+

Hire Verified Professionals

+

+ Post requirements and connect with trusted specialists. +

+
+
+ +
+ +

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 Customer Account

+

+ Register to post requirements and hire verified professionals. +

+ +
+
+ + 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()}

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