Files
Homecare_Admin/src/pages/LoginPage.jsx
T

437 lines
17 KiB
React

import { useState, useEffect, useRef } from 'react'
import { adminService } from '../services/adminService'
import './LoginPage.css'
/* ── Animated floating orbs background ── */
function Orbs() {
return (
<div className="orbs-container" aria-hidden="true">
<div className="orb orb-1" />
<div className="orb orb-2" />
<div className="orb orb-3" />
<div className="orb orb-4" />
</div>
)
}
/* ── Particle grid ── */
function GridLines() {
return <div className="grid-lines" aria-hidden="true" />
}
/* ── Eye icon for password toggle ── */
function EyeIcon({ open }) {
return open ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/>
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/>
<line x1="1" y1="23" x2="23" y2="1"/>
</svg>
)
}
/* ── Shield logo SVG ── */
function ShieldLogo() {
return (
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="shieldGrad" x1="0" y1="0" x2="44" y2="44" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#3b82f6"/>
<stop offset="100%" stopColor="#14b8a6"/>
</linearGradient>
</defs>
<path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z" fill="url(#shieldGrad)" opacity="0.15"/>
<path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z" stroke="url(#shieldGrad)" strokeWidth="1.5" fill="none"/>
<line x1="22" y1="13" x2="22" y2="31" stroke="url(#shieldGrad)" strokeWidth="2.5" strokeLinecap="round"/>
<line x1="13" y1="22" x2="31" y2="22" stroke="url(#shieldGrad)" strokeWidth="2.5" strokeLinecap="round"/>
</svg>
)
}
/* ── Spinner ── */
function Spinner() {
return (
<span className="spinner" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<circle cx="10" cy="10" r="8" stroke="rgba(255,255,255,0.3)" strokeWidth="2"/>
<path d="M10 2a8 8 0 0 1 8 8" stroke="#fff" strokeWidth="2" strokeLinecap="round"/>
</svg>
</span>
)
}
/* ─────────────────────────────────────────────── */
/* Main Login Page Component */
/* ─────────────────────────────────────────────── */
export default function LoginPage({ onLoginSuccess }) {
const [form, setForm] = useState({ email: 'admin@cureselect.com', password: 'CureSelect@2025', remember: true })
const [showPwd, setShowPwd] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [shake, setShake] = useState(false)
const [mounted, setMounted] = useState(false)
const [requires2fa, setRequires2fa] = useState(false)
const [sessionToken, setSessionToken] = useState('')
const [totpCode, setTotpCode] = useState('')
const emailRef = useRef(null)
const totpRef = useRef(null)
useEffect(() => {
const t = setTimeout(() => setMounted(true), 50)
emailRef.current?.focus()
return () => clearTimeout(t)
}, [])
useEffect(() => {
if (requires2fa) {
totpRef.current?.focus()
}
}, [requires2fa])
const handleChange = (e) => {
const { name, value, type, checked } = e.target
setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value }))
if (error) setError('')
}
const triggerShake = () => {
setShake(true)
setTimeout(() => setShake(false), 600)
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!form.email || !form.password) {
setError('Please fill in all fields.')
triggerShake()
return
}
setLoading(true)
setError('')
try {
const res = await adminService.auth.login(form.email, form.password)
if (res?.data?.requires2fa) {
setRequires2fa(true)
setSessionToken(res.data.sessionToken)
setLoading(false)
} else if (res?.data?.user) {
onLoginSuccess(res.data.user)
} else {
throw new Error('Invalid response structure received from server.')
}
} catch (err) {
setLoading(false)
setError(err.message || 'Login failed. Please check credentials.')
triggerShake()
}
}
const handle2faSubmit = async (e) => {
e.preventDefault()
if (!totpCode || totpCode.length < 6) {
setError('Please enter a valid 6-digit authentication code.')
triggerShake()
return
}
setLoading(true)
setError('')
try {
const res = await adminService.auth.verifyTotp(form.email, totpCode, sessionToken)
if (res?.data?.user) {
onLoginSuccess(res.data.user)
} else {
throw new Error('2FA verification failed.')
}
} catch (err) {
setLoading(false)
setError(err.message || 'Invalid 2FA code. Please try again.')
triggerShake()
}
}
return (
<div className="login-root">
<GridLines />
<Orbs />
{/* ── Left panel — brand hero ── */}
<aside className={`login-hero ${mounted ? 'mounted' : ''}`} aria-hidden="true">
<div className="hero-content">
<div className="hero-badge">
<span className="badge-dot" />
System Live & Connected
</div>
<h1 className="hero-title">
CureSelect<br />
<span className="hero-title-accent">Healthcare LLP</span>
</h1>
<p className="hero-subtitle">
Super Admin Control Center — manage patients, caregivers, teleconsults, hospital networks and enterprise operations from one unified dashboard.
</p>
<div className="hero-stats">
<div className="stat-card">
<span className="stat-value">2,400+</span>
<span className="stat-label">Active Patients</span>
</div>
<div className="stat-card">
<span className="stat-value">340+</span>
<span className="stat-label">Caregivers</span>
</div>
<div className="stat-card">
<span className="stat-value">99.9%</span>
<span className="stat-label">System Uptime</span>
</div>
</div>
<div className="hero-graphic">
<div className="pulse-ring pulse-ring-1" />
<div className="pulse-ring pulse-ring-2" />
<div className="pulse-ring pulse-ring-3" />
<div className="hero-icon-center">
<svg width="64" height="64" viewBox="0 0 44 44" fill="none">
<defs>
<linearGradient id="hg1" x1="0" y1="0" x2="44" y2="44" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#3b82f6"/>
<stop offset="100%" stopColor="#14b8a6"/>
</linearGradient>
</defs>
<path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z"
fill="url(#hg1)" opacity="0.2"/>
<path d="M22 2L4 10v12c0 10.5 7.7 20.3 18 22.6C32.3 42.3 40 32.5 40 22V10L22 2z"
stroke="url(#hg1)" strokeWidth="1.5" fill="none"/>
<line x1="22" y1="13" x2="22" y2="31" stroke="url(#hg1)" strokeWidth="2.5" strokeLinecap="round"/>
<line x1="13" y1="22" x2="31" y2="22" stroke="url(#hg1)" strokeWidth="2.5" strokeLinecap="round"/>
</svg>
</div>
</div>
</div>
<div className="hero-footer">
<span>CureSelect Healthcare LLP © 2025</span>
<span>·</span>
<span>Version 1.0.0</span>
</div>
</aside>
{/* ── Right panel — login form ── */}
<main className={`login-panel ${mounted ? 'mounted' : ''}`}>
<div className={`login-card ${shake ? 'shake' : ''}`}>
{/* Card header */}
<div className="card-header">
<div className="card-logo">
<ShieldLogo />
</div>
<div className="card-title-block">
<h2 className="card-title">Super Admin</h2>
<p className="card-subtitle">Secure administrative access</p>
</div>
</div>
{/* Divider */}
<div className="card-divider">
<span>{requires2fa ? 'Two-Factor Authentication' : 'Sign in to your account'}</span>
</div>
{/* Error banner */}
{error && (
<div className="error-banner" role="alert">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
{error}
</div>
)}
{requires2fa ? (
/* ── 2FA TOTP Form ── */
<form onSubmit={handle2faSubmit} className="login-form" noValidate>
<p style={{ color: '#94a3b8', fontSize: '0.88rem', marginBottom: '1rem', lineHeight: '1.5' }}>
Enter the 6-digit verification code from your Authenticator app for <strong>{form.email}</strong>.
</p>
<div className="field-group">
<label htmlFor="totpCode" className="field-label">
Authentication Code
</label>
<div className="field-wrapper">
<span className="field-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
</span>
<input
id="totpCode"
ref={totpRef}
type="text"
name="totpCode"
maxLength={6}
value={totpCode}
onChange={(e) => setTotpCode(e.target.value.trim())}
placeholder="123456"
className="field-input"
autoComplete="one-time-code"
required
/>
</div>
</div>
<button
type="submit"
className={`login-btn ${loading ? 'loading' : ''}`}
disabled={loading}
>
{loading ? <><Spinner /> Verifying…</> : 'Verify & Sign In'}
</button>
<button
type="button"
className="forgot-btn"
style={{ marginTop: '0.75rem', width: '100%', textAlign: 'center' }}
onClick={() => setRequires2fa(false)}
>
← Back to Login
</button>
</form>
) : (
/* ── Standard Login Form ── */
<form onSubmit={handleSubmit} className="login-form" noValidate>
{/* Email field */}
<div className="field-group">
<label htmlFor="email" className="field-label">
Email Address
</label>
<div className="field-wrapper">
<span className="field-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
</span>
<input
id="email"
ref={emailRef}
type="email"
name="email"
value={form.email}
onChange={handleChange}
placeholder="admin@cureselect.com"
className="field-input"
autoComplete="username"
required
/>
</div>
</div>
{/* Password field */}
<div className="field-group">
<label htmlFor="password" className="field-label">
Password
</label>
<div className="field-wrapper">
<span className="field-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
</span>
<input
id="password"
type={showPwd ? 'text' : 'password'}
name="password"
value={form.password}
onChange={handleChange}
placeholder="Enter your password"
className="field-input"
autoComplete="current-password"
required
/>
<button
type="button"
className="pwd-toggle"
onClick={() => setShowPwd(v => !v)}
aria-label={showPwd ? 'Hide password' : 'Show password'}
>
<EyeIcon open={showPwd} />
</button>
</div>
</div>
{/* Remember + Forgot */}
<div className="form-options">
<label className="remember-label" htmlFor="remember">
<input
type="checkbox"
id="remember"
name="remember"
checked={form.remember}
onChange={handleChange}
className="remember-check"
/>
<span className="custom-check" aria-hidden="true" />
Remember me
</label>
<button type="button" className="forgot-btn">Forgot password?</button>
</div>
{/* Submit */}
<button
id="login-submit-btn"
type="submit"
className={`login-btn ${loading ? 'loading' : ''}`}
disabled={loading}
>
{loading ? (
<>
<Spinner />
Authenticating…
</>
) : (
<>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
<polyline points="10 17 15 12 10 7"/>
<line x1="15" y1="12" x2="3" y2="12"/>
</svg>
Sign In to Dashboard
</>
)}
</button>
</form>
)}
{/* Demo hint */}
<div className="demo-hint">
<span className="hint-icon">💡</span>
<span>Default: <strong>admin@cureselect.com</strong> / <strong>CureSelect@2025</strong></span>
</div>
{/* Security badge */}
<div className="security-badge">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="3" y="11" width="18" height="11" rx="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
256-bit SSL encrypted · Direct API Integration
</div>
</div>
</main>
</div>
)
}