2026-04-05 16:52:02 +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';
|
|
|
|
|
|
import {
|
|
|
|
|
|
checkPasswordStrength,
|
|
|
|
|
|
isPasswordStrong,
|
|
|
|
|
|
isValidCaptcha,
|
|
|
|
|
|
isValidEmail,
|
|
|
|
|
|
isValidName,
|
|
|
|
|
|
validateRegisterForm,
|
|
|
|
|
|
} from '~/lib/form-validation';
|
|
|
|
|
|
|
|
|
|
|
|
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';
|
2026-04-06 01:47:05 +02:00
|
|
|
|
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';
|
2026-04-05 16:52:02 +02:00
|
|
|
|
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() {
|
|
|
|
|
|
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
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
const selectedProfessionalRole = createMemo(() => String(search.role || '').trim().toUpperCase());
|
2026-04-05 16:52:02 +02:00
|
|
|
|
const [termsAccepted, setTermsAccepted] = createSignal(false);
|
|
|
|
|
|
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 [pendingPassword, setPendingPassword] = createSignal('');
|
|
|
|
|
|
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()) &&
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/gateway/api/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;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (clean) {
|
|
|
|
|
|
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 }) => {
|
|
|
|
|
|
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,
|
|
|
|
|
|
};
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const storeAuth = (payload: any) => {
|
|
|
|
|
|
const accessToken = String(payload?.access_token || '').trim();
|
|
|
|
|
|
if (typeof window !== 'undefined' && accessToken) {
|
|
|
|
|
|
window.sessionStorage.setItem('nxtgauge_access_token', accessToken);
|
|
|
|
|
|
window.sessionStorage.setItem('nxtgauge_frontend_access_token', accessToken);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const register = async () => {
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch('/api/gateway/api/auth/register', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
full_name: `${firstName().trim()} ${lastName().trim()}`.trim(),
|
|
|
|
|
|
email: email().trim().toLowerCase(),
|
|
|
|
|
|
password: password(),
|
|
|
|
|
|
phone: null,
|
2026-04-08 22:40:43 +02:00
|
|
|
|
intent: role(),
|
|
|
|
|
|
profession: selectedProfessionalRole() || undefined,
|
2026-04-05 16:52:02 +02:00
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
setPendingPassword(password());
|
|
|
|
|
|
saveUserForDashboard({
|
|
|
|
|
|
firstName: firstName().trim(),
|
|
|
|
|
|
lastName: lastName().trim(),
|
|
|
|
|
|
email: cleanEmail,
|
|
|
|
|
|
roleKey: role(),
|
|
|
|
|
|
});
|
|
|
|
|
|
setStep('verify');
|
|
|
|
|
|
setOtp(['', '', '', '', '', '']);
|
|
|
|
|
|
} 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/api/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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const loginRes = await fetch('/api/gateway/api/auth/login', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
email: pendingEmail() || email().trim().toLowerCase(),
|
|
|
|
|
|
password: pendingPassword() || password(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
const loginData = await loginRes.json().catch(() => ({}));
|
|
|
|
|
|
if (!loginRes.ok) {
|
|
|
|
|
|
setServerError(String(loginData?.error || loginData?.message || 'Email verified. Please login manually.'));
|
|
|
|
|
|
navigate('/login', { replace: true });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
storeAuth(loginData);
|
|
|
|
|
|
saveUserForDashboard({
|
|
|
|
|
|
firstName: firstName().trim(),
|
|
|
|
|
|
lastName: lastName().trim(),
|
|
|
|
|
|
email: pendingEmail() || email().trim().toLowerCase(),
|
|
|
|
|
|
roleKey: role(),
|
|
|
|
|
|
user: loginData?.user,
|
|
|
|
|
|
});
|
|
|
|
|
|
navigate('/dashboard', { replace: true });
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const resendOtp = async () => {
|
|
|
|
|
|
setServerError('');
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch('/api/gateway/api/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.'));
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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>
|
|
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</>
|
|
|
|
|
|
}>
|
|
|
|
|
|
<h2 class="title">Create Your Account</h2>
|
|
|
|
|
|
<p class="subtitle">Sign up first, then go directly to dashboard after email verification.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<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">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button class="auth-submit-btn" type="button" disabled={submitting() || !canSubmit()} 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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|