login screen fix
This commit is contained in:
parent
144f70cd34
commit
dc0b46fbd9
63
src/components/auth/AuthShell.tsx
Normal file
63
src/components/auth/AuthShell.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { Card } from '../reusable';
|
||||||
|
import logo from '../../assets/logo.png';
|
||||||
|
|
||||||
|
/** Shared auth-page chrome: curved green background, brand header and a card. */
|
||||||
|
export function AuthShell({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="relative min-h-screen overflow-hidden flex flex-col items-center justify-center gap-6 p-4 bg-app">
|
||||||
|
{/* Decorative curves */}
|
||||||
|
<svg
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute top-0 left-0 w-[70%] max-w-[520px] h-auto"
|
||||||
|
viewBox="0 0 400 220"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="wave-top" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stopColor="#7fd49a" stopOpacity="0.85" />
|
||||||
|
<stop offset="1" stopColor="#d9f3e2" stopOpacity="0.2" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path d="M0 0H400C340 40 300 120 190 150C110 172 50 150 0 210Z" fill="url(#wave-top)" />
|
||||||
|
<path d="M0 0H250C200 30 150 70 90 90C50 103 20 100 0 130Z" fill="#9adcae" fillOpacity="0.55" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute bottom-0 left-0 w-full h-auto"
|
||||||
|
viewBox="0 0 400 180"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="wave-bottom" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0" stopColor="#d9f3e2" stopOpacity="0.4" />
|
||||||
|
<stop offset="1" stopColor="#6cc88b" stopOpacity="0.8" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path d="M0 80C80 30 160 40 240 80C310 115 360 90 400 50V180H0Z" fill="url(#wave-bottom)" />
|
||||||
|
<path d="M0 130C90 100 170 110 250 135C320 157 370 140 400 115V180H0Z" fill="#4fb374" fillOpacity="0.55" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div className="relative flex flex-col gap-2 items-center text-center">
|
||||||
|
<img src={logo} alt="Krishna Sales Logo" className="h-20 w-20 rounded-full object-contain shadow-md" />
|
||||||
|
<h1 className="m-0 text-3xl font-extrabold text-emerald-900 tracking-[-0.02em]">Krishna Sales</h1>
|
||||||
|
</div>
|
||||||
|
<Card className="relative w-full max-w-[420px] rounded-3xl shadow-xl shadow-emerald-900/10 border-white/60 bg-white/85 backdrop-blur p-6 sm:p-8">
|
||||||
|
<div className="mb-5">
|
||||||
|
<h2 className="m-0 text-2xl font-extrabold text-emerald-950">{title}</h2>
|
||||||
|
{subtitle && <p className="text-sm text-slate-600 mt-1 leading-relaxed">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, type FormEvent } from 'react';
|
import { useState, useEffect, useRef, type FormEvent } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { Eye, EyeOff, ArrowLeft, Mail, KeyRound, CheckCircle2, AlertCircle } from 'lucide-react';
|
import { Eye, EyeOff, ArrowLeft, Mail, Lock, KeyRound, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||||
import { Card } from '../components/reusable';
|
import { AuthShell } from '../components/auth/AuthShell';
|
||||||
import { Button } from '../components/buttons';
|
import { Button } from '../components/buttons';
|
||||||
import { PasswordRulesChecklist } from '../components/auth/PasswordRulesChecklist';
|
import { PasswordRulesChecklist } from '../components/auth/PasswordRulesChecklist';
|
||||||
import { forgotPassword, verifyResetCode, resetPassword } from '../api/auth';
|
import { forgotPassword, verifyResetCode, resetPassword } from '../api/auth';
|
||||||
@ -11,7 +11,6 @@ import {
|
|||||||
passwordTooLong,
|
passwordTooLong,
|
||||||
PASSWORD_MAX_CHARS,
|
PASSWORD_MAX_CHARS,
|
||||||
} from '../utils/passwordPolicy';
|
} from '../utils/passwordPolicy';
|
||||||
import logo from '../assets/logo.png';
|
|
||||||
|
|
||||||
const CODE_LENGTH = 6;
|
const CODE_LENGTH = 6;
|
||||||
const RESEND_COOLDOWN = 60; // seconds
|
const RESEND_COOLDOWN = 60; // seconds
|
||||||
@ -34,6 +33,13 @@ export function ForgotPasswordPage() {
|
|||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
const policy = usePasswordPolicy(step === 'password');
|
const policy = usePasswordPolicy(step === 'password');
|
||||||
|
const codeInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// The email and code inputs share a tree position, so React reuses the DOM node
|
||||||
|
// and autoFocus doesn't fire on step change — focus the code input explicitly.
|
||||||
|
useEffect(() => {
|
||||||
|
if (step === 'code') codeInputRef.current?.focus();
|
||||||
|
}, [step]);
|
||||||
|
|
||||||
// Cooldown countdown
|
// Cooldown countdown
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -137,31 +143,23 @@ export function ForgotPasswordPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const title = done
|
||||||
<div className="min-h-screen flex items-center justify-center p-4 bg-emerald-50">
|
|
||||||
<Card className="w-full max-w-[420px] shadow-lg border-emerald-100 bg-white p-6 sm:p-7">
|
|
||||||
<div className="flex flex-col gap-2 items-center mb-6 text-center">
|
|
||||||
<img src={logo} alt="Krishna Sales Logo" className="h-14 w-auto object-contain" />
|
|
||||||
<h1 className="text-xl font-extrabold text-emerald-950 tracking-tight">
|
|
||||||
{done
|
|
||||||
? 'Password Reset Complete'
|
? 'Password Reset Complete'
|
||||||
: step === 'email'
|
: step === 'email'
|
||||||
? 'Forgot Password'
|
? 'Forgot Password'
|
||||||
: step === 'code'
|
: step === 'code'
|
||||||
? 'Enter Verification Code'
|
? 'Enter Verification Code'
|
||||||
: 'Create New Password'}
|
: 'Create New Password';
|
||||||
</h1>
|
const subtitle = done
|
||||||
<p className="text-xs text-slate-500 max-w-[340px] leading-relaxed">
|
|
||||||
{done
|
|
||||||
? 'Your password has been changed. Redirecting to login…'
|
? 'Your password has been changed. Redirecting to login…'
|
||||||
: step === 'email'
|
: step === 'email'
|
||||||
? "Enter your email address and we'll send you a 6-digit verification code."
|
? "Enter your email address and we'll send you a 6-digit verification code."
|
||||||
: step === 'code'
|
: step === 'code'
|
||||||
? `We emailed a ${CODE_LENGTH}-digit code to ${email}. It is valid for 10 minutes.`
|
? `We emailed a ${CODE_LENGTH}-digit code to ${email}. It is valid for 10 minutes.`
|
||||||
: 'Choose a strong new password that meets the security policy.'}
|
: 'Choose a strong new password that meets the security policy.';
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthShell title={title} subtitle={subtitle}>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 p-3 rounded-lg bg-rose-50 border border-rose-200 text-rose-700 text-xs flex items-start gap-2">
|
<div className="mb-4 p-3 rounded-lg bg-rose-50 border border-rose-200 text-rose-700 text-xs flex items-start gap-2">
|
||||||
<AlertCircle size={16} className="shrink-0 mt-0.5" />
|
<AlertCircle size={16} className="shrink-0 mt-0.5" />
|
||||||
@ -175,35 +173,33 @@ export function ForgotPasswordPage() {
|
|||||||
<CheckCircle2 size={36} className="text-emerald-600" />
|
<CheckCircle2 size={36} className="text-emerald-600" />
|
||||||
<span className="font-semibold">{message}</span>
|
<span className="font-semibold">{message}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" full onClick={goToLogin} className="bg-emerald-600 hover:bg-emerald-700 text-white">
|
<Button type="button" full onClick={goToLogin} className="mt-1 !rounded-full !h-12 !text-base bg-gradient-to-r from-[#3aa75a] to-[#0f5a30] hover:brightness-110 text-white border-transparent shadow-lg shadow-emerald-900/20">
|
||||||
Go to Login
|
Go to Login
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : step === 'email' ? (
|
) : step === 'email' ? (
|
||||||
/* Step 1: Email Form */
|
/* Step 1: Email Form */
|
||||||
<form onSubmit={handleEmailSubmit} className="flex flex-col gap-4">
|
<form onSubmit={handleEmailSubmit} className="flex flex-col gap-4">
|
||||||
<label className="flex flex-col gap-1.5 font-sans">
|
<div className="relative flex items-center">
|
||||||
<span className="text-sm font-semibold text-emerald-800 flex items-center gap-1.5">
|
<Mail size={18} className="absolute left-4 text-slate-500 pointer-events-none" />
|
||||||
<Mail size={15} />
|
|
||||||
Email address <span className="text-emerald-500">*</span>
|
|
||||||
</span>
|
|
||||||
<input
|
<input
|
||||||
|
aria-label="Email address"
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="h-[42px] px-3 border border-emerald-200 bg-emerald-50/50 rounded-md outline-none focus:ring-2 focus:ring-emerald-400 focus:border-emerald-400 transition-all text-emerald-900 placeholder:text-slate-400 text-sm"
|
className="h-12 w-full pl-11 pr-4 rounded-full border border-slate-200 bg-slate-100/80 outline-none focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400 transition-all text-slate-900 placeholder:text-slate-500 text-sm"
|
||||||
placeholder="you@example.com"
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
full
|
full
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="mt-2 bg-emerald-600 hover:bg-emerald-700 text-white border-transparent"
|
className="mt-1 !rounded-full !h-12 !text-base bg-gradient-to-r from-[#3aa75a] to-[#0f5a30] hover:brightness-110 text-white border-transparent shadow-lg shadow-emerald-900/20"
|
||||||
>
|
>
|
||||||
{submitting ? 'Sending code…' : 'Send Reset Code'}
|
{submitting ? 'Sending code…' : 'Send Reset Code'}
|
||||||
</Button>
|
</Button>
|
||||||
@ -219,12 +215,11 @@ export function ForgotPasswordPage() {
|
|||||||
) : step === 'code' ? (
|
) : step === 'code' ? (
|
||||||
/* Step 2: OTP Verification Form */
|
/* Step 2: OTP Verification Form */
|
||||||
<form onSubmit={handleCodeSubmit} className="flex flex-col gap-4">
|
<form onSubmit={handleCodeSubmit} className="flex flex-col gap-4">
|
||||||
<label className="flex flex-col gap-1.5 font-sans">
|
<div className="relative flex items-center">
|
||||||
<span className="text-sm font-semibold text-emerald-800 flex items-center gap-1.5">
|
<KeyRound size={18} className="absolute left-4 text-slate-500 pointer-events-none" />
|
||||||
<KeyRound size={15} />
|
|
||||||
Reset Code <span className="text-emerald-500">*</span>
|
|
||||||
</span>
|
|
||||||
<input
|
<input
|
||||||
|
ref={codeInputRef}
|
||||||
|
aria-label="Reset code"
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
@ -234,16 +229,16 @@ export function ForgotPasswordPage() {
|
|||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="h-[46px] px-3 text-center text-xl font-bold tracking-[0.3em] border border-emerald-200 bg-emerald-50/50 rounded-md outline-none focus:ring-2 focus:ring-emerald-400 focus:border-emerald-400 transition-all text-emerald-900 placeholder:text-slate-400"
|
className="h-12 w-full px-11 text-center text-xl font-bold tracking-[0.3em] rounded-full border border-slate-200 bg-slate-100/80 outline-none focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400 transition-all text-slate-900 placeholder:text-slate-400"
|
||||||
placeholder="123456"
|
placeholder="123456"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
full
|
full
|
||||||
disabled={submitting || code.length !== CODE_LENGTH}
|
disabled={submitting || code.length !== CODE_LENGTH}
|
||||||
className="mt-2 bg-emerald-600 hover:bg-emerald-700 text-white border-transparent"
|
className="mt-1 !rounded-full !h-12 !text-base bg-gradient-to-r from-[#3aa75a] to-[#0f5a30] hover:brightness-110 text-white border-transparent shadow-lg shadow-emerald-900/20"
|
||||||
>
|
>
|
||||||
{submitting ? 'Verifying…' : 'Verify Code'}
|
{submitting ? 'Verifying…' : 'Verify Code'}
|
||||||
</Button>
|
</Button>
|
||||||
@ -274,11 +269,10 @@ export function ForgotPasswordPage() {
|
|||||||
/* Step 3: New Password Form */
|
/* Step 3: New Password Form */
|
||||||
<form onSubmit={handlePasswordSubmit} className="flex flex-col gap-4">
|
<form onSubmit={handlePasswordSubmit} className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1.5 font-sans">
|
<div className="flex flex-col gap-1.5 font-sans">
|
||||||
<span className="text-sm font-semibold text-emerald-800">
|
|
||||||
New password <span className="text-emerald-500">*</span>
|
|
||||||
</span>
|
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
|
<Lock size={18} className="absolute left-4 text-slate-500 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
|
aria-label="New password"
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
value={password}
|
value={password}
|
||||||
@ -286,45 +280,44 @@ export function ForgotPasswordPage() {
|
|||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="h-[42px] w-full px-3 pr-10 border border-emerald-200 bg-emerald-50/50 rounded-md outline-none focus:ring-2 focus:ring-emerald-400 focus:border-emerald-400 transition-all text-emerald-900 placeholder:text-slate-400 text-sm"
|
className="h-12 w-full pl-11 pr-11 rounded-full border border-slate-200 bg-slate-100/80 outline-none focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400 transition-all text-slate-900 placeholder:text-slate-500 text-sm"
|
||||||
placeholder="Enter new password"
|
placeholder="New password"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword((s) => !s)}
|
onClick={() => setShowPassword((s) => !s)}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
|
className="absolute right-4 text-slate-600 hover:text-slate-800 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
{showPassword ? <Eye size={18} /> : <EyeOff size={18} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<PasswordRulesChecklist policy={policy} password={password} email={email} className="mt-1" />
|
<PasswordRulesChecklist policy={policy} password={password} email={email} className="mt-1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5 font-sans">
|
<div className="flex flex-col gap-1.5 font-sans">
|
||||||
<span className="text-sm font-semibold text-emerald-800">
|
|
||||||
Confirm new password <span className="text-emerald-500">*</span>
|
|
||||||
</span>
|
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
|
<Lock size={18} className="absolute left-4 text-slate-500 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
|
aria-label="Confirm new password"
|
||||||
type={showConfirm ? 'text' : 'password'}
|
type={showConfirm ? 'text' : 'password'}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
value={confirm}
|
value={confirm}
|
||||||
onChange={(e) => setConfirm(e.target.value)}
|
onChange={(e) => setConfirm(e.target.value)}
|
||||||
required
|
required
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="h-[42px] w-full px-3 pr-10 border border-emerald-200 bg-emerald-50/50 rounded-md outline-none focus:ring-2 focus:ring-emerald-400 focus:border-emerald-400 transition-all text-emerald-900 placeholder:text-slate-400 text-sm"
|
className="h-12 w-full pl-11 pr-11 rounded-full border border-slate-200 bg-slate-100/80 outline-none focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400 transition-all text-slate-900 placeholder:text-slate-500 text-sm"
|
||||||
placeholder="Re-enter new password"
|
placeholder="Confirm new password"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowConfirm((s) => !s)}
|
onClick={() => setShowConfirm((s) => !s)}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
|
className="absolute right-4 text-slate-600 hover:text-slate-800 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
{showConfirm ? <EyeOff size={16} /> : <Eye size={16} />}
|
{showConfirm ? <Eye size={18} /> : <EyeOff size={18} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -333,7 +326,7 @@ export function ForgotPasswordPage() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
full
|
full
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="mt-2 bg-emerald-600 hover:bg-emerald-700 text-white border-transparent"
|
className="mt-1 !rounded-full !h-12 !text-base bg-gradient-to-r from-[#3aa75a] to-[#0f5a30] hover:brightness-110 text-white border-transparent shadow-lg shadow-emerald-900/20"
|
||||||
>
|
>
|
||||||
{submitting ? 'Updating password…' : 'Set New Password'}
|
{submitting ? 'Updating password…' : 'Set New Password'}
|
||||||
</Button>
|
</Button>
|
||||||
@ -351,7 +344,6 @@ export function ForgotPasswordPage() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</AuthShell>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { useState, type FormEvent } from 'react';
|
import { useState, type FormEvent } from 'react';
|
||||||
import { Navigate, useNavigate, Link } from 'react-router-dom';
|
import { Navigate, useNavigate, Link } from 'react-router-dom';
|
||||||
import { Eye, EyeOff, Lock, AlertCircle, ArrowLeft } from 'lucide-react';
|
import { Eye, EyeOff, Lock, Mail, AlertCircle, ArrowLeft, ArrowRight } from 'lucide-react';
|
||||||
import { useAuth, type PasswordChangeRequiredInfo } from '../auth/context';
|
import { useAuth, type PasswordChangeRequiredInfo } from '../auth/context';
|
||||||
import { Button } from '../components/buttons';
|
import { Button } from '../components/buttons';
|
||||||
import { Card } from '../components/reusable';
|
import { AuthShell } from '../components/auth/AuthShell';
|
||||||
import { PasswordRulesChecklist } from '../components/auth/PasswordRulesChecklist';
|
import { PasswordRulesChecklist } from '../components/auth/PasswordRulesChecklist';
|
||||||
import { usePasswordPolicy } from '../hooks/usePasswordPolicy';
|
import { usePasswordPolicy } from '../hooks/usePasswordPolicy';
|
||||||
import { cn } from '../lib/cn';
|
import { cn } from '../lib/cn';
|
||||||
@ -12,7 +12,6 @@ import {
|
|||||||
passwordTooLong,
|
passwordTooLong,
|
||||||
PASSWORD_MAX_CHARS,
|
PASSWORD_MAX_CHARS,
|
||||||
} from '../utils/passwordPolicy';
|
} from '../utils/passwordPolicy';
|
||||||
import logo from '../assets/logo.png';
|
|
||||||
|
|
||||||
/** Login gate. Redirects to /daily once authenticated. */
|
/** Login gate. Redirects to /daily once authenticated. */
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
@ -149,20 +148,10 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center p-4 bg-emerald-50">
|
<AuthShell
|
||||||
<Card className="w-full max-w-[420px] shadow-lg border-emerald-100 bg-white p-6 sm:p-7">
|
title={changeRequired ? 'Set a New Password' : 'Welcome Back'}
|
||||||
<div className="flex flex-col gap-2 items-center mb-6 text-center">
|
subtitle={changeRequired ? changeRequired.message : 'Sign in to continue to Krishna Sales'}
|
||||||
<img src={logo} alt="Krishna Sales Logo" className="h-16 w-auto object-contain" />
|
>
|
||||||
<h1 className="m-0 text-2xl font-extrabold text-emerald-900 tracking-[-0.02em]">
|
|
||||||
{changeRequired ? 'Set a New Password' : 'Krishna Sales'}
|
|
||||||
</h1>
|
|
||||||
{changeRequired && (
|
|
||||||
<p className="text-xs text-slate-600 mt-1 max-w-[340px] leading-relaxed">
|
|
||||||
{changeRequired.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 text-xs text-rose-700 bg-rose-50 border border-rose-200 p-3 rounded-lg flex items-start gap-2">
|
<div className="mb-4 text-xs text-rose-700 bg-rose-50 border border-rose-200 p-3 rounded-lg flex items-start gap-2">
|
||||||
<AlertCircle size={16} className="shrink-0 mt-0.5" />
|
<AlertCircle size={16} className="shrink-0 mt-0.5" />
|
||||||
@ -182,10 +171,8 @@ export function LoginPage() {
|
|||||||
|
|
||||||
{/* New Password */}
|
{/* New Password */}
|
||||||
<div className="flex flex-col gap-1.5 font-sans">
|
<div className="flex flex-col gap-1.5 font-sans">
|
||||||
<label className="text-xs font-semibold text-slate-700">
|
|
||||||
New password <span className="text-emerald-500">*</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
|
<Lock size={18} className="absolute left-4 text-slate-500 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
type={showNewPassword ? 'text' : 'password'}
|
type={showNewPassword ? 'text' : 'password'}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
@ -198,21 +185,22 @@ export function LoginPage() {
|
|||||||
autoFocus
|
autoFocus
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-[42px] w-full px-3 pr-10 rounded-md outline-none transition-all text-sm',
|
'h-12 w-full pl-11 pr-11 rounded-full outline-none transition-all text-sm',
|
||||||
fieldErrors.newPassword
|
fieldErrors.newPassword
|
||||||
? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400 focus:border-rose-500'
|
? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400'
|
||||||
: 'border border-emerald-200 bg-emerald-50/50 text-emerald-900 placeholder:text-slate-400 focus:ring-2 focus:ring-emerald-400 focus:border-emerald-400'
|
: 'border border-slate-200 bg-slate-100/80 text-slate-900 placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400'
|
||||||
)}
|
)}
|
||||||
placeholder="Enter new password"
|
aria-label="New password"
|
||||||
|
placeholder="New password"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowNewPassword((s) => !s)}
|
onClick={() => setShowNewPassword((s) => !s)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
|
className="absolute right-4 text-slate-600 hover:text-slate-800 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
{showNewPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
{showNewPassword ? <Eye size={18} /> : <EyeOff size={18} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<PasswordRulesChecklist
|
<PasswordRulesChecklist
|
||||||
@ -225,10 +213,8 @@ export function LoginPage() {
|
|||||||
|
|
||||||
{/* Confirm New Password */}
|
{/* Confirm New Password */}
|
||||||
<div className="flex flex-col gap-1.5 font-sans">
|
<div className="flex flex-col gap-1.5 font-sans">
|
||||||
<label className="text-xs font-semibold text-slate-700">
|
|
||||||
Confirm new password <span className="text-emerald-500">*</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
|
<Lock size={18} className="absolute left-4 text-slate-500 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
type={showConfirmPassword ? 'text' : 'password'}
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
@ -240,11 +226,12 @@ export function LoginPage() {
|
|||||||
}}
|
}}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-[42px] w-full px-3 pr-10 rounded-md outline-none transition-all text-sm',
|
'h-12 w-full pl-11 pr-11 rounded-full outline-none transition-all text-sm',
|
||||||
fieldErrors.confirmPassword
|
fieldErrors.confirmPassword
|
||||||
? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400 focus:border-rose-500'
|
? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400'
|
||||||
: 'border border-emerald-200 bg-emerald-50/50 text-emerald-900 placeholder:text-slate-400 focus:ring-2 focus:ring-emerald-400 focus:border-emerald-400'
|
: 'border border-slate-200 bg-slate-100/80 text-slate-900 placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400'
|
||||||
)}
|
)}
|
||||||
|
aria-label="Confirm new password"
|
||||||
placeholder="Confirm new password"
|
placeholder="Confirm new password"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@ -252,9 +239,9 @@ export function LoginPage() {
|
|||||||
onClick={() => setShowConfirmPassword((s) => !s)}
|
onClick={() => setShowConfirmPassword((s) => !s)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
|
className="absolute right-4 text-slate-600 hover:text-slate-800 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
{showConfirmPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
{showConfirmPassword ? <Eye size={18} /> : <EyeOff size={18} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -263,9 +250,9 @@ export function LoginPage() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
full
|
full
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className="mt-2 bg-emerald-600 hover:bg-emerald-700 text-white border-transparent"
|
className="mt-1 !rounded-full !h-12 !text-base bg-gradient-to-r from-[#3aa75a] to-[#0f5a30] hover:brightness-110 text-white border-transparent shadow-lg shadow-emerald-900/20"
|
||||||
>
|
>
|
||||||
{busy ? 'Updating & Signing in…' : 'Update Password & Sign In'}
|
{busy ? 'Updating & Signing in…' : (<>Update Password & Sign In <ArrowRight size={18} className="ml-2" /></>)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@ -281,12 +268,11 @@ export function LoginPage() {
|
|||||||
) : (
|
) : (
|
||||||
/* Standard Login Form */
|
/* Standard Login Form */
|
||||||
<form noValidate onSubmit={submit} className="flex flex-col gap-4">
|
<form noValidate onSubmit={submit} className="flex flex-col gap-4">
|
||||||
<label className="flex flex-col gap-1.5 font-sans">
|
<div className="relative flex items-center">
|
||||||
<span className="text-sm font-semibold text-emerald-800">
|
<Mail size={18} className="absolute left-4 text-slate-500 pointer-events-none" />
|
||||||
Email <span className="text-emerald-500">*</span>
|
|
||||||
</span>
|
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
|
aria-label="Email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setEmail(e.target.value);
|
setEmail(e.target.value);
|
||||||
@ -296,30 +282,21 @@ export function LoginPage() {
|
|||||||
autoFocus
|
autoFocus
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-[42px] px-3 rounded-md outline-none transition-all text-sm',
|
'h-12 w-full pl-11 pr-4 rounded-full outline-none transition-all text-sm',
|
||||||
fieldErrors.email
|
fieldErrors.email
|
||||||
? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400 focus:border-rose-500'
|
? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400'
|
||||||
: 'border border-emerald-200 bg-emerald-50/50 text-emerald-900 placeholder:text-slate-400 focus:ring-2 focus:ring-emerald-400 focus:border-emerald-400'
|
: 'border border-slate-200 bg-slate-100/80 text-slate-900 placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400'
|
||||||
)}
|
)}
|
||||||
placeholder="Enter your email"
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5 font-sans">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm font-semibold text-emerald-800">
|
|
||||||
Password <span className="text-emerald-500">*</span>
|
|
||||||
</span>
|
|
||||||
<Link
|
|
||||||
to="/forgot-password"
|
|
||||||
className="text-xs font-semibold text-emerald-700 hover:text-emerald-900 transition-colors"
|
|
||||||
>
|
|
||||||
Forgot password?
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
|
<Lock size={18} className="absolute left-4 text-slate-500 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
aria-label="Password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setPassword(e.target.value);
|
setPassword(e.target.value);
|
||||||
@ -328,36 +305,42 @@ export function LoginPage() {
|
|||||||
}}
|
}}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-[42px] w-full px-3 pr-10 rounded-md outline-none transition-all text-sm',
|
'h-12 w-full pl-11 pr-11 rounded-full outline-none transition-all text-sm',
|
||||||
fieldErrors.password
|
fieldErrors.password
|
||||||
? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400 focus:border-rose-500'
|
? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400'
|
||||||
: 'border border-emerald-200 bg-emerald-50/50 text-emerald-900 placeholder:text-slate-400 focus:ring-2 focus:ring-emerald-400 focus:border-emerald-400'
|
: 'border border-slate-200 bg-slate-100/80 text-slate-900 placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400'
|
||||||
)}
|
)}
|
||||||
placeholder="Enter your password"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword((s) => !s)}
|
onClick={() => setShowPassword((s) => !s)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
|
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||||
|
className="absolute right-4 text-slate-600 hover:text-slate-800 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
{showPassword ? <Eye size={18} /> : <EyeOff size={18} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<Link
|
||||||
|
to="/forgot-password"
|
||||||
|
className="self-end text-sm font-medium text-emerald-900 hover:text-emerald-700 transition-colors"
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
full
|
full
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className="mt-2 bg-emerald-600 hover:bg-emerald-700 text-white border-transparent"
|
className="mt-1 !rounded-full !h-12 !text-base bg-gradient-to-r from-[#3aa75a] to-[#0f5a30] hover:brightness-110 text-white border-transparent shadow-lg shadow-emerald-900/20"
|
||||||
>
|
>
|
||||||
{busy ? 'Signing in…' : 'Sign in'}
|
{busy ? 'Signing in…' : (<>Sign In <ArrowRight size={18} className="ml-2" /></>)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</AuthShell>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user