style: redesign login page with split-panel layout

Replaced cramped single-column layout with a polished two-panel design:
- Left: navy (#0a1d37) brand panel with logo, headline, feature checklist,
  radial glow accents, and security footer — visible at lg+ breakpoints
- Right: centered white card on slate-50 background with clean form,
  proper input focus states (navy ring), and consistent navy submit button
- Mobile: collapses to single column with logo above the card
- Removed washed-out disabled orange button; button is always visible
- Consistent with admin design system (navy primary, orange accent only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-24 15:57:28 +01:00
parent 3b2c09cd4b
commit eefdbc4561

View file

@ -24,6 +24,11 @@ function pickMaskedEmail(payload: any, fallback: string): string {
return fallback; return fallback;
} }
const inputCls =
'h-11 w-full rounded-lg border border-gray-200 bg-gray-50 px-3.5 text-sm text-gray-900 outline-none transition placeholder:text-gray-400 focus:border-[#0a1d37] focus:bg-white focus:ring-2 focus:ring-[#0a1d37]/10';
const labelCls = 'mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-500';
export default function LoginPage() { export default function LoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [mode, setMode] = createSignal<AuthMode>('login'); const [mode, setMode] = createSignal<AuthMode>('login');
@ -45,7 +50,6 @@ export default function LoginPage() {
newPassword().trim().length > 0 && newPassword().trim().length > 0 &&
confirmPassword().trim().length > 0, confirmPassword().trim().length > 0,
); );
const canSubmitResetVerify = createMemo( const canSubmitResetVerify = createMemo(
() => challengeId().trim().length > 0 && resetCode().trim().length === 6, () => challengeId().trim().length > 0 && resetCode().trim().length === 6,
); );
@ -53,10 +57,7 @@ export default function LoginPage() {
() => email().trim().length > 0 && password().trim().length > 0, () => email().trim().length > 0 && password().trim().length > 0,
); );
const clearMessages = () => { const clearMessages = () => { setError(''); setInfo(''); };
setError('');
setInfo('');
};
const resetPasswordFlow = () => { const resetPasswordFlow = () => {
setResetStep('request'); setResetStep('request');
@ -70,86 +71,51 @@ export default function LoginPage() {
const switchMode = (nextMode: AuthMode) => { const switchMode = (nextMode: AuthMode) => {
clearMessages(); clearMessages();
setMode(nextMode); setMode(nextMode);
if (nextMode === 'login') { if (nextMode === 'login') resetPasswordFlow();
resetPasswordFlow();
}
}; };
onMount(() => { onMount(() => {
if (hasAdminSession()) { if (hasAdminSession()) navigate('/admin', { replace: true });
navigate('/admin', { replace: true });
}
}); });
const completeAdminLogin = () => { const completeAdminLogin = () => {
setAdminSession(); setAdminSession();
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const from = params.get('from'); const from = params.get('from');
const nextPath = from && from.startsWith('/admin') ? from : '/admin'; navigate(from && from.startsWith('/admin') ? from : '/admin', { replace: true });
navigate(nextPath, { replace: true });
}; };
const directSignIn = async () => { const directSignIn = async () => {
clearMessages(); clearMessages();
if (!canSubmitLoginCredentials()) { if (!canSubmitLoginCredentials()) { setError('Email and password are required.'); return; }
setError('Email and password are required.');
return;
}
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const loginPayload = { const loginPayload = { email: email().trim().toLowerCase(), password: password(), loginTarget: 'admin' };
email: email().trim().toLowerCase(), const attempts = ['/api/gateway/users/auth/internal/login', '/api/gateway/auth/internal/login'];
password: password(),
loginTarget: 'admin',
};
const attempts: string[] = [
'/api/gateway/users/auth/internal/login',
'/api/gateway/auth/internal/login',
];
let payload: any = {}; let payload: any = {};
let status = 500; let status = 500;
let success = false; let success = false;
for (const url of attempts) { for (const url of attempts) {
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'x-portal-target': 'admin' },
'Content-Type': 'application/json',
Accept: 'application/json',
'x-portal-target': 'admin',
},
credentials: 'include', credentials: 'include',
body: JSON.stringify(loginPayload), body: JSON.stringify(loginPayload),
}); });
status = response.status; status = response.status;
payload = await response.json().catch(() => ({})); payload = await response.json().catch(() => ({}));
if (response.ok) { if (response.ok) { success = true; break; }
success = true;
break;
}
} }
if (!success) { if (!success) {
const fallback = status === 502 const fallback = status === 502 ? 'Auth service unavailable (502). Please retry in 12 minutes.' : 'Sign in failed.';
? 'Auth service is temporarily unavailable (502). Please retry in 1-2 minutes.'
: 'Sign in failed.';
throw new Error(pickManagementLoginError(payload) || fallback); throw new Error(pickManagementLoginError(payload) || fallback);
} }
if (isExternalIdentity(payload)) throw new Error('External users cannot use this portal.');
if (isExternalIdentity(payload)) {
throw new Error('External users are not allowed on management login. Please use the external user login.');
}
const accessToken = String(payload?.access_token || payload?.accessToken || '').trim(); const accessToken = String(payload?.access_token || payload?.accessToken || '').trim();
if (accessToken && typeof sessionStorage !== 'undefined') { if (accessToken) sessionStorage.setItem('nxtgauge_admin_access_token', accessToken);
sessionStorage.setItem('nxtgauge_admin_access_token', accessToken);
}
completeAdminLogin(); completeAdminLogin();
} catch (nextError: any) { } catch (e: any) {
setError(String(nextError?.message || 'Sign in failed.')); setError(String(e?.message || 'Sign in failed.'));
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -157,77 +123,39 @@ export default function LoginPage() {
const requestResetCode = async () => { const requestResetCode = async () => {
clearMessages(); clearMessages();
if (!canSubmitResetRequest()) { if (!canSubmitResetRequest()) { setError('Email, new password, and confirm password are required.'); return; }
setError('Email, new password, and confirm password are required.'); if (newPassword() !== confirmPassword()) { setError('Passwords do not match.'); return; }
return;
}
if (newPassword() !== confirmPassword()) {
setError('Passwords do not match.');
return;
}
const trimmedEmail = email().trim().toLowerCase(); const trimmedEmail = email().trim().toLowerCase();
const resolvedPassword = newPassword().trim(); const resolvedPassword = newPassword().trim();
const requestPayload = { const requestPayload = { email: trimmedEmail, newPassword: resolvedPassword, new_password: resolvedPassword, password: resolvedPassword };
email: trimmedEmail, const attempts = [
newPassword: resolvedPassword, { url: '/api/gateway/users/auth/internal/forgot-password/request-code', body: JSON.stringify(requestPayload) },
new_password: resolvedPassword, { url: '/api/gateway/auth/internal/forgot-password/request-code', body: JSON.stringify(requestPayload) },
password: resolvedPassword, { url: '/api/gateway/users/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: requestPayload }) },
}; { url: '/api/gateway/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: requestPayload }) },
const attempts: Array<{ url: string; body: string }> = [
{
url: '/api/gateway/users/auth/internal/forgot-password/request-code',
body: JSON.stringify(requestPayload),
},
{
url: '/api/gateway/auth/internal/forgot-password/request-code',
body: JSON.stringify(requestPayload),
},
{
url: '/api/gateway/users/auth/internal/forgot-password/request-code',
body: JSON.stringify({ data: requestPayload }),
},
{
url: '/api/gateway/auth/internal/forgot-password/request-code',
body: JSON.stringify({ data: requestPayload }),
},
]; ];
setIsSubmitting(true); setIsSubmitting(true);
try { try {
let payload: any = {}; let payload: any = {};
let status = 500; let status = 500;
for (const attempt of attempts) { for (const attempt of attempts) {
const response = await fetch(attempt.url, { const response = await fetch(attempt.url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: attempt.body });
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: attempt.body,
});
status = response.status; status = response.status;
payload = await response.json().catch(() => ({})); payload = await response.json().catch(() => ({}));
if (response.ok) break; if (response.ok) break;
} }
const nextChallengeId = pickChallengeId(payload); const nextChallengeId = pickChallengeId(payload);
if (!nextChallengeId) { if (!nextChallengeId) {
const fallbackMessage = const fallback = status === 502 ? 'Verification service unavailable (502). Please retry in 12 minutes.' : 'Failed to send reset code.';
status === 502 throw new Error(String(payload?.message || payload?.error || fallback).trim());
? 'Verification service is temporarily unavailable (502). Please retry in 1-2 minutes.'
: 'Failed to send reset code.';
throw new Error(String(payload?.message || payload?.error || fallbackMessage).trim());
} }
setChallengeId(nextChallengeId); setChallengeId(nextChallengeId);
setMaskedEmail(pickMaskedEmail(payload, trimmedEmail)); setMaskedEmail(pickMaskedEmail(payload, trimmedEmail));
setResetStep('verify'); setResetStep('verify');
const debugCode = String(payload?.debugCode || payload?.data?.debugCode || '').trim(); const debugCode = String(payload?.debugCode || payload?.data?.debugCode || '').trim();
setInfo(debugCode ? `Reset code sent. [DEV CODE: ${debugCode}]` : 'Reset code sent to your email.'); setInfo(debugCode ? `Reset code sent. [DEV: ${debugCode}]` : 'Reset code sent to your email.');
} catch (nextError: any) { } catch (e: any) {
setError(String(nextError?.message || 'Failed to send reset code.')); setError(String(e?.message || 'Failed to send reset code.'));
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -235,228 +163,238 @@ export default function LoginPage() {
const verifyResetCode = async () => { const verifyResetCode = async () => {
clearMessages(); clearMessages();
if (!canSubmitResetVerify()) { if (!canSubmitResetVerify()) { setError('A valid 6-digit code is required.'); return; }
setError('A valid 6-digit verification code is required.'); if (newPassword().trim() !== confirmPassword().trim()) { setError('Passwords do not match.'); return; }
return;
}
if (newPassword().trim() !== confirmPassword().trim()) {
setError('Passwords do not match.');
return;
}
const resolvedPassword = newPassword().trim(); const resolvedPassword = newPassword().trim();
const resolvedChallengeId = challengeId().trim(); const resolvedChallengeId = challengeId().trim();
const resolvedCode = resetCode().trim(); const resolvedCode = resetCode().trim();
const verifyPayload = { const verifyPayload = {
challengeId: resolvedChallengeId, challengeId: resolvedChallengeId, challenge_id: resolvedChallengeId,
challenge_id: resolvedChallengeId, code: resolvedCode, otp: resolvedCode, verificationCode: resolvedCode, verification_code: resolvedCode,
code: resolvedCode, newPassword: resolvedPassword, new_password: resolvedPassword, password: resolvedPassword,
otp: resolvedCode,
verificationCode: resolvedCode,
verification_code: resolvedCode,
newPassword: resolvedPassword,
new_password: resolvedPassword,
password: resolvedPassword,
}; };
const attempts = ['/api/gateway/users/auth/internal/forgot-password/verify-code', '/api/gateway/auth/internal/forgot-password/verify-code'];
const attempts: string[] = [
'/api/gateway/users/auth/internal/forgot-password/verify-code',
'/api/gateway/auth/internal/forgot-password/verify-code',
];
setIsSubmitting(true); setIsSubmitting(true);
try { try {
let payload: any = {}; let payload: any = {};
let status = 500; let status = 500;
let success = false; let success = false;
for (const url of attempts) { for (const url of attempts) {
const response = await fetch(url, { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(verifyPayload) });
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(verifyPayload),
});
status = response.status; status = response.status;
payload = await response.json().catch(() => ({})); payload = await response.json().catch(() => ({}));
if (response.ok) { if (response.ok) { success = true; break; }
success = true;
break;
}
} }
if (!success) { if (!success) {
const fallbackMessage = const fallback = status === 502 ? 'Verification service unavailable (502). Please retry in 12 minutes.' : 'Password reset failed.';
status === 502 throw new Error(String(payload?.message || payload?.error || fallback).trim());
? 'Verification service is temporarily unavailable (502). Please retry in 1-2 minutes.'
: 'Password reset failed.';
throw new Error(String(payload?.message || payload?.error || fallbackMessage).trim());
} }
setPassword(''); setPassword('');
switchMode('login'); switchMode('login');
setInfo('Password reset successful. Please sign in with your new password.'); setInfo('Password reset successful. Please sign in with your new password.');
} catch (nextError: any) { } catch (e: any) {
setError(String(nextError?.message || 'Password reset failed.')); setError(String(e?.message || 'Password reset failed.'));
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
return ( return (
<main class="min-h-screen bg-[radial-gradient(circle_at_10%_10%,rgba(253,98,22,0.18),transparent_35%),radial-gradient(circle_at_90%_0%,rgba(34,197,94,0.12),transparent_32%),linear-gradient(180deg,#0f1630_0%,#0b1226_100%)]"> <div class="flex min-h-screen w-full overflow-hidden">
<div class="mx-auto grid min-h-screen w-full max-w-[1240px] grid-cols-1 items-center gap-6 px-4 py-8 lg:grid-cols-[1.05fr_0.95fr] lg:px-8">
<section class="relative hidden min-h-[620px] overflow-hidden rounded-[28px] border border-white/20 bg-white/10 p-8 text-white shadow-[0_28px_60px_-34px_rgba(2,6,23,0.88)] backdrop-blur lg:block"> {/* ── Left brand panel ── */}
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-12 w-auto brightness-0 invert" /> <div class="relative hidden flex-col justify-between overflow-hidden bg-[#0a1d37] p-10 lg:flex lg:w-[44%] xl:w-[42%]">
<p class="mt-8 inline-flex rounded-full border border-white/30 bg-white/10 px-3 py-1 text-[11px] font-bold uppercase tracking-[0.1em] text-orange-100">Internal Admin Portal</p> {/* Subtle radial glow */}
<h1 class="mt-6 max-w-[520px] text-[52px] font-extrabold leading-[1.05] text-white">Welcome back to Nxtgauge.</h1> <div class="pointer-events-none absolute -top-32 -left-32 h-96 w-96 rounded-full bg-[#fd6216]/10 blur-3xl" />
<p class="mt-4 max-w-[520px] text-[17px] leading-relaxed text-slate-200">Sign in to manage operations, roles, and approval workflows from one secure control panel.</p> <div class="pointer-events-none absolute bottom-0 right-0 h-72 w-72 rounded-full bg-[#fd6216]/5 blur-2xl" />
<div class="absolute bottom-8 left-8 right-8 rounded-2xl border border-white/20 bg-white/10 p-4 text-[14px] text-slate-100">
<p class="font-semibold">Secure login with internal access policies.</p> {/* Logo */}
<div class="relative z-10">
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-10 w-auto brightness-0 invert" />
</div>
{/* Headline */}
<div class="relative z-10 space-y-6">
<span class="inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-[11px] font-bold uppercase tracking-widest text-orange-300">
<span class="h-1.5 w-1.5 rounded-full bg-orange-400" />
Internal Admin Portal
</span>
<h1 class="text-4xl font-extrabold leading-tight text-white xl:text-[44px]">
Manage everything<br />from one place.
</h1>
<p class="text-[15px] leading-relaxed text-slate-400">
Roles, approvals, users, dashboards all controlled from a single secure control panel.
</p>
{/* Feature list */}
<ul class="space-y-3 text-sm text-slate-300">
{(['Role & permission management', 'Approval workflow control', 'User & company oversight', 'Dashboard configuration'] as const).map((item) => (
<li class="flex items-center gap-3">
<span class="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-[#fd6216]/20 text-[#fd6216]">
<svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4l2.5 2.5L9 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
{item}
</li>
))}
</ul>
</div>
{/* Bottom tag */}
<div class="relative z-10 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-[13px] text-slate-400">
🔒 Secured with internal access policies. Authorised personnel only.
</div>
</div>
{/* ── Right form panel ── */}
<div class="flex flex-1 items-center justify-center bg-[#f5f6fa] px-4 py-12 sm:px-8">
<div class="w-full max-w-[420px]">
{/* Mobile logo */}
<div class="mb-8 flex justify-center lg:hidden">
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-9 w-auto" />
</div> </div>
</section>
<section class="rounded-[28px] border border-white/30 bg-white/95 p-6 shadow-[0_28px_60px_-34px_rgba(2,6,23,0.88)] backdrop-blur sm:p-8"> {/* Card */}
<div class="mx-auto w-full max-w-[560px]"> <div class="rounded-2xl border border-gray-200 bg-white p-8 shadow-sm">
<h2 class="text-[46px] font-extrabold tracking-tight text-[#101228] sm:text-[38px] lg:text-[46px]">{mode() === 'login' ? 'Sign In' : 'Reset Password'}</h2>
<p class="mt-2 text-[15px] text-[#535e7a]">{mode() === 'login' ? 'Internal team access only.' : 'Use your internal email to reset access.'}</p>
<form class="mt-6 space-y-5"> <div class="mb-7">
<div class="space-y-4"> <h2 class="text-2xl font-bold text-gray-900">
<div> {mode() === 'login' ? 'Sign in' : 'Reset password'}
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Email</label> </h2>
<input <p class="mt-1 text-sm text-gray-500">
type="email" {mode() === 'login' ? 'Internal team access only.' : 'Enter your email and set a new password.'}
value={email()} </p>
onInput={(event) => { </div>
setEmail(event.currentTarget.value);
clearMessages();
}}
placeholder="Enter your email"
class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]"
/>
</div>
<Show when={mode() === 'login'}> <div class="space-y-4">
<div>
<div class="mb-1 flex items-center justify-between gap-2">
<label class="text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Password</label>
<button type="button" class="text-[12px] font-bold text-[#fd6216] underline" onClick={() => switchMode('reset')}>
Forgot?
</button>
</div>
<input
type="password"
value={password()}
onInput={(event) => {
setPassword(event.currentTarget.value);
clearMessages();
}}
placeholder="Enter your password"
class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]"
/>
</div>
</Show>
<Show when={mode() === 'reset'}> {/* Email */}
<div class="space-y-4"> <div>
<div> <label class={labelCls}>Email</label>
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">New Password</label> <input
<input type="email"
type="password" value={email()}
value={newPassword()} onInput={(e) => { setEmail(e.currentTarget.value); clearMessages(); }}
onInput={(event) => { placeholder="you@nxtgauge.com"
setNewPassword(event.currentTarget.value); class={inputCls}
clearMessages(); autocomplete="email"
}} />
placeholder="Minimum 8 characters"
class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]"
/>
</div>
<div>
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Confirm Password</label>
<input
type="password"
value={confirmPassword()}
onInput={(event) => {
setConfirmPassword(event.currentTarget.value);
clearMessages();
}}
placeholder="Confirm new password"
class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]"
/>
</div>
<Show when={resetStep() === 'verify'}>
<div>
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Verification Code</label>
<input
type="text"
inputMode="numeric"
maxLength={6}
value={resetCode()}
onInput={(event) => {
setResetCode(event.currentTarget.value.replace(/\D/g, '').slice(0, 6));
clearMessages();
}}
placeholder="000000"
class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-center text-[18px] tracking-[0.2em] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]"
/>
<p class="mt-2 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-900">
Code sent to <span class="font-semibold">{maskedEmail() || email()}</span>.
</p>
</div>
</Show>
</div>
</Show>
</div> </div>
{/* Login: Password */}
<Show when={mode() === 'login'}>
<div>
<div class="mb-1.5 flex items-center justify-between">
<label class={labelCls} style="margin-bottom:0">Password</label>
<button type="button" class="text-xs font-semibold text-[#fd6216] hover:underline" onClick={() => switchMode('reset')}>
Forgot password?
</button>
</div>
<input
type="password"
value={password()}
onInput={(e) => { setPassword(e.currentTarget.value); clearMessages(); }}
placeholder="••••••••"
class={inputCls}
autocomplete="current-password"
/>
</div>
</Show>
{/* Reset: New + Confirm + Code */}
<Show when={mode() === 'reset'}>
<div>
<label class={labelCls}>New password</label>
<input
type="password"
value={newPassword()}
onInput={(e) => { setNewPassword(e.currentTarget.value); clearMessages(); }}
placeholder="Minimum 8 characters"
class={inputCls}
/>
</div>
<div>
<label class={labelCls}>Confirm password</label>
<input
type="password"
value={confirmPassword()}
onInput={(e) => { setConfirmPassword(e.currentTarget.value); clearMessages(); }}
placeholder="Repeat new password"
class={inputCls}
/>
</div>
<Show when={resetStep() === 'verify'}>
<div>
<label class={labelCls}>Verification code</label>
<input
type="text"
inputMode="numeric"
maxLength={6}
value={resetCode()}
onInput={(e) => { setResetCode(e.currentTarget.value.replace(/\D/g, '').slice(0, 6)); clearMessages(); }}
placeholder="000000"
class={`${inputCls} text-center text-lg tracking-[0.3em]`}
/>
<p class="mt-2 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs text-emerald-700">
Code sent to <span class="font-semibold">{maskedEmail() || email()}</span>
</p>
</div>
</Show>
</Show>
{/* Error / Info */}
<Show when={error()}>
<p class="rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-600">{error()}</p>
</Show>
<Show when={info()}>
<p class="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2.5 text-sm text-emerald-700">{info()}</p>
</Show>
{/* Submit button */}
<Show when={mode() === 'login'}> <Show when={mode() === 'login'}>
<button <button
type="button" type="button"
class="h-11 w-full rounded-xl bg-[#fd6216] text-[15px] font-bold text-white transition hover:bg-[#e4570f] disabled:cursor-not-allowed disabled:opacity-70" class="mt-1 h-11 w-full rounded-lg bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-50"
disabled={isSubmitting() || !canSubmitLoginCredentials()} disabled={isSubmitting()}
onClick={directSignIn} onClick={directSignIn}
> >
{isSubmitting() ? 'Signing In...' : 'Sign In'} {isSubmitting() ? 'Signing in…' : 'Sign in'}
</button> </button>
</Show> </Show>
<Show when={mode() === 'reset'}> <Show when={mode() === 'reset'}>
<div class="flex items-center justify-between"> <Show when={resetStep() === 'request'} fallback={
<button type="button" class="text-[13px] font-semibold text-[#fd6216] underline" onClick={() => switchMode('login')}>
Back to sign in
</button>
</div>
<Show when={resetStep() === 'request'} fallback={(
<button <button
type="button" type="button"
class="h-11 w-full rounded-xl bg-[#fd6216] text-[15px] font-bold text-white transition hover:bg-[#e4570f] disabled:cursor-not-allowed disabled:opacity-70" class="mt-1 h-11 w-full rounded-lg bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-50"
disabled={isSubmitting() || !canSubmitResetVerify()} disabled={isSubmitting() || !canSubmitResetVerify()}
onClick={verifyResetCode} onClick={verifyResetCode}
> >
{isSubmitting() ? 'Resetting Password...' : 'Verify & Reset Password'} {isSubmitting() ? 'Resetting…' : 'Verify & reset password'}
</button> </button>
)}> }>
<button <button
type="button" type="button"
class="h-11 w-full rounded-xl bg-[#fd6216] text-[15px] font-bold text-white transition hover:bg-[#e4570f] disabled:cursor-not-allowed disabled:opacity-70" class="mt-1 h-11 w-full rounded-lg bg-[#0a1d37] text-sm font-semibold text-white transition hover:bg-[#0f2a4e] disabled:cursor-not-allowed disabled:opacity-50"
disabled={isSubmitting() || !canSubmitResetRequest()} disabled={isSubmitting() || !canSubmitResetRequest()}
onClick={requestResetCode} onClick={requestResetCode}
> >
{isSubmitting() ? 'Sending Code...' : 'Send Reset Code'} {isSubmitting() ? 'Sending code…' : 'Send reset code'}
</button> </button>
</Show> </Show>
<button type="button" class="w-full text-center text-sm font-medium text-gray-500 hover:text-gray-700" onClick={() => switchMode('login')}>
Back to sign in
</button>
</Show> </Show>
</form>
{info() ? <p class="mt-4 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-[14px] text-emerald-700">{info()}</p> : null} </div>
{error() ? <p class="mt-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-[14px] text-red-700">{error()}</p> : null}
</div> </div>
</section>
<p class="mt-6 text-center text-xs text-gray-400">
© {new Date().getFullYear()} Nxtgauge. Internal use only.
</p>
</div>
</div> </div>
</main> </div>
); );
} }