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.
This commit is contained in:
parent
9519337030
commit
bdf51ba3da
4 changed files with 1718 additions and 823 deletions
|
|
@ -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<string, string>;
|
||||
|
||||
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 (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M3 3l18 18" />
|
||||
<path d="M10.58 10.58a2 2 0 0 0 2.83 2.83" />
|
||||
<path d="M9.88 5.09A11 11 0 0 1 12 4.9c5.5 0 10 4.1 10 7.1 0 1.2-.72 2.53-1.95 3.72" />
|
||||
<path d="M6.1 6.1C3.54 7.58 2 9.79 2 12c0 3 4.48 7.1 10 7.1 1.72 0 3.36-.4 4.84-1.12" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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<RoleKey>(normalizeIntent(search.intent || search.role));
|
||||
const role = createMemo<RoleKey>(() => 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<RegisterErrors>({});
|
||||
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<HTMLInputElement>(`#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 (
|
||||
<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="Get Started" />
|
||||
<div class="auth-visual-overlay" />
|
||||
<div class="auth-visual-content">
|
||||
<p class="eyebrow">Get Started</p>
|
||||
<h1 class="title light">Create Your Nxtgauge Account</h1>
|
||||
<p class="subtitle light">
|
||||
Join verified opportunities and continue directly to your dashboard after signup.
|
||||
</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 login...
|
||||
</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 Your Account</h2>
|
||||
<p class="subtitle">
|
||||
Sign up first, then go directly to dashboard after email verification.
|
||||
</p>
|
||||
|
||||
{/* Role Selector - Only show tabs for job_seeker and company when no intent is provided */}
|
||||
<Show when={!search.intent && !search.role}>
|
||||
<div class="role-selector" style={{ display: "flex", gap: "8px", marginBottom: "24px" }}>
|
||||
<button
|
||||
type="button"
|
||||
class={`role-tab ${role() === "job_seeker" ? "active" : ""}`}
|
||||
onClick={() => setSelectedRole("job_seeker")}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "14px 16px",
|
||||
border: role() === "job_seeker" ? "2px solid #fd6116" : "2px solid #e5e7eb",
|
||||
"border-radius": "8px",
|
||||
background: role() === "job_seeker" ? "#fff5f0" : "#fff",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
gap: "10px",
|
||||
"font-size": "15px",
|
||||
"font-weight": role() === "job_seeker" ? "600" : "500",
|
||||
color: role() === "job_seeker" ? "#fd6116" : "#4b5563",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
Job Seeker
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`role-tab ${role() === "company" ? "active" : ""}`}
|
||||
onClick={() => setSelectedRole("company")}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "14px 16px",
|
||||
border: role() === "company" ? "2px solid #fd6116" : "2px solid #e5e7eb",
|
||||
"border-radius": "8px",
|
||||
background: role() === "company" ? "#fff5f0" : "#fff",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
gap: "10px",
|
||||
"font-size": "15px",
|
||||
"font-weight": role() === "company" ? "600" : "500",
|
||||
color: role() === "company" ? "#fd6116" : "#4b5563",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 21h18"/>
|
||||
<path d="M9 21V8l-6 4 6 9h6v-9l-6-4 6-3"/>
|
||||
<path d="M9 3h6v5H9z"/>
|
||||
</svg>
|
||||
Company
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Show role badge for professional/customer when coming via direct link */}
|
||||
<Show when={search.intent === "professional" || search.intent === "customer" || search.role === "professional" || search.role === "customer"}>
|
||||
<div style={{
|
||||
"margin-bottom": "24px",
|
||||
padding: "12px 16px",
|
||||
"background-color": "#fff5f0",
|
||||
"border-radius": "8px",
|
||||
"border-left": "4px solid #fd6116"
|
||||
}}>
|
||||
<p style={{ margin: 0, "font-weight": "600", color: "#fd6116" }}>
|
||||
Registering as: {role() === "professional" ? "Professional" : role() === "customer" ? "Customer" : role()}
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="grid" style={{ "grid-template-columns": "1fr 1fr", margin: 0 }}>
|
||||
<div class="field">
|
||||
<label class="label" for="first-name">
|
||||
FULL NAME
|
||||
</label>
|
||||
<input
|
||||
id="first-name"
|
||||
class="input"
|
||||
value={firstName()}
|
||||
onInput={(e) => setFirstName(e.currentTarget.value)}
|
||||
/>
|
||||
<p
|
||||
class="validation-note"
|
||||
style={{ color: firstName().trim() && firstNameValid() ? "#fd6116" : "#6e7591" }}
|
||||
>
|
||||
{firstName().trim() && firstNameValid()
|
||||
? "✓ First name looks good"
|
||||
: "• First name is required"}
|
||||
</p>
|
||||
</div>
|
||||
<Show
|
||||
when={role() === "company"}
|
||||
fallback={
|
||||
<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)}
|
||||
/>
|
||||
<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 class="field">
|
||||
<label class="label" for="company-name">
|
||||
COMPANY NAME
|
||||
</label>
|
||||
<input
|
||||
id="company-name"
|
||||
class="input"
|
||||
value={companyName()}
|
||||
onInput={(e) => setCompanyName(e.currentTarget.value)}
|
||||
/>
|
||||
<p
|
||||
class="validation-note"
|
||||
style={{ color: companyName().trim() ? "#fd6116" : "#6e7591" }}
|
||||
>
|
||||
{companyName().trim()
|
||||
? "✓ Company name looks good"
|
||||
: "• Company name is required"}
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</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());
|
||||
}}
|
||||
/>
|
||||
<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"
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
aria-label={showPassword() ? "Hide password" : "Show password"}
|
||||
>
|
||||
<PasswordVisibilityIcon visible={showPassword()} />
|
||||
</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"
|
||||
onClick={() => setShowConfirmPassword((prev) => !prev)}
|
||||
aria-label={showConfirmPassword() ? "Hide password" : "Show password"}
|
||||
>
|
||||
<PasswordVisibilityIcon visible={showConfirmPassword()} />
|
||||
</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"
|
||||
onClick={refreshCaptcha}
|
||||
aria-label="Refresh captcha"
|
||||
>
|
||||
↻
|
||||
</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() && isValidCaptcha(captcha(), captchaCode()) ? "#fd6116" : "#6e7591",
|
||||
}}
|
||||
>
|
||||
{captcha()
|
||||
? isValidCaptcha(captcha(), captchaCode())
|
||||
? "✓ Captcha matched"
|
||||
: "• Captcha does not match"
|
||||
: "• Enter captcha to continue"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field" style={{ "margin-top": "16px" }}>
|
||||
<label
|
||||
class="auth-checkbox-wrapper"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setTermsAccepted(v => !v);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="terms-check"
|
||||
class="visually-hidden"
|
||||
checked={termsAccepted()}
|
||||
/>
|
||||
<span class="auth-checkbox-custom">
|
||||
{termsAccepted() ? "✓" : ""}
|
||||
</span>
|
||||
<span class="auth-checkbox-label">
|
||||
I agree to the <A href="/terms" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>Terms and Conditions</A> and{" "}
|
||||
<A href="/privacy" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>Privacy Policy</A>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="auth-submit-btn"
|
||||
type="button"
|
||||
disabled={submitting()}
|
||||
onClick={() => void register()}
|
||||
>
|
||||
{submitting() ? "Creating Account..." : "Sign Up"}
|
||||
</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>
|
||||
|
||||
<Show when={serverError()}>
|
||||
<p class="error">{serverError()}</p>
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100vh",
|
||||
background: "#07051a"
|
||||
}}>
|
||||
<p style={{ color: "#fff" }}>Redirecting..."</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
548
src/routes/signup/company.tsx
Normal file
548
src/routes/signup/company.tsx
Normal file
|
|
@ -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<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 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<HTMLInputElement>(`#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 (
|
||||
<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="Company Registration" />
|
||||
<div class="auth-visual-overlay" />
|
||||
<div class="auth-visual-content">
|
||||
<p class="eyebrow">Company Registration</p>
|
||||
<h1 class="title light">Register Your Company</h1>
|
||||
<p class="subtitle light">
|
||||
Post verified jobs and hire faster through our trust-first workflow.
|
||||
</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 Company Account</h2>
|
||||
<p class="subtitle">
|
||||
Register your company to post jobs and hire verified talent.
|
||||
</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="first-name">CONTACT PERSON NAME</label>
|
||||
<input
|
||||
id="first-name"
|
||||
class="input"
|
||||
value={firstName()}
|
||||
onInput={(e) => setFirstName(e.currentTarget.value)}
|
||||
placeholder="Your name"
|
||||
/>
|
||||
<p
|
||||
class="validation-note"
|
||||
style={{ color: firstName().trim() && firstNameValid() ? "#fd6116" : "#6e7591" }}
|
||||
>
|
||||
{firstName().trim() && firstNameValid()
|
||||
? "✓ Name looks good"
|
||||
: "• Name is required"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="company-name">COMPANY NAME</label>
|
||||
<input
|
||||
id="company-name"
|
||||
class="input"
|
||||
value={companyName()}
|
||||
onInput={(e) => setCompanyName(e.currentTarget.value)}
|
||||
placeholder="Your company name"
|
||||
/>
|
||||
<p
|
||||
class="validation-note"
|
||||
style={{ color: companyName().trim() && companyNameValid() ? "#fd6116" : "#6e7591" }}
|
||||
>
|
||||
{companyName().trim() && companyNameValid()
|
||||
? "✓ Company name looks good"
|
||||
: "• Company name is required"}
|
||||
</p>
|
||||
</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="company@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 Company 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>
|
||||
<p class="note" style={{ "margin-top": "8px" }}>
|
||||
<A href="/signup/job-seeker">Register as Job Seeker instead</A>
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
547
src/routes/signup/job-seeker.tsx
Normal file
547
src/routes/signup/job-seeker.tsx
Normal file
|
|
@ -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<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: "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 (
|
||||
<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="Job Seeker Registration" />
|
||||
<div class="auth-visual-overlay" />
|
||||
<div class="auth-visual-content">
|
||||
<p class="eyebrow">Job Seeker Registration</p>
|
||||
<h1 class="title light">Find Your Dream Job</h1>
|
||||
<p class="subtitle light">
|
||||
Apply to verified opportunities with a stronger profile and clearer status flow.
|
||||
</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 Job Seeker Account</h2>
|
||||
<p class="subtitle">
|
||||
Build your profile and apply to approved opportunities quickly.
|
||||
</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 Job Seeker 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>
|
||||
<p class="note" style={{ "margin-top": "8px" }}>
|
||||
<A href="/signup/company">Register as Company instead</A>
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
586
src/routes/signup/professional.tsx
Normal file
586
src/routes/signup/professional.tsx
Normal file
|
|
@ -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<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() &&
|
||||
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<HTMLInputElement>(`#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 (
|
||||
<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="Professional Registration" />
|
||||
<div class="auth-visual-overlay" />
|
||||
<div class="auth-visual-content">
|
||||
<p class="eyebrow">Professional Registration</p>
|
||||
<h1 class="title light">Join as a Professional</h1>
|
||||
<p class="subtitle light">
|
||||
Create a trusted profile and grow through verified demand.
|
||||
</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 Professional Account</h2>
|
||||
<p class="subtitle">
|
||||
Select your profession and build your trusted profile.
|
||||
</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="profession">PROFESSION</label>
|
||||
<select
|
||||
id="profession"
|
||||
class="input"
|
||||
value={profession()}
|
||||
onChange={(e) => setProfession(e.currentTarget.value)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<option value="">Select your profession</option>
|
||||
<For each={professions}>
|
||||
{(p) => <option value={p.value}>{p.label}</option>}
|
||||
</For>
|
||||
</select>
|
||||
<p
|
||||
class="validation-note"
|
||||
style={{ color: profession().trim() ? "#fd6116" : "#6e7591" }}
|
||||
>
|
||||
{profession().trim() ? "✓ Profession selected" : "• Select your profession"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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 Professional 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue