2026-04-15 06:23:28 +02:00
|
|
|
|
import { A, useNavigate, useSearchParams } 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";
|
2026-04-05 16:52:02 +02:00
|
|
|
|
import {
|
|
|
|
|
|
checkPasswordStrength,
|
|
|
|
|
|
isPasswordStrong,
|
|
|
|
|
|
isValidCaptcha,
|
|
|
|
|
|
isValidEmail,
|
|
|
|
|
|
isValidName,
|
|
|
|
|
|
validateRegisterForm,
|
2026-04-15 06:23:28 +02:00
|
|
|
|
} from "~/lib/form-validation";
|
2026-04-05 16:52:02 +02:00
|
|
|
|
|
2026-04-15 06:23:28 +02:00
|
|
|
|
type RoleKey = "company" | "job_seeker" | "professional" | "customer";
|
2026-04-05 16:52:02 +02:00
|
|
|
|
|
|
|
|
|
|
type RegisterErrors = Record<string, string>;
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeIntent(intent: string | null | undefined): RoleKey {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
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";
|
2026-04-05 16:52:02 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function randomCaptcha(length = 6): string {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
|
|
|
|
let out = "";
|
2026-04-05 16:52:02 +02:00
|
|
|
|
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() {
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const [search] = useSearchParams();
|
|
|
|
|
|
onMount(() => {
|
2026-04-06 03:33:29 +02:00
|
|
|
|
// Legacy redirect to choose-role removed for dashboard-first flow.
|
|
|
|
|
|
// If no intent/role provided, normalizeIntent will default to job_seeker.
|
2026-04-05 16:52:02 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-15 06:23:28 +02:00
|
|
|
|
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("");
|
2026-04-06 01:47:05 +02:00
|
|
|
|
const role = createMemo<RoleKey>(() => normalizeIntent(search.intent || search.role));
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const selectedProfessionalRole = createMemo(() =>
|
|
|
|
|
|
String(search.role || "")
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.toUpperCase()
|
|
|
|
|
|
);
|
2026-04-05 16:52:02 +02:00
|
|
|
|
const [termsAccepted, setTermsAccepted] = createSignal(false);
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const [captcha, setCaptcha] = createSignal("");
|
2026-04-05 16:52:02 +02:00
|
|
|
|
const [captchaCode, setCaptchaCode] = createSignal(randomCaptcha());
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const [otp, setOtp] = createSignal(["", "", "", "", "", ""]);
|
2026-04-05 16:52:02 +02:00
|
|
|
|
const [errors, setErrors] = createSignal<RegisterErrors>({});
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const [serverError, setServerError] = createSignal("");
|
2026-04-05 16:52:02 +02:00
|
|
|
|
const [emailExists, setEmailExists] = createSignal(false);
|
|
|
|
|
|
const [submitting, setSubmitting] = createSignal(false);
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const [pendingEmail, setPendingEmail] = createSignal("");
|
2026-04-10 01:21:36 +02:00
|
|
|
|
const [verifiedSuccess, setVerifiedSuccess] = createSignal(false);
|
2026-04-05 16:52:02 +02:00
|
|
|
|
const [showPassword, setShowPassword] = createSignal(false);
|
|
|
|
|
|
const [showConfirmPassword, setShowConfirmPassword] = createSignal(false);
|
|
|
|
|
|
|
|
|
|
|
|
const passwordChecks = createMemo(() => checkPasswordStrength(password(), confirmPassword()));
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const otpCode = createMemo(() => otp().join(""));
|
2026-04-05 16:52:02 +02:00
|
|
|
|
const firstNameValid = createMemo(() => !firstName().trim() || isValidName(firstName()));
|
|
|
|
|
|
const lastNameValid = createMemo(() => !lastName().trim() || isValidName(lastName()));
|
|
|
|
|
|
const emailValid = createMemo(() => !email().trim() || isValidEmail(email()));
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const canSubmit = createMemo(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
firstName().trim().length > 0 &&
|
|
|
|
|
|
firstNameValid() &&
|
|
|
|
|
|
lastName().trim().length > 0 &&
|
|
|
|
|
|
lastNameValid() &&
|
|
|
|
|
|
emailValid() &&
|
|
|
|
|
|
isValidEmail(email()) &&
|
|
|
|
|
|
isPasswordStrong(passwordChecks()) &&
|
|
|
|
|
|
passwordChecks().match &&
|
|
|
|
|
|
isValidCaptcha(captcha(), captchaCode()) &&
|
|
|
|
|
|
termsAccepted() &&
|
|
|
|
|
|
!emailExists()
|
2026-04-05 16:52:02 +02:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const refreshCaptcha = () => {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
setCaptcha("");
|
2026-04-05 16:52:02 +02:00
|
|
|
|
setCaptchaCode(randomCaptcha());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const checkEmailExists = async (emailValue: string) => {
|
|
|
|
|
|
const normalized = emailValue.trim().toLowerCase();
|
|
|
|
|
|
if (!normalized || !isValidEmail(normalized)) {
|
|
|
|
|
|
setEmailExists(false);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const response = await fetch("/api/gateway/api/auth/check-email", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
|
|
|
|
credentials: "include",
|
2026-04-05 16:52:02 +02:00
|
|
|
|
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) => {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const clean = value.replace(/\D/g, "").slice(0, 1);
|
2026-04-05 16:52:02 +02:00
|
|
|
|
setOtp((prev) => {
|
|
|
|
|
|
const next = prev.slice();
|
|
|
|
|
|
next[index] = clean;
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (clean) {
|
|
|
|
|
|
const nextEl = document.querySelector<HTMLInputElement>(`#otp-${index + 1}`);
|
|
|
|
|
|
if (nextEl) nextEl.focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const saveUserForDashboard = (input: {
|
|
|
|
|
|
firstName: string;
|
|
|
|
|
|
lastName: string;
|
|
|
|
|
|
email: string;
|
|
|
|
|
|
roleKey: RoleKey;
|
|
|
|
|
|
user?: any;
|
|
|
|
|
|
}) => {
|
2026-04-05 16:52:02 +02:00
|
|
|
|
const fullName = `${input.firstName} ${input.lastName}`.trim();
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
firstName: input.firstName,
|
|
|
|
|
|
lastName: input.lastName,
|
|
|
|
|
|
fullName,
|
|
|
|
|
|
name: fullName,
|
|
|
|
|
|
displayName: fullName,
|
|
|
|
|
|
email: input.email.toLowerCase(),
|
|
|
|
|
|
roleKey: input.roleKey,
|
|
|
|
|
|
role: input.roleKey,
|
2026-04-06 01:47:05 +02:00
|
|
|
|
selectedProfessionalRole: selectedProfessionalRole() || null,
|
2026-04-05 16:52:02 +02:00
|
|
|
|
user: input.user || null,
|
|
|
|
|
|
};
|
2026-04-15 06:23:28 +02:00
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
|
window.localStorage.setItem("nxtgauge_signup_profile_v1", JSON.stringify(payload));
|
|
|
|
|
|
window.localStorage.setItem("nxtgauge_auth_user", JSON.stringify(payload));
|
|
|
|
|
|
window.localStorage.setItem("nxtgauge_user", JSON.stringify(payload));
|
2026-04-05 16:52:02 +02:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const register = async () => {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
setServerError("");
|
2026-04-05 16:52:02 +02:00
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
try {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const res = await fetch("/api/auth/register", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
|
|
|
|
credentials: "include",
|
2026-04-05 16:52:02 +02:00
|
|
|
|
body: JSON.stringify({
|
2026-04-16 10:35:40 +02:00
|
|
|
|
first_name: firstName().trim(),
|
|
|
|
|
|
last_name: lastName().trim(),
|
2026-04-05 16:52:02 +02:00
|
|
|
|
email: email().trim().toLowerCase(),
|
|
|
|
|
|
password: password(),
|
2026-04-15 06:23:28 +02:00
|
|
|
|
phone: "",
|
2026-04-08 22:40:43 +02:00
|
|
|
|
intent: role(),
|
2026-04-15 06:23:28 +02:00
|
|
|
|
role_key: selectedProfessionalRole() || undefined,
|
2026-04-05 16:52:02 +02:00
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
|
if (!res.ok) {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
setServerError(String(data?.error || data?.message || "Unable to create account."));
|
2026-04-05 16:52:02 +02:00
|
|
|
|
refreshCaptcha();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cleanEmail = email().trim().toLowerCase();
|
|
|
|
|
|
setPendingEmail(cleanEmail);
|
2026-04-10 01:21:36 +02:00
|
|
|
|
setVerifiedSuccess(false);
|
2026-04-05 16:52:02 +02:00
|
|
|
|
saveUserForDashboard({
|
|
|
|
|
|
firstName: firstName().trim(),
|
|
|
|
|
|
lastName: lastName().trim(),
|
|
|
|
|
|
email: cleanEmail,
|
|
|
|
|
|
roleKey: role(),
|
|
|
|
|
|
});
|
2026-04-15 06:23:28 +02:00
|
|
|
|
setStep("verify");
|
|
|
|
|
|
setOtp(["", "", "", "", "", ""]);
|
2026-04-05 16:52:02 +02:00
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const verifyOtp = async () => {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
setServerError("");
|
2026-04-05 16:52:02 +02:00
|
|
|
|
if (otpCode().length !== 6) {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
setServerError("Enter the 6-digit code sent to your email.");
|
2026-04-05 16:52:02 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
try {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const verifyRes = await fetch("/api/gateway/api/auth/verify-email", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
|
|
|
|
credentials: "include",
|
2026-04-05 16:52:02 +02:00
|
|
|
|
body: JSON.stringify({ otp: otpCode() }),
|
|
|
|
|
|
});
|
|
|
|
|
|
const verifyData = await verifyRes.json().catch(() => ({}));
|
|
|
|
|
|
if (!verifyRes.ok) {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
setServerError(String(verifyData?.error || verifyData?.message || "Verification failed."));
|
2026-04-05 16:52:02 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-10 01:21:36 +02:00
|
|
|
|
setVerifiedSuccess(true);
|
2026-04-15 06:23:28 +02:00
|
|
|
|
setTimeout(() => navigate("/login?verified=1", { replace: true }), 1400);
|
2026-04-05 16:52:02 +02:00
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const resendOtp = async () => {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
setServerError("");
|
2026-04-05 16:52:02 +02:00
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
try {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
const res = await fetch("/api/gateway/api/auth/resend-otp", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
|
|
|
|
credentials: "include",
|
2026-04-05 16:52:02 +02:00
|
|
|
|
body: JSON.stringify({ email: pendingEmail() || email().trim().toLowerCase() }),
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
|
if (!res.ok) {
|
2026-04-15 06:23:28 +02:00
|
|
|
|
setServerError(String(data?.error || data?.message || "Unable to resend OTP right now."));
|
2026-04-05 16:52:02 +02:00
|
|
|
|
}
|
|
|
|
|
|
} 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>
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<p class="subtitle light">
|
|
|
|
|
|
Join verified opportunities and continue directly to your dashboard after signup.
|
|
|
|
|
|
</p>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="auth-form card glass-light">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<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>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<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>
|
2026-04-10 01:21:36 +02:00
|
|
|
|
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<button
|
|
|
|
|
|
class="auth-submit-btn"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={submitting()}
|
|
|
|
|
|
onClick={() => void verifyOtp()}
|
|
|
|
|
|
>
|
|
|
|
|
|
{submitting() ? "Verifying..." : "Verify and Continue"}
|
2026-04-10 01:21:36 +02:00
|
|
|
|
</button>
|
2026-04-15 06:23:28 +02:00
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<h2 class="title">Create Your Account</h2>
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<p class="subtitle">
|
|
|
|
|
|
Sign up first, then go directly to dashboard after email verification.
|
|
|
|
|
|
</p>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<div class="grid" style={{ "grid-template-columns": "1fr 1fr", margin: 0 }}>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<div class="field">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<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"}
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<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"}
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<label class="label" for="email">
|
|
|
|
|
|
EMAIL ADDRESS
|
|
|
|
|
|
</label>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<input
|
|
|
|
|
|
id="email"
|
|
|
|
|
|
type="email"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
value={email()}
|
|
|
|
|
|
onInput={(e) => {
|
|
|
|
|
|
setEmail(e.currentTarget.value);
|
|
|
|
|
|
setEmailExists(false);
|
|
|
|
|
|
}}
|
2026-04-15 06:23:28 +02:00
|
|
|
|
onBlur={() => {
|
|
|
|
|
|
void checkEmailExists(email());
|
|
|
|
|
|
}}
|
2026-04-05 16:52:02 +02:00
|
|
|
|
/>
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<p
|
|
|
|
|
|
class="validation-note"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: emailExists()
|
|
|
|
|
|
? "#dc2626"
|
|
|
|
|
|
: email().trim() && emailValid()
|
|
|
|
|
|
? "#fd6116"
|
|
|
|
|
|
: "#6e7591",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
{emailExists()
|
2026-04-15 06:23:28 +02:00
|
|
|
|
? "• This email is already registered"
|
|
|
|
|
|
: email().trim() && emailValid()
|
|
|
|
|
|
? "✓ Valid email format"
|
|
|
|
|
|
: "• Enter a valid email format"}
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<div class="grid" style={{ "grid-template-columns": "1fr 1fr", margin: 0 }}>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<div class="field">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<label class="label" for="password">
|
|
|
|
|
|
PASSWORD
|
|
|
|
|
|
</label>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<div class="auth-password-wrap">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<input
|
|
|
|
|
|
id="password"
|
|
|
|
|
|
type={showPassword() ? "text" : "password"}
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
value={password()}
|
|
|
|
|
|
onInput={(e) => setPassword(e.currentTarget.value)}
|
|
|
|
|
|
/>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<button
|
|
|
|
|
|
class="auth-toggle-visibility"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowPassword((prev) => !prev)}
|
2026-04-15 06:23:28 +02:00
|
|
|
|
aria-label={showPassword() ? "Hide password" : "Show password"}
|
2026-04-05 16:52:02 +02:00
|
|
|
|
>
|
|
|
|
|
|
<PasswordVisibilityIcon visible={showPassword()} />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="password-strength-grid">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<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>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<label class="label" for="confirm-password">
|
|
|
|
|
|
CONFIRM PASSWORD
|
|
|
|
|
|
</label>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<div class="auth-password-wrap">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<input
|
|
|
|
|
|
id="confirm-password"
|
|
|
|
|
|
type={showConfirmPassword() ? "text" : "password"}
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
value={confirmPassword()}
|
|
|
|
|
|
onInput={(e) => setConfirmPassword(e.currentTarget.value)}
|
|
|
|
|
|
/>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<button
|
|
|
|
|
|
class="auth-toggle-visibility"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowConfirmPassword((prev) => !prev)}
|
2026-04-15 06:23:28 +02:00
|
|
|
|
aria-label={showConfirmPassword() ? "Hide password" : "Show password"}
|
2026-04-05 16:52:02 +02:00
|
|
|
|
>
|
|
|
|
|
|
<PasswordVisibilityIcon visible={showConfirmPassword()} />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<p
|
|
|
|
|
|
class="validation-note"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: confirmPassword() && passwordChecks().match ? "#fd6116" : "#6e7591",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{confirmPassword() && passwordChecks().match
|
|
|
|
|
|
? "✓ Passwords match"
|
|
|
|
|
|
: "• Passwords do not match"}
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<label class="label" for="captcha">
|
|
|
|
|
|
CAPTCHA
|
|
|
|
|
|
</label>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<div class="auth-captcha-row">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="auth-captcha-refresh"
|
|
|
|
|
|
onClick={refreshCaptcha}
|
|
|
|
|
|
aria-label="Refresh captcha"
|
|
|
|
|
|
>
|
|
|
|
|
|
↻
|
|
|
|
|
|
</button>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<CaptchaCanvas code={captchaCode()} class="auth-captcha-canvas" />
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<input
|
|
|
|
|
|
id="captcha"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
value={captcha()}
|
|
|
|
|
|
onInput={(e) => setCaptcha(e.currentTarget.value.toUpperCase())}
|
|
|
|
|
|
placeholder="Enter captcha"
|
|
|
|
|
|
/>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</div>
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<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"}
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<div class="field" style={{ "margin-top": "16px" }}>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
<label class="auth-checkbox-wrapper">
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<input
|
|
|
|
|
|
class="auth-checkbox"
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={termsAccepted()}
|
|
|
|
|
|
onChange={(e) => setTermsAccepted(e.currentTarget.checked)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="auth-checkbox-label">
|
|
|
|
|
|
I agree to the <A href="/terms">Terms and Conditions</A> and{" "}
|
|
|
|
|
|
<A href="/privacy">Privacy Policy</A>
|
|
|
|
|
|
</span>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<button
|
|
|
|
|
|
class="auth-submit-btn"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={submitting() || !canSubmit()}
|
|
|
|
|
|
onClick={() => void register()}
|
|
|
|
|
|
>
|
|
|
|
|
|
{submitting() ? "Creating Account..." : "Sign Up"}
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="auth-footer-row">
|
|
|
|
|
|
<p class="footer-text">We will send a verification code to your email.</p>
|
2026-04-15 06:23:28 +02:00
|
|
|
|
<p class="note">
|
|
|
|
|
|
Already have an account? <A href="/login">Sign In</A>
|
|
|
|
|
|
</p>
|
2026-04-05 16:52:02 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
|
|
<Show when={serverError()}>
|
|
|
|
|
|
<p class="error">{serverError()}</p>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|