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
This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-03 17:55:45 +05:30
parent bdf51ba3da
commit cbe49ff5ad
4 changed files with 581 additions and 16 deletions

View file

@ -374,7 +374,7 @@ export default function PublicLanding() {
Hire trusted professionals, post verified jobs, and apply faster in one platform.
</p>
<div class="hero-actions">
<A class="lp-primary-btn" href="/signup?intent=customer">Get Started</A>
<A class="lp-primary-btn" href="/signup/customer">Get Started</A>
<A class="lp-ghost-btn lp-ghost-btn-dark" href="#how-it-works">How It Works</A>
</div>
<p class="lp-hero-note">Most profile and listing reviews are completed within 24-48 hours.</p>
@ -589,9 +589,9 @@ export default function PublicLanding() {
<p class="sub">Hire, post, apply, or join as a professional from stable role-specific pages.</p>
</div>
<div class="hero-actions cta-actions">
<A class="lp-primary-btn pulse" href="/signup?intent=customer">Get Started</A>
<A class="lp-primary-btn pulse" href="/signup/customer">Get Started</A>
<A class="lp-ghost-btn lp-ghost-btn-dark" href="/professionals">Explore Professionals</A>
<A class="lp-ghost-btn lp-ghost-btn-dark" href="/signup?intent=company">Post a Job</A>
<A class="lp-ghost-btn lp-ghost-btn-dark" href="/signup/company">Post a Job</A>
</div>
</div>
</section>

View file

@ -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.
</p>
<div class="hero-actions">
<A class="lp-primary-btn" href="/signup?intent=professional">Get Started</A>
<A class="lp-primary-btn" href="/signup/professional">Get Started</A>
<A class="lp-ghost-btn lp-ghost-btn-dark" href="/professionals#paths">View categories</A>
</div>
<p class="lp-hero-note">Dedicated role pages are optimized for campaign traffic and search visibility.</p>

View file

@ -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<string, string> = {};
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 {

View file

@ -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<Record<string, string>>({});
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<HTMLInputElement>(`#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<string, string> = {};
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 (
<main class="auth-page">
<PublicBackground />
<PublicHeader />
<div class="auth-layout">
<section class="auth-visual card glass-dark">
<img class="auth-visual-img" src="/images/auth-company-2.jpg" alt="Customer Registration" />
<div class="auth-visual-overlay" />
<div class="auth-visual-content">
<p class="eyebrow">Customer Registration</p>
<h1 class="title light">Hire Verified Professionals</h1>
<p class="subtitle light">
Post requirements and connect with trusted specialists.
</p>
</div>
</section>
<section class="auth-form card glass-light">
<Show
when={step() === "register"}
fallback={
<>
<h2 class="title">Verify Email</h2>
<p class="subtitle">
Enter the 6-digit code sent to <strong>{pendingEmail() || email()}</strong>.
</p>
<Show
when={!verifiedSuccess()}
fallback={
<div
style={{
"margin-top": "12px",
"border-radius": "12px",
border: "1px solid #FED7AA",
background: "#FFF7ED",
padding: "14px 16px",
color: "#C2410C",
"text-align": "center",
}}
>
<div style={{ "font-size": "30px", "line-height": "1" }}></div>
<p style={{ margin: "8px 0 0", "font-weight": "700", "font-size": "14px" }}>
Your email has been verified.
</p>
<p style={{ margin: "6px 0 0", "font-size": "13px" }}>
Redirecting to dashboard...
</p>
</div>
}
>
<div class="otp-row">
<For each={Array.from({ length: 6 }, (_, index) => index)}>
{(index) => (
<input
id={`otp-${index}`}
class="otp-input"
inputMode="numeric"
maxlength={1}
value={otp()[index]}
onInput={(e) => setOtpDigit(index, e.currentTarget.value)}
/>
)}
</For>
</div>
<button
class="auth-submit-btn"
type="button"
disabled={submitting()}
onClick={() => void verifyOtp()}
>
{submitting() ? "Verifying..." : "Verify and Continue"}
</button>
<div class="auth-footer-row">
<p class="note">Didn't receive code?</p>
<button
class="auth-forgot-link"
type="button"
onClick={() => void resendOtp()}
disabled={submitting()}
>
Resend OTP
</button>
</div>
</Show>
</>
}
>
<h2 class="title">Create Customer Account</h2>
<p class="subtitle">
Register to post requirements and hire verified professionals.
</p>
<div class="grid" style={{ "grid-template-columns": "1fr 1fr", margin: 0 }}>
<div class="field">
<label class="label" for="first-name">FIRST NAME</label>
<input
id="first-name"
class="input"
value={firstName()}
onInput={(e) => setFirstName(e.currentTarget.value)}
placeholder="John"
/>
<p
class="validation-note"
style={{ color: firstName().trim() && firstNameValid() ? "#fd6116" : "#6e7591" }}
>
{firstName().trim() && firstNameValid()
? "✓ First name looks good"
: "• First name is required"}
</p>
</div>
<div class="field">
<label class="label" for="last-name">LAST NAME</label>
<input
id="last-name"
class="input"
value={lastName()}
onInput={(e) => setLastName(e.currentTarget.value)}
placeholder="Doe"
/>
<p
class="validation-note"
style={{ color: lastName().trim() && lastNameValid() ? "#fd6116" : "#6e7591" }}
>
{lastName().trim() && lastNameValid()
? "✓ Last name looks good"
: "• Last name is required"}
</p>
</div>
</div>
<div class="field">
<label class="label" for="email">EMAIL ADDRESS</label>
<input
id="email"
type="email"
class="input"
value={email()}
onInput={(e) => {
setEmail(e.currentTarget.value);
setEmailExists(false);
}}
onBlur={() => void checkEmailExists(email())}
placeholder="john.doe@example.com"
/>
<p
class="validation-note"
style={{
color: emailExists() ? "#dc2626" : email().trim() && emailValid() ? "#fd6116" : "#6e7591",
}}
>
{emailExists()
? "• This email is already registered"
: email().trim() && emailValid()
? "✓ Valid email format"
: "• Enter a valid email format"}
</p>
</div>
<div class="grid" style={{ "grid-template-columns": "1fr 1fr", margin: 0 }}>
<div class="field">
<label class="label" for="password">PASSWORD</label>
<div class="auth-password-wrap">
<input
id="password"
type={showPassword() ? "text" : "password"}
class="input"
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
/>
<button
class="auth-toggle-visibility"
type="button"
aria-label={showPassword() ? "Hide password" : "Show password"}
onClick={() => setShowPassword((s) => !s)}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
{showPassword() ? (
<>
<path d="M3 3l18 18" />
<path d="M10.58 10.58a2 2 0 0 0 2.83 2.83" />
</>
) : (
<>
<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z" />
<circle cx="12" cy="12" r="3" />
</>
)}
</svg>
</button>
</div>
<div class="password-strength-grid">
<p style={{ color: passwordChecks().minLength ? "#fd6116" : "#6e7591" }}>
{passwordChecks().minLength ? "✓" : "•"} 8+ chars
</p>
<p style={{ color: passwordChecks().uppercase ? "#fd6116" : "#6e7591" }}>
{passwordChecks().uppercase ? "✓" : "•"} Uppercase
</p>
<p style={{ color: passwordChecks().special ? "#fd6116" : "#6e7591" }}>
{passwordChecks().special ? "✓" : "•"} Special
</p>
<p style={{ color: passwordChecks().lowercase ? "#fd6116" : "#6e7591" }}>
{passwordChecks().lowercase ? "✓" : "•"} Lowercase
</p>
<p style={{ color: passwordChecks().number ? "#fd6116" : "#6e7591" }}>
{passwordChecks().number ? "✓" : "•"} Number
</p>
</div>
</div>
<div class="field">
<label class="label" for="confirm-password">CONFIRM PASSWORD</label>
<div class="auth-password-wrap">
<input
id="confirm-password"
type={showConfirmPassword() ? "text" : "password"}
class="input"
value={confirmPassword()}
onInput={(e) => setConfirmPassword(e.currentTarget.value)}
/>
<button
class="auth-toggle-visibility"
type="button"
aria-label={showConfirmPassword() ? "Hide password" : "Show password"}
onClick={() => setShowConfirmPassword((s) => !s)}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
{showConfirmPassword() ? (
<>
<path d="M3 3l18 18" />
<path d="M10.58 10.58a2 2 0 0 0 2.83 2.83" />
</>
) : (
<>
<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z" />
<circle cx="12" cy="12" r="3" />
</>
)}
</svg>
</button>
</div>
<p
class="validation-note"
style={{ color: confirmPassword() && passwordChecks().match ? "#fd6116" : "#6e7591" }}
>
{confirmPassword() && passwordChecks().match
? "✓ Passwords match"
: "• Passwords do not match"}
</p>
</div>
</div>
<div class="field">
<label class="label" for="captcha">CAPTCHA</label>
<div class="auth-captcha-row">
<button
type="button"
class="auth-captcha-refresh"
aria-label="Refresh captcha"
onClick={refreshCaptcha}
>
</button>
<CaptchaCanvas code={captchaCode()} class="auth-captcha-canvas" />
<input
id="captcha"
class="input"
value={captcha()}
onInput={(e) => setCaptcha(e.currentTarget.value.toUpperCase())}
placeholder="Enter captcha"
/>
</div>
<p
class="validation-note"
style={{
color: captcha() && captcha().toUpperCase() === captchaCode() ? "#fd6116" : "#6e7591",
}}
>
{captcha() && captcha().toUpperCase() === captchaCode()
? "✓ Captcha matched"
: "• Enter captcha to continue"}
</p>
</div>
<Show when={serverError()}>
<p class="error">{serverError()}</p>
</Show>
<button
class="auth-submit-btn"
type="button"
disabled={submitting() || !canSubmit()}
onClick={() => void register()}
>
{submitting() ? "Creating Account..." : "Create Customer Account"}
</button>
<div class="auth-footer-row">
<p class="footer-text">We will send a verification code to your email.</p>
<p class="note">
Already have an account? <A href="/login">Sign In</A>
</p>
</div>
</Show>
</section>
</div>
</main>
);
}