826 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { loadFrappeTranslations } from '../i18n';
import { bootstrapFrappeUserFromSession } from '../utils/bootstrapFrappeUserFromSession';
interface LoginFormData {
email: string;
password: string;
}
const AFTER_PASSWORD_RESET_KEY = 'asm_show_after_password_reset';
const TWO_FACTOR_TMP_ID_KEY = 'asm_login_tmp_id';
type LoginStep = 'credentials' | 'otp';
const Login: React.FC = () => {
const [formData, setFormData] = useState<LoginFormData>({
email: '',
password: '',
});
const [loading, setLoading] = useState(false);
const [checkingSession, setCheckingSession] = useState(true);
const [error, setError] = useState<string | null>(null);
const [forgotOpen, setForgotOpen] = useState(false);
const [forgotEmail, setForgotEmail] = useState('');
const [forgotLoading, setForgotLoading] = useState(false);
const [forgotError, setForgotError] = useState<string | null>(null);
const [forgotMessage, setForgotMessage] = useState(false);
const [pwdResetBusy, setPwdResetBusy] = useState(false);
const [postResetBanner, setPostResetBanner] = useState(false);
const [loginStep, setLoginStep] = useState<LoginStep>('credentials');
const [tmpId, setTmpId] = useState<string | null>(null);
const [otpCode, setOtpCode] = useState('');
const [verification, setVerification] = useState<{
method: string;
setup?: boolean;
prompt?: string;
} | null>(null);
const navigate = useNavigate();
const location = useLocation();
const { t } = useTranslation();
const manualLoginHandledRef = useRef(false);
const forgotAbortRef = useRef<AbortController | null>(null);
const closeForgotModal = useCallback(() => {
forgotAbortRef.current?.abort();
forgotAbortRef.current = null;
setForgotOpen(false);
setForgotLoading(false);
setForgotError(null);
setForgotMessage(false);
}, []);
const openForgotModal = useCallback(() => {
setForgotOpen(true);
setForgotEmail(formData.email);
setForgotError(null);
setForgotMessage(false);
setForgotLoading(false);
setError(null);
}, [formData.email]);
useEffect(() => {
if (!forgotOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') closeForgotModal();
};
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [forgotOpen, closeForgotModal]);
useEffect(() => {
let cancelled = false;
(async () => {
if (
typeof sessionStorage !== 'undefined' &&
sessionStorage.getItem(AFTER_PASSWORD_RESET_KEY) === '1'
) {
sessionStorage.removeItem(AFTER_PASSWORD_RESET_KEY);
if (!cancelled) setPostResetBanner(true);
}
const params = new URLSearchParams(
typeof window !== 'undefined' ? window.location.search : ''
);
if (params.get('manual_login') === '1' && !manualLoginHandledRef.current) {
manualLoginHandledRef.current = true;
if (!cancelled) setPwdResetBusy(true);
localStorage.removeItem('user');
localStorage.removeItem('frappe_session_id');
const baseURL = import.meta.env.VITE_FRAPPE_BASE_URL || '';
let csrf =
typeof window !== 'undefined'
? (window as { csrf_token?: string }).csrf_token
: undefined;
if (!csrf) {
try {
const csrfRes = await fetch(`${baseURL}/api/method/frappe.sessions.get_csrf_token`, {
method: 'GET',
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (csrfRes.ok) {
const csrfData = (await csrfRes.json()) as { message?: string };
if (typeof csrfData.message === 'string') csrf = csrfData.message;
}
} catch {
/* ignore */
}
}
const headers: Record<string, string> = { Accept: 'application/json' };
if (csrf) headers['X-Frappe-CSRF-Token'] = csrf;
try {
await fetch(`${baseURL}/api/method/logout`, {
method: 'POST',
credentials: 'include',
headers,
});
} catch {
/* ignore */
}
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem(AFTER_PASSWORD_RESET_KEY, '1');
}
if (cancelled) return;
setPwdResetBusy(false);
navigate('/login', { replace: true });
return;
}
if (localStorage.getItem('user')) {
if (!cancelled) {
setCheckingSession(false);
navigate('/dashboard', { replace: true });
}
return;
}
const result = await bootstrapFrappeUserFromSession();
if (cancelled) return;
if (result.ok) {
try {
await loadFrappeTranslations();
} catch (err) {
console.warn('Could not load translations after session bootstrap:', err);
}
if (!cancelled) navigate('/dashboard', { replace: true });
}
if (!cancelled) setCheckingSession(false);
})();
return () => {
cancelled = true;
};
}, [navigate, location.pathname, location.search]);
const baseUrl = import.meta.env.BASE_URL || '/';
const logoVersion = import.meta.env.DEV
? `?v=${Date.now()}`
: `?v=1774269853`; // Auto-updated by build script
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
setError(null);
};
const completeLogin = useCallback(
async (user: { full_name?: string; user_id?: string; sid?: string; email?: string }) => {
const apiService = (await import('../services/apiService')).default;
const userData = {
...user,
email: user.email || formData.email,
};
localStorage.setItem('user', JSON.stringify(userData));
if (user.sid) {
apiService.setSessionId(user.sid);
}
sessionStorage.removeItem(TWO_FACTOR_TMP_ID_KEY);
try {
await loadFrappeTranslations();
} catch (err) {
console.warn('Could not load translations after login:', err);
}
navigate('/dashboard');
},
[formData.email, navigate]
);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const apiService = (await import('../services/apiService')).default;
const result = await apiService.login(formData);
if (result.status === 'two_factor_required') {
sessionStorage.setItem(TWO_FACTOR_TMP_ID_KEY, result.tmp_id);
setTmpId(result.tmp_id);
setVerification(result.verification);
setLoginStep('otp');
setOtpCode('');
return;
}
await completeLogin(result.user);
} catch (err: unknown) {
console.error('Login error:', err);
const message = err instanceof Error ? err.message : t('login.loginFailed');
setError(message);
} finally {
setLoading(false);
}
};
const handleOtpSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const storedTmpId = tmpId || sessionStorage.getItem(TWO_FACTOR_TMP_ID_KEY);
if (!storedTmpId) {
setError(t('login.twoFactorSessionExpired'));
setLoginStep('credentials');
return;
}
if (!otpCode.trim()) {
setError(t('login.twoFactorCodeRequired'));
return;
}
setLoading(true);
setError(null);
try {
const apiService = (await import('../services/apiService')).default;
const result = await apiService.verifyLoginOtp(storedTmpId, otpCode);
if (result.status === 'logged_in') {
await completeLogin(result.user);
}
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('login.twoFactorInvalid');
setError(message);
} finally {
setLoading(false);
}
};
const backToCredentials = () => {
sessionStorage.removeItem(TWO_FACTOR_TMP_ID_KEY);
setLoginStep('credentials');
setTmpId(null);
setVerification(null);
setOtpCode('');
setError(null);
};
const handleDemoLogin = async () => {
const demoUser = {
full_name: 'Demo User',
email: 'demo@seeraarabia.com',
user_image: '',
roles: ['System Manager', 'Administrator']
};
localStorage.setItem('user', JSON.stringify(demoUser));
// Load translations from Frappe after demo login
try {
await loadFrappeTranslations();
} catch (err) {
console.warn('Could not load translations after demo login:', err);
}
navigate('/dashboard');
};
const handleForgotPasswordSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const userVal = forgotEmail.trim();
if (!userVal) {
setForgotError(t('login.forgotPasswordUserRequired'));
return;
}
forgotAbortRef.current?.abort();
const controller = new AbortController();
forgotAbortRef.current = controller;
setForgotLoading(true);
setForgotError(null);
setForgotMessage(false);
const forgotApi = await import('../services/apiService');
try {
await forgotApi.default.requestPasswordReset(userVal, controller.signal);
setForgotMessage(true);
} catch (err: unknown) {
if (err instanceof Error && err.name === 'AbortError') {
setForgotError(t('login.forgotPasswordTimeout'));
} else if (err instanceof forgotApi.ApiError) {
if (err.code === 'USER_NOT_FOUND') setForgotError(t('login.forgotPasswordNotFound'));
else if (err.code === 'RESET_NOT_ALLOWED') setForgotError(t('login.forgotPasswordCannotReset'));
else if (err.code === 'FORBIDDEN') setForgotError(t('login.forgotPasswordFailed'));
else if (err.code === 'EMPTY_EMAIL') setForgotError(t('login.forgotPasswordUserRequired'));
else setForgotError(t('login.forgotPasswordFailed'));
} else {
setForgotError(t('login.forgotPasswordFailed'));
}
} finally {
if (forgotAbortRef.current === controller) {
forgotAbortRef.current = null;
}
setForgotLoading(false);
}
};
if (checkingSession || pwdResetBusy) {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="flex flex-col items-center gap-3 text-gray-600 dark:text-gray-400">
<svg
className="h-10 w-10 animate-spin text-indigo-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<span className="text-sm">
{pwdResetBusy ? t('login.finishingSignOut') : t('common.loading')}
</span>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<div className="flex justify-center mb-6">
<div className="w-32 h-32 flex items-center justify-center bg-white dark:bg-gray-800 rounded-2xl shadow-2xl p-4">
<img
src={`${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}seera-logo.png${logoVersion}`}
alt="Seera Arabia"
className="w-full h-full object-contain"
onError={(e) => {
const container = e.currentTarget.parentElement;
if (container) {
container.classList.add('bg-gradient-to-br', 'from-indigo-600', 'to-purple-600');
}
e.currentTarget.style.display = 'none';
const nextSibling = e.currentTarget.nextElementSibling;
if (nextSibling) {
nextSibling.classList.remove('hidden');
}
}}
/>
<svg className="w-20 h-20 hidden" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" fill="white" fillOpacity="0.9"/>
<path d="M2 17L12 22L22 17V12L12 17L2 12V17Z" fill="white" fillOpacity="0.7"/>
<path d="M12 12V17" stroke="white" strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
</div>
<h2 className="text-center text-3xl font-semibold text-gray-900 dark:text-white">
{t('login.title')}
</h2>
<p className="mt-2 text-center text-sm font-medium text-indigo-600 dark:text-indigo-400">
{t('login.subtitle')}
</p>
<p className="mt-1 text-center text-xs text-gray-600 dark:text-gray-400">
{loginStep === 'otp' ? t('login.twoFactorTitle') : t('login.signIn')}
</p>
</div>
<div className="mt-8 space-y-6">
{postResetBanner && loginStep === 'credentials' && (
<div className="rounded-md bg-green-50 p-4 dark:bg-green-900/20">
<p className="text-sm text-green-800 dark:text-green-300">
{t('login.afterPasswordResetSignIn')}
</p>
</div>
)}
{loginStep === 'otp' ? (
<form className="space-y-6" onSubmit={handleOtpSubmit}>
<div className="rounded-md bg-indigo-50 p-4 text-sm text-indigo-900 dark:bg-indigo-900/20 dark:text-indigo-200">
{verification?.method === 'Email' && verification.prompt ? (
<p>{verification.prompt}</p>
) : verification?.method === 'OTP App' && verification.setup ? (
<p>{t('login.twoFactorOtpAppEnter')}</p>
) : verification?.method === 'OTP App' && !verification.setup ? (
<p>{t('login.twoFactorOtpAppSetupIncomplete')}</p>
) : (
<p>{t('login.twoFactorOtpAppEnter')}</p>
)}
{verification?.method === 'Email' && (
<p className="mt-2 text-xs text-indigo-800 dark:text-indigo-300">
{t('login.twoFactorEmailQrHint')}
</p>
)}
</div>
<div>
<label htmlFor="otp" className="sr-only">
{t('login.twoFactorCodeLabel')}
</label>
<input
id="otp"
name="otp"
type="text"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={6}
required
autoFocus
className="relative block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-center text-lg tracking-widest text-gray-900 placeholder-gray-500 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white"
placeholder={t('login.twoFactorCodePlaceholder')}
value={otpCode}
onChange={(ev) => {
setOtpCode(ev.target.value.replace(/\D/g, '').slice(0, 6));
setError(null);
}}
/>
</div>
{error && (
<div className="rounded-md bg-red-50 p-4 dark:bg-red-900/20">
<div className="text-sm text-red-700 dark:text-red-400">{error}</div>
</div>
)}
<button
type="submit"
disabled={loading}
className="group relative flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
{loading ? t('common.loading') : t('login.twoFactorVerify')}
</button>
<button
type="button"
onClick={backToCredentials}
className="w-full text-center text-sm font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400"
>
{t('login.twoFactorBackToLogin')}
</button>
</form>
) : (
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
{t('common.email')}
</label>
<input
id="email"
name="email"
type="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-800 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder={t('login.emailPlaceholder')}
value={formData.email}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
{t('common.password')}
</label>
<input
id="password"
name="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-800 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder={t('login.passwordPlaceholder')}
value={formData.password}
onChange={handleChange}
/>
</div>
</div>
{error && (
<div className="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
<div className="text-sm text-red-700 dark:text-red-400">{error}</div>
</div>
)}
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<div className="flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{t('common.loading')}
</div>
) : (
t('common.login')
)}
</button>
</form>
)}
{loginStep === 'credentials' && (
<div className="text-center">
<button
type="button"
onClick={openForgotModal}
className="text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 dark:hover:text-indigo-300 focus:outline-none focus:underline"
>
{t('login.forgotPassword')}
</button>
</div>
)}
<div className="hidden space-y-3" aria-hidden="true">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300 dark:border-gray-600" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400">{t('login.or')}</span>
</div>
</div>
<button
type="button"
onClick={handleDemoLogin}
tabIndex={-1}
className="w-full flex justify-center py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
🚀 {t('login.demoLogin')}
</button>
</div>
</div>
</div>
{forgotOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onClick={closeForgotModal}
role="presentation"
>
<div
className="relative w-full max-w-md rounded-lg border border-gray-200 bg-white p-5 shadow-xl dark:border-gray-700 dark:bg-gray-800"
onClick={(ev) => ev.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby="forgot-password-title"
>
<button
type="button"
onClick={closeForgotModal}
className="absolute right-3 top-3 rounded p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-700 dark:hover:text-gray-200"
aria-label={t('login.forgotPasswordClose')}
>
×
</button>
<h3
id="forgot-password-title"
className="pr-8 text-lg font-semibold text-gray-900 dark:text-white"
>
{t('login.forgotPasswordTitle')}
</h3>
<p className="mt-2 text-xs leading-relaxed text-gray-600 dark:text-gray-400">
{t('login.forgotPasswordHint')}
</p>
<form className="mt-4 space-y-3" onSubmit={handleForgotPasswordSubmit}>
<input
type="text"
name="reset-user"
autoComplete="username"
value={forgotEmail}
onChange={(ev) => {
setForgotEmail(ev.target.value);
setForgotError(null);
setForgotMessage(false);
}}
className="relative block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-500 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder-gray-400"
placeholder={t('login.forgotPasswordUserPlaceholder')}
/>
{forgotError && (
<div className="rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/20 dark:text-red-400">
{forgotError}
</div>
)}
{forgotMessage && (
<div className="rounded-md bg-green-50 p-3 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-300">
{t('login.forgotPasswordSentSuccess')}
</div>
)}
<div className="flex flex-col gap-2 sm:flex-row sm:justify-end sm:gap-3">
<button
type="button"
onClick={closeForgotModal}
className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
>
{t('login.forgotPasswordClose')}
</button>
<button
type="submit"
disabled={forgotLoading}
className="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
{forgotLoading ? t('common.loading') : t('login.forgotPasswordSubmit')}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
export default Login;
// import React, { useState } from 'react';
// import { useNavigate } from 'react-router-dom';
// import { useAuth } from '../hooks/useApi';
// interface LoginFormData {
// email: string;
// password: string;
// }
// const Login: React.FC = () => {
// const [formData, setFormData] = useState<LoginFormData>({
// email: '',
// password: '',
// });
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState<string | null>(null);
// const navigate = useNavigate();
// const { login } = useAuth();
// const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// const { name, value } = e.target;
// setFormData(prev => ({
// ...prev,
// [name]: value,
// }));
// setError(null);
// };
// const handleSubmit = async (e: React.FormEvent) => {
// e.preventDefault();
// setLoading(true);
// setError(null);
// try {
// const response = await login(formData);
// if (response && response.message) {
// // Store user info in localStorage with email field
// const userData = {
// ...response.message,
// email: formData.email // Ensure email is stored
// };
// localStorage.setItem('user', JSON.stringify(userData));
// navigate('/dashboard');
// } else {
// setError('Login failed. Please check your credentials.');
// }
// } catch (err: any) {
// setError(err.message || 'Login failed. Please try again.');
// } finally {
// setLoading(false);
// }
// };
// const handleDemoLogin = () => {
// // Create dummy user data for demo purposes
// const demoUser = {
// full_name: 'Demo User',
// email: 'demo@seeraarabia.com',
// user_image: '',
// roles: ['System Manager', 'Administrator']
// };
// // Store demo user in localStorage
// localStorage.setItem('user', JSON.stringify(demoUser));
// navigate('/dashboard');
// };
// return (
// <div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
// <div className="max-w-md w-full space-y-8">
// <div>
// <div className="flex justify-center mb-6">
// <div className="w-32 h-32 flex items-center justify-center bg-white dark:bg-gray-800 rounded-2xl shadow-2xl p-4">
// {/* Seera Arabia Logo */}
// <img
// src="/seera-logo.png?v=1765198405"
// alt="Seera Arabia"
// className="w-full h-full object-contain"
// onError={(e) => {
// // Fallback to gradient background with SVG if image not found
// const container = e.currentTarget.parentElement;
// if (container) {
// container.classList.add('bg-gradient-to-br', 'from-indigo-600', 'to-purple-600');
// }
// e.currentTarget.style.display = 'none';
// e.currentTarget.nextElementSibling?.classList.remove('hidden');
// }}
// />
// <svg className="w-20 h-20 hidden" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
// <path d="M12 2L2 7L12 12L22 7L12 2Z" fill="white" fillOpacity="0.9"/>
// <path d="M2 17L12 22L22 17V12L12 17L2 12V17Z" fill="white" fillOpacity="0.7"/>
// <path d="M12 12V17" stroke="white" strokeWidth="2" strokeLinecap="round"/>
// </svg>
// </div>
// </div>
// <h2 className="text-center text-3xl font-semibold text-gray-900 dark:text-white">
// Seera Arabia
// </h2>
// <p className="mt-2 text-center text-sm font-medium text-indigo-600 dark:text-indigo-400">
// Asset Management System
// </p>
// <p className="mt-1 text-center text-xs text-gray-600 dark:text-gray-400">
// Sign in to continue
// </p>
// </div>
// <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
// <div className="rounded-md shadow-sm -space-y-px">
// <div>
// <label htmlFor="email" className="sr-only">
// Email
// </label>
// <input
// id="email"
// name="email"
// type="email"
// required
// className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-800 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
// placeholder="Email"
// value={formData.email}
// onChange={handleChange}
// />
// </div>
// <div>
// <label htmlFor="password" className="sr-only">
// Password
// </label>
// <input
// id="password"
// name="password"
// type="password"
// required
// className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-800 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
// placeholder="Password"
// value={formData.password}
// onChange={handleChange}
// />
// </div>
// </div>
// {error && (
// <div className="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
// <div className="text-sm text-red-700 dark:text-red-400">{error}</div>
// </div>
// )}
// <div className="space-y-3">
// <button
// type="submit"
// disabled={loading}
// className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
// >
// {loading ? (
// <div className="flex items-center">
// <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
// <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
// <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
// </svg>
// Signing in...
// </div>
// ) : (
// 'Sign in'
// )}
// </button>
// <div className="relative">
// <div className="absolute inset-0 flex items-center">
// <div className="w-full border-t border-gray-300 dark:border-gray-600" />
// </div>
// <div className="relative flex justify-center text-sm">
// <span className="px-2 bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400">or</span>
// </div>
// </div>
// <button
// type="button"
// onClick={handleDemoLogin}
// className="w-full flex justify-center py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
// >
// 🚀 {t('login.demoLogin')}
// </button>
// </div>
// </form>
// </div>
// </div>
// );
// };
// export default Login;