'use client' import Image from 'next/image' import Link from 'next/link' import { useEffect, useState } from 'react' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { useDashboardI18n } from '@/components/I18nProvider' import { adminUrl, marketplaceUrl } from '@/lib/urls' import { API_BASE, EMPLOYEE_PROFILE_KEY, EMPLOYEE_TOKEN_KEY } from '@/lib/api' function notifyParent(message: Record) { if (typeof window === 'undefined' || window.parent === window) return window.parent.postMessage(message, '*') } export default function SignInPage() { const { language, theme, setLanguage, setTheme } = useDashboardI18n() const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const requestedPortal = searchParams.get('portal') === 'admin' ? 'admin' : 'workspace' const [portal, setPortal] = useState<'admin' | 'workspace'>(requestedPortal) const requestedLanguage = searchParams.get('lang') const requestedTheme = searchParams.get('theme') const dict = { en: { title: 'Sign in', workspaceTab: 'Company Workspace', platformTab: 'Platform', subtitle: 'Choose where you want to sign in, then enter your credentials.', email: 'Email', password: 'Password', signIn: 'Sign in', signingIn: 'Signing in…', verify: 'Verify code', verifying: 'Verifying…', authCode: 'Authentication code', enterCode: 'Enter the 6-digit code from your authenticator app.', back: 'Back to credentials', forgotPassword: 'Forgot your password?', newAccount: 'New company account?', createWorkspace: 'Create your workspace', invalidCredentials: 'Invalid email or password.', passwordNotSet: 'No password set yet. Check your invitation email or use "Forgot your password?"', unexpectedError: 'Something went wrong. Please try again.', }, fr: { title: 'Connexion', workspaceTab: 'Espace', platformTab: 'Plateforme', subtitle: 'Choisissez votre accès puis saisissez vos identifiants.', email: 'Email', password: 'Mot de passe', signIn: 'Connexion', signingIn: 'Connexion…', verify: 'Vérifier le code', verifying: 'Vérification…', authCode: 'Code d’authentification', enterCode: 'Entrez le code à 6 chiffres de votre application d’authentification.', back: 'Retour aux identifiants', forgotPassword: 'Mot de passe oublié ?', newAccount: 'Nouveau compte entreprise ?', createWorkspace: 'Créer votre espace', invalidCredentials: 'Email ou mot de passe invalide.', passwordNotSet: 'Aucun mot de passe défini. Vérifiez votre e-mail d\'invitation ou utilisez « Mot de passe oublié ? »', unexpectedError: 'Une erreur est survenue. Veuillez réessayer.', }, ar: { title: 'تسجيل الدخول', workspaceTab: 'المساحة', platformTab: 'المنصة', subtitle: 'اختر جهة الدخول ثم أدخل بياناتك.', email: 'البريد الإلكتروني', password: 'كلمة المرور', signIn: 'تسجيل الدخول', signingIn: 'جارٍ تسجيل الدخول…', verify: 'تحقق من الرمز', verifying: 'جارٍ التحقق…', authCode: 'رمز المصادقة', enterCode: 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.', back: 'العودة إلى بيانات الدخول', forgotPassword: 'نسيت كلمة المرور؟', newAccount: 'حساب شركة جديد؟', createWorkspace: 'أنشئ مساحتك', invalidCredentials: 'البريد الإلكتروني أو كلمة المرور غير صحيحة.', passwordNotSet: 'لم يتم تعيين كلمة مرور بعد. تحقق من بريد الدعوة أو استخدم "نسيت كلمة المرور؟"', unexpectedError: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.', }, }[language] const themeStyles = { light: { main: 'bg-[radial-gradient(circle_at_top,#dbeafe,transparent_35%),linear-gradient(180deg,#f8fafc,white)]', logoFrame: 'border-blue-100 bg-white shadow-sm', brand: 'text-blue-600', title: 'text-slate-900', subtitle: 'text-slate-500', card: 'border-slate-200 bg-white shadow-sm', toggleWrap: 'border-slate-200 bg-slate-50', activeToggle: 'bg-slate-900 text-white shadow-sm', inactiveToggle: 'text-slate-600 hover:bg-white hover:text-slate-900', }, medium: { main: 'bg-[radial-gradient(circle_at_top,rgba(148,163,184,0.32),transparent_35%),linear-gradient(180deg,#d7dee8,#f3f4f6)]', logoFrame: 'border-slate-300 bg-white/90 shadow-[0_12px_30px_rgba(71,85,105,0.18)]', brand: 'text-slate-700', title: 'text-slate-900', subtitle: 'text-slate-600', card: 'border-slate-300/80 bg-white/88 shadow-[0_18px_50px_rgba(51,65,85,0.14)] backdrop-blur', toggleWrap: 'border-slate-300 bg-slate-200/80', activeToggle: 'bg-amber-400 text-slate-950 shadow-sm', inactiveToggle: 'text-slate-700 hover:bg-white/85 hover:text-slate-950', }, dark: { main: 'bg-[radial-gradient(circle_at_top,rgba(37,99,235,0.2),transparent_35%),linear-gradient(180deg,#020617,#0f172a)]', logoFrame: 'border-slate-700 bg-slate-900 shadow-[0_12px_30px_rgba(2,6,23,0.45)]', brand: 'text-amber-400', title: 'text-slate-100', subtitle: 'text-slate-400', card: 'border-slate-800 bg-slate-900/92 shadow-[0_18px_50px_rgba(2,6,23,0.45)] backdrop-blur', toggleWrap: 'border-slate-700 bg-slate-950/80', activeToggle: 'bg-amber-400 text-slate-950 shadow-sm', inactiveToggle: 'text-slate-300 hover:bg-slate-800 hover:text-white', }, }[theme] useEffect(() => { setPortal(requestedPortal) }, [requestedPortal]) useEffect(() => { if ( (requestedLanguage === 'en' || requestedLanguage === 'fr' || requestedLanguage === 'ar') && requestedLanguage !== language ) { setLanguage(requestedLanguage) } }, [language, requestedLanguage, setLanguage]) useEffect(() => { if ( (requestedTheme === 'light' || requestedTheme === 'medium' || requestedTheme === 'dark') && requestedTheme !== theme ) { setTheme(requestedTheme) } }, [requestedTheme, setTheme, theme]) useEffect(() => { const currentPath = window.location.pathname + (window.location.search || '') notifyParent({ type: 'rentaldrivego:embedded-path', path: currentPath }) }, [pathname, searchParams]) function handlePortalChange(nextPortal: 'admin' | 'workspace') { setPortal(nextPortal) const nextParams = new URLSearchParams(searchParams.toString()) if (nextPortal === 'admin') { nextParams.set('portal', 'admin') } else { nextParams.delete('portal') } const nextSearch = nextParams.toString() router.replace(nextSearch ? `${pathname}?${nextSearch}` : pathname, { scroll: false }) } return (
RentalDriveGo
RentalDriveGo

{dict.title}

{dict.subtitle}

{([ { value: 'workspace', label: dict.workspaceTab }, { value: 'admin', label: dict.platformTab }, ] as const).map((option) => { const active = option.value === portal return ( ) })}
) } function LocalSignInForm({ dict, portal }: { dict: { email: string password: string signIn: string signingIn: string verify: string verifying: string authCode: string enterCode: string back: string forgotPassword: string invalidCredentials: string passwordNotSet: string unexpectedError: string } portal: 'admin' | 'workspace' }) { const router = useRouter() const searchParams = useSearchParams() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [totpCode, setTotpCode] = useState('') const [step, setStep] = useState<'credentials' | 'totp'>('credentials') const [showPassword, setShowPassword] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const adminNext = searchParams.get('next') || '/dashboard' const employeeRedirect = searchParams.get('redirect') || '/dashboard' const forgotPasswordHref = portal === 'admin' ? `${adminUrl}/forgot-password` : '/forgot-password' useEffect(() => { setStep('credentials') setTotpCode('') setError(null) }, [portal]) function redirectAdmin(token: string) { const hash = new URLSearchParams({ token, next: adminNext }).toString() window.location.href = `${adminUrl}/auth-redirect#${hash}` } async function handleCredentials(e: React.FormEvent) { e.preventDefault() setLoading(true) setError(null) try { if (portal === 'admin') { const adminRes = await fetch(`${API_BASE}/admin/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) const adminJson = await adminRes.json() if (adminRes.ok && adminJson?.data?.token) { redirectAdmin(adminJson.data.token) return } if (adminRes.status === 401 && adminJson?.error === 'totp_required') { setStep('totp') return } setError(dict.invalidCredentials) return } const empRes = await fetch(`${API_BASE}/auth/employee/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) const empJson = await empRes.json() if (empRes.ok && empJson?.data?.token) { const token = empJson.data.token localStorage.setItem(EMPLOYEE_TOKEN_KEY, token) if (empJson?.data?.employee) { localStorage.setItem(EMPLOYEE_PROFILE_KEY, JSON.stringify(empJson.data.employee)) } document.cookie = `employee_token=${token}; path=/; max-age=28800; samesite=lax` window.dispatchEvent(new CustomEvent('rentaldrivego:auth-changed')) notifyParent({ type: 'rentaldrivego:employee-login', path: employeeRedirect }) router.push(employeeRedirect) return } if (empJson?.error === 'password_not_set') { setError(dict.passwordNotSet) return } setError(dict.invalidCredentials) } catch { setError(dict.unexpectedError) } finally { setLoading(false) } } async function handleTotp(e: React.FormEvent) { e.preventDefault() setLoading(true) setError(null) try { const adminRes = await fetch(`${API_BASE}/admin/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password, totpCode }), }) const adminJson = await adminRes.json() if (adminRes.ok && adminJson?.data?.token) { redirectAdmin(adminJson.data.token) return } setError(dict.invalidCredentials) } catch { setError(dict.unexpectedError) } finally { setLoading(false) } } return ( <> {error ? (
{error}
) : null} {step === 'credentials' ? (
setEmail(e.target.value)} placeholder="owner@company.com" className="input-field" />
setPassword(e.target.value)} placeholder="••••••••" className="input-field pr-10" />
{dict.forgotPassword}
) : (
{dict.enterCode}
setTotpCode(e.target.value.replace(/\D/g, ''))} placeholder="000000" className="input-field text-center text-xl tracking-[0.45em]" />
)} ) }