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' ;
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' ) ;
const otpCode = createMemo ( ( ) = > otp ( ) . join ( '' ) ) ;
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 ( ' ' ) ;
const normalizedRole = String ( user ? . active_role || user ? . role || roleGuess ( ) || '' )
. trim ( )
. toUpperCase ( )
. replace ( /\s+/g , '_' ) ;
const storedRole = normalizedRole
? normalizedRole . toLowerCase ( )
: roleGuess ( ) ;
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' ,
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 ( ) = > {
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 ) ;
window . localStorage . setItem ( 'nxtgauge_access_token' , accessToken ) ;
}
saveUser ( data ? . user || { } ) ;
2026-04-06 06:19:23 +02:00
if ( auth . setUser ) {
auth . setUser ( {
id : data?.user?.id || '' ,
email : data?.user?.email || email ( ) . trim ( ) . toLowerCase ( ) ,
full_name : data?.user?.full_name || '' ,
active_role : data?.user?.active_role || 'JOB_SEEKER' ,
email_verified : data?.user?.email_verified || false ,
} ) ;
}
2026-04-05 16:52:02 +02:00
navigate ( '/dashboard' , { replace : true } ) ;
} finally {
setSubmitting ( false ) ;
}
} ;
const resendOtp = async ( ) = > {
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 ( ) = > {
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 >
< input id = "login-email" type = "email" class = "input" value = { email ( ) } onInput = { ( e ) = > setEmail ( e . currentTarget . value ) } placeholder = "Enter your email" / >
< 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 >
< / 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 >
) ;
}