2026-04-05 16:52:02 +02:00
import { A , useNavigate } from '@solidjs/router' ;
import { createMemo , createSignal , For , Show } from 'solid-js' ;
2026-04-06 06:19:23 +02:00
import { useAuth } from '~/lib/auth' ;
2026-04-05 16:52:02 +02:00
import PublicBackground from '~/components/PublicBackground' ;
import PublicHeader from '~/components/PublicHeader' ;
import CaptchaCanvas from '~/components/CaptchaCanvas' ;
import { isValidEmail } from '~/lib/form-validation' ;
type RoleKey = 'company' | 'job_seeker' | 'professional' | 'customer' ;
2026-04-15 06:23:28 +02:00
function normalizeRoleValue ( value : unknown ) : string {
return String ( value || '' ) . trim ( ) . toUpperCase ( ) . replace ( /\s+/g , '_' ) ;
}
function getStoredPreferredRole ( emailHint? : string ) : string | null {
if ( typeof window === 'undefined' ) return null ;
const keys = [ 'nxtgauge_signup_profile_v1' , 'nxtgauge_auth_user' , 'nxtgauge_user' ] ;
for ( const key of keys ) {
const raw = window . localStorage . getItem ( key ) ;
if ( ! raw ) continue ;
try {
const parsed = JSON . parse ( raw ) as Record < string , any > ;
const storedEmail = String ( parsed ? . email || '' ) . trim ( ) . toLowerCase ( ) ;
if ( emailHint && storedEmail && storedEmail !== emailHint . trim ( ) . toLowerCase ( ) ) continue ;
const selectedProfessionalRole = normalizeRoleValue ( parsed ? . selectedProfessionalRole ) ;
if ( selectedProfessionalRole ) return selectedProfessionalRole ;
const activeRole = normalizeRoleValue ( parsed ? . active_role || parsed ? . role ) ;
if ( activeRole ) return activeRole ;
} catch {
// Ignore malformed local storage payloads.
}
}
return null ;
}
function resolveActiveRole ( rawBackendRole : unknown , emailHint? : string ) : string {
const backendRole = normalizeRoleValue ( rawBackendRole ) ;
if ( backendRole ) return backendRole ;
const preferredRole = getStoredPreferredRole ( emailHint ) ;
if ( preferredRole ) return preferredRole ;
return preferredRole || 'JOB_SEEKER' ;
}
2026-04-05 16:52:02 +02:00
function makeCaptcha() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' ;
return Array . from ( { length : 6 } , ( ) = > chars [ Math . floor ( Math . random ( ) * chars . length ) ] ) . join ( '' ) ;
}
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 LoginRoute() {
const navigate = useNavigate ( ) ;
2026-04-06 06:19:23 +02:00
const auth = useAuth ( ) ;
2026-04-05 16:52:02 +02:00
const [ email , setEmail ] = createSignal ( '' ) ;
const [ password , setPassword ] = createSignal ( '' ) ;
const [ otp , setOtp ] = createSignal ( [ '' , '' , '' , '' , '' , '' ] ) ;
const [ showVerify , setShowVerify ] = createSignal ( false ) ;
const [ showPassword , setShowPassword ] = createSignal ( false ) ;
const [ captcha , setCaptcha ] = createSignal ( makeCaptcha ( ) ) ;
const [ captchaInput , setCaptchaInput ] = createSignal ( '' ) ;
const [ error , setError ] = createSignal ( '' ) ;
const [ submitting , setSubmitting ] = createSignal ( false ) ;
const [ roleGuess , setRoleGuess ] = createSignal < RoleKey > ( 'job_seeker' ) ;
2026-04-15 06:23:28 +02:00
const [ roleHint , setRoleHint ] = createSignal ( '' ) ;
const [ checkingRole , setCheckingRole ] = createSignal ( false ) ;
2026-04-05 16:52:02 +02:00
const otpCode = createMemo ( ( ) = > otp ( ) . join ( '' ) ) ;
2026-04-15 06:23:28 +02:00
const formatRoleLabel = ( value : string ) : string = >
String ( value || '' )
. trim ( )
. replace ( /[_\s]+/g , ' ' )
. toLowerCase ( )
. replace ( /\b\w/g , ( ch ) = > ch . toUpperCase ( ) ) ;
const lookupRoleByEmail = async ( emailValue : string ) = > {
const normalized = emailValue . trim ( ) . toLowerCase ( ) ;
if ( ! normalized || ! isValidEmail ( normalized ) ) {
setRoleHint ( '' ) ;
return ;
}
setCheckingRole ( true ) ;
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 ( ( ) = > ( { } ) ) ;
if ( ! response . ok || ! payload ? . exists ) {
setRoleHint ( '' ) ;
return ;
}
const detectedRole = normalizeRoleValue (
payload ? . active_role || payload ? . role || payload ? . roles ? . [ 0 ]
) ;
if ( ! detectedRole ) {
const fallbackRole = normalizeRoleValue ( getStoredPreferredRole ( normalized ) ) ;
setRoleHint (
fallbackRole
? ` Role: ${ formatRoleLabel ( fallbackRole ) } `
: 'Role: Not assigned'
) ;
return ;
}
setRoleHint ( ` Role: ${ formatRoleLabel ( detectedRole ) } ` ) ;
const roleLower = detectedRole . toLowerCase ( ) ;
if ( roleLower === 'company' || roleLower === 'customer' || roleLower === 'job_seeker' || roleLower === 'professional' ) {
setRoleGuess ( roleLower as RoleKey ) ;
}
} catch {
setRoleHint ( '' ) ;
} finally {
setCheckingRole ( false ) ;
}
} ;
2026-04-05 16:52:02 +02:00
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 > ( ` #login-otp- ${ index + 1 } ` ) ;
if ( nextEl ) nextEl . focus ( ) ;
}
} ;
const saveUser = ( user : any ) = > {
const fullName = String ( user ? . full_name || user ? . fullName || '' ) . trim ( ) ;
const [ firstName , . . . rest ] = fullName . split ( ' ' ) ;
const lastName = rest . join ( ' ' ) ;
2026-04-15 06:23:28 +02:00
const normalizedRole = resolveActiveRole (
user ? . active_role || user ? . role || roleGuess ( ) ,
String ( user ? . email || email ( ) )
) ;
2026-04-05 16:52:02 +02:00
const storedRole = normalizedRole
? normalizedRole . toLowerCase ( )
: roleGuess ( ) ;
2026-04-15 06:23:28 +02:00
const selectedProfessionalRole = getStoredPreferredRole ( String ( user ? . email || email ( ) ) ) ;
2026-04-05 16:52:02 +02:00
const payload = {
firstName : firstName || '' ,
lastName : lastName || '' ,
fullName : fullName || '' ,
name : fullName || '' ,
displayName : fullName || '' ,
email : String ( user ? . email || email ( ) ) . trim ( ) . toLowerCase ( ) ,
roleKey : storedRole ,
role : storedRole ,
active_role : normalizedRole || 'JOB_SEEKER' ,
2026-04-15 06:23:28 +02:00
selectedProfessionalRole : selectedProfessionalRole || null ,
2026-04-05 16:52:02 +02:00
user ,
} ;
if ( typeof window !== 'undefined' ) {
window . localStorage . setItem ( 'nxtgauge_auth_user' , JSON . stringify ( payload ) ) ;
window . localStorage . setItem ( 'nxtgauge_user' , JSON . stringify ( payload ) ) ;
window . localStorage . setItem ( 'nxtgauge_signup_profile_v1' , JSON . stringify ( payload ) ) ;
}
} ;
const login = async ( ) = > {
2026-04-15 06:23:28 +02:00
if ( submitting ( ) ) return ;
2026-04-05 16:52:02 +02:00
setError ( '' ) ;
if ( ! isValidEmail ( email ( ) ) ) {
setError ( 'Enter a valid email address.' ) ;
return ;
}
if ( ! password ( ) . trim ( ) ) {
setError ( 'Password is required.' ) ;
return ;
}
if ( ! captchaInput ( ) . trim ( ) || captchaInput ( ) . trim ( ) . toUpperCase ( ) !== captcha ( ) . toUpperCase ( ) ) {
setError ( 'Captcha does not match. Please try again.' ) ;
setCaptcha ( makeCaptcha ( ) ) ;
setCaptchaInput ( '' ) ;
return ;
}
setSubmitting ( true ) ;
try {
const res = await fetch ( '/api/gateway/api/auth/login' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , Accept : 'application/json' } ,
credentials : 'include' ,
body : JSON.stringify ( {
email : email ( ) . trim ( ) . toLowerCase ( ) ,
password : password ( ) ,
} ) ,
} ) ;
const data = await res . json ( ) . catch ( ( ) = > ( { } ) ) ;
if ( ! res . ok ) {
const code = String ( data ? . code || '' ) . toUpperCase ( ) ;
if ( code === 'EMAIL_NOT_VERIFIED' ) {
setShowVerify ( true ) ;
setError ( 'Email not verified. Enter OTP sent to your inbox.' ) ;
return ;
}
setError ( String ( data ? . error || data ? . message || 'Invalid login credentials.' ) ) ;
return ;
}
const accessToken = String ( data ? . access_token || '' ) . trim ( ) ;
if ( typeof window !== 'undefined' && accessToken ) {
window . sessionStorage . setItem ( 'nxtgauge_access_token' , accessToken ) ;
window . sessionStorage . setItem ( 'nxtgauge_frontend_access_token' , accessToken ) ;
}
2026-04-15 06:23:28 +02:00
const resolvedActiveRole = resolveActiveRole (
data ? . user ? . active_role || data ? . user ? . role ,
data ? . user ? . email || email ( ) . trim ( ) . toLowerCase ( )
) ;
const normalizedEmail = email ( ) . trim ( ) . toLowerCase ( ) ;
const userPayload = {
id : String ( data ? . user ? . id || '' ) ,
email : String ( data ? . user ? . email || normalizedEmail ) ,
full_name : String ( data ? . user ? . full_name || '' ) ,
active_role : resolvedActiveRole ,
email_verified : Boolean ( data ? . user ? . email_verified ? ? true ) ,
} ;
saveUser ( { . . . ( data ? . user || { } ) , . . . userPayload } ) ;
2026-04-06 06:19:23 +02:00
if ( auth . setUser ) {
2026-04-15 06:23:28 +02:00
auth . setUser ( userPayload ) ;
2026-04-06 06:19:23 +02:00
}
2026-04-05 16:52:02 +02:00
navigate ( '/dashboard' , { replace : true } ) ;
2026-04-15 06:23:28 +02:00
} catch {
setError ( 'Network error during login. Please try again.' ) ;
2026-04-05 16:52:02 +02:00
} finally {
setSubmitting ( false ) ;
}
} ;
const resendOtp = async ( ) = > {
2026-04-15 06:23:28 +02:00
if ( submitting ( ) ) return ;
2026-04-05 16:52:02 +02:00
setError ( '' ) ;
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 : email ( ) . trim ( ) . toLowerCase ( ) } ) ,
} ) ;
const data = await res . json ( ) . catch ( ( ) = > ( { } ) ) ;
if ( ! res . ok ) {
setError ( String ( data ? . error || data ? . message || 'Unable to resend OTP.' ) ) ;
}
} finally {
setSubmitting ( false ) ;
}
} ;
const verifyThenLogin = async ( ) = > {
2026-04-15 06:23:28 +02:00
if ( submitting ( ) ) return ;
2026-04-05 16:52:02 +02:00
setError ( '' ) ;
if ( otpCode ( ) . length !== 6 ) {
setError ( 'Enter a valid 6-digit OTP.' ) ;
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 ) {
setError ( String ( verifyData ? . error || verifyData ? . message || 'OTP verification failed.' ) ) ;
return ;
}
await login ( ) ;
} 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-1.jpg" alt = "Public Workspace" / >
< div class = "auth-visual-overlay" / >
< div class = "auth-visual-content" >
< p class = "eyebrow" > Public Workspace < / p >
< h1 class = "title light" > Welcome Back To Nxtgauge < / h1 >
< p class = "subtitle light" > Sign in to manage your profile , portfolio , and verification in one place . < / p >
< / div >
< / section >
< section class = "auth-form card glass-light" >
< h2 class = "title" > Sign In < / h2 >
< div class = "field" >
< label class = "label" for = "login-email" > EMAIL < / label >
2026-04-15 06:23:28 +02:00
< input
id = "login-email"
type = "email"
class = "input"
value = { email ( ) }
onInput = { ( e ) = > {
const value = e . currentTarget . value ;
setEmail ( value ) ;
void lookupRoleByEmail ( value ) ;
} }
onBlur = { ( e ) = > {
void lookupRoleByEmail ( e . currentTarget . value ) ;
} }
placeholder = "Enter your email"
/ >
2026-04-05 16:52:02 +02:00
< p class = "validation-note" style = { { color : email ( ) . trim ( ) && isValidEmail ( email ( ) ) ? '#fd6116' : '#6e7591' } } >
{ email ( ) . trim ( ) && isValidEmail ( email ( ) ) ? '✓ Valid email format' : '• Enter a valid email format' }
< / p >
2026-04-15 06:23:28 +02:00
< Show when = { roleHint ( ) || checkingRole ( ) } >
< p class = "validation-note" style = { { color : '#0f766e' } } >
{ checkingRole ( ) ? 'Checking account role...' : ` • ${ roleHint ( ) } ` }
< / p >
< / Show >
2026-04-05 16:52:02 +02:00
< / div >
< div class = "field" >
< label class = "label" for = "login-password" > PASSWORD < / label >
< div class = "auth-password-wrap" >
< input id = "login-password" type = { showPassword ( ) ? 'text' : 'password' } class = "input" value = { password ( ) } onInput = { ( e ) = > setPassword ( e . currentTarget . value ) } placeholder = "Enter your password" / >
< button
class = "auth-toggle-visibility"
type = "button"
onClick = { ( ) = > setShowPassword ( ( prev ) = > ! prev ) }
aria - label = { showPassword ( ) ? 'Hide password' : 'Show password' }
>
< PasswordVisibilityIcon visible = { showPassword ( ) } / >
< / button >
< / div >
< / div >
< div class = "field" >
< label class = "label" > CAPTCHA < / label >
< div class = "auth-captcha-row" >
< button class = "auth-captcha-refresh" type = "button" onClick = { ( ) = > { setCaptcha ( makeCaptcha ( ) ) ; setCaptchaInput ( '' ) ; } } > ↻ < / button >
< CaptchaCanvas code = { captcha ( ) } class = "auth-captcha-canvas" / >
< input class = "input" value = { captchaInput ( ) } onInput = { ( e ) = > setCaptchaInput ( e . currentTarget . value ) } placeholder = "Enter captcha" / >
< / div >
< / div >
< Show when = { showVerify ( ) } >
< label class = "label" > EMAIL OTP < / label >
< div class = "otp-row" >
< For each = { Array . from ( { length : 6 } , ( _ , index ) = > index ) } >
{ ( index ) = > (
< input
id = { ` login-otp- ${ index } ` }
class = "otp-input"
inputMode = "numeric"
maxlength = { 1 }
value = { otp ( ) [ index ] }
onInput = { ( e ) = > setOtpDigit ( index , e . currentTarget . value ) }
/ >
) }
< / For >
< / div >
< 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 >
< button class = "auth-submit-btn" type = "button" onClick = { ( ) = > void login ( ) } disabled = { submitting ( ) } >
{ submitting ( ) ? 'Signing In...' : 'Sign In' }
< / button >
< Show when = { showVerify ( ) } >
< button class = "auth-submit-btn" type = "button" onClick = { ( ) = > void verifyThenLogin ( ) } disabled = { submitting ( ) } >
{ submitting ( ) ? 'Verifying...' : 'Verify Email and Login' }
< / button >
< / Show >
< div class = "auth-footer-row" >
< p class = "footer-text" > Secure login with email verification . < / p >
2026-04-06 03:33:29 +02:00
< p class = "note" > New user ? < A href = "/signup" > Sign Up < / A > < / p >
2026-04-06 06:19:23 +02:00
< p class = "note" > < A href = "/forgot-password" > Forgot Password ? < / A > < / p >
2026-04-05 16:52:02 +02:00
< / div >
< Show when = { error ( ) } >
< p class = "error" > { error ( ) } < / p >
< / Show >
< / section >
< / div >
< / main >
) ;
}