perfect-postcode/frontend/src/components/ui/AuthModal.tsx

247 lines
9 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CloseIcon } from './icons/CloseIcon';
import { GoogleIcon } from './icons/GoogleIcon';
import { trackEvent } from '../../lib/analytics';
type View = 'login' | 'register' | 'forgot';
export default function AuthModal({
onClose,
onLogin,
onRegister,
onOAuthLogin,
onForgotPassword,
loading,
error,
onClearError,
initialTab = 'login',
}: {
onClose: () => void;
onLogin: (email: string, password: string) => Promise<void>;
onRegister: (email: string, password: string) => Promise<void>;
onOAuthLogin: (provider: string) => Promise<void>;
onForgotPassword: (email: string) => Promise<void>;
loading: boolean;
error: string | null;
onClearError: () => void;
initialTab?: 'login' | 'register';
}) {
const { t } = useTranslation();
const [view, setView] = useState<View>(initialTab);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [resetSent, setResetSent] = useState(false);
useEffect(() => {
trackEvent('Auth Modal Open', { tab: initialTab });
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const switchView = useCallback(
(newView: View) => {
setView(newView);
setResetSent(false);
onClearError();
},
[onClearError]
);
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
try {
if (view === 'login') {
await onLogin(email, password);
onClose();
} else if (view === 'register') {
await onRegister(email, password);
onClose();
} else {
await onForgotPassword(email);
setResetSent(true);
}
} catch {
// Error is handled by the hook
}
},
[view, email, password, onLogin, onRegister, onForgotPassword, onClose]
);
const handleOAuth = useCallback(
async (provider: string) => {
try {
await onOAuthLogin(provider);
onClose();
} catch {
// Error is handled by the hook
}
},
[onOAuthLogin, onClose]
);
const title =
view === 'login'
? t('auth.logIn')
: view === 'register'
? t('auth.createAccount')
: t('auth.resetPassword');
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
onMouseDown={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div className="absolute inset-0 bg-black/50 dark:bg-black/70" />
<div className="relative w-full max-w-sm mx-4 bg-white dark:bg-warm-900 rounded-lg shadow-xl border border-warm-200 dark:border-warm-700">
{/* Header */}
<div className="flex items-center justify-between px-5 pt-5 pb-3">
<h2 className="text-lg font-semibold text-navy-950 dark:text-white">{title}</h2>
<button
onClick={onClose}
className="text-warm-400 hover:text-warm-700 dark:text-warm-400 dark:hover:text-warm-200"
>
<CloseIcon className="w-5 h-5" />
</button>
</div>
{/* Tabs (hidden in forgot view) */}
{view !== 'forgot' && (
<div className="flex px-5 gap-4 border-b border-warm-200 dark:border-warm-700">
<button
className={`pb-2 text-sm font-medium border-b-2 transition-colors ${
view === 'login'
? 'border-teal-600 text-teal-600 dark:text-teal-400 dark:border-teal-400'
: 'border-transparent text-warm-500 dark:text-warm-400 hover:text-warm-700 dark:hover:text-warm-300'
}`}
onClick={() => switchView('login')}
>
{t('auth.logIn')}
</button>
<button
className={`pb-2 text-sm font-medium border-b-2 transition-colors ${
view === 'register'
? 'border-teal-600 text-teal-600 dark:text-teal-400 dark:border-teal-400'
: 'border-transparent text-warm-500 dark:text-warm-400 hover:text-warm-700 dark:hover:text-warm-300'
}`}
onClick={() => switchView('register')}
>
{t('auth.createAccount')}
</button>
</div>
)}
<div className="p-5 space-y-4">
{/* Value prop */}
{view !== 'forgot' && (
<p className="text-xs text-warm-500 dark:text-warm-400 text-center">
{t('auth.valueProp')}
</p>
)}
{/* OAuth buttons (hidden in forgot view) */}
{view !== 'forgot' && (
<>
<div className="space-y-2">
<button
type="button"
onClick={() => handleOAuth('google')}
disabled={loading}
className="w-full flex items-center justify-center gap-2 py-2 px-4 rounded border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 text-navy-950 dark:text-warm-100 text-sm font-medium hover:bg-warm-50 dark:hover:bg-warm-700 disabled:opacity-50 disabled:cursor-wait"
>
<GoogleIcon className="w-4 h-4" />
{t('auth.continueWithGoogle')}
</button>
</div>
{/* Divider */}
<div className="flex items-center gap-3">
<div className="flex-1 h-px bg-warm-200 dark:bg-warm-700" />
<span className="text-xs text-warm-400 dark:text-warm-500">{t('common.or')}</span>
<div className="flex-1 h-px bg-warm-200 dark:bg-warm-700" />
</div>
</>
)}
{/* Email form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-warm-700 dark:text-warm-300 mb-1">
{t('auth.email')}
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-3 py-2 text-sm rounded border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 text-navy-950 dark:text-white placeholder-warm-400 dark:placeholder-warm-500 outline-none focus:ring-2 ring-teal-400 dark:ring-teal-500"
placeholder={t('auth.emailPlaceholder')}
/>
</div>
{view !== 'forgot' && (
<div>
<label className="block text-sm font-medium text-warm-700 dark:text-warm-300 mb-1">
{t('auth.password')}
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
className="w-full px-3 py-2 text-sm rounded border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 text-navy-950 dark:text-white placeholder-warm-400 dark:placeholder-warm-500 outline-none focus:ring-2 ring-teal-400 dark:ring-teal-500"
placeholder={view === 'register' ? t('auth.passwordPlaceholderRegister') : t('auth.passwordPlaceholderLogin')}
/>
{view === 'login' && (
<button
type="button"
onClick={() => switchView('forgot')}
className="mt-1 text-xs text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300"
>
{t('auth.forgotPassword')}
</button>
)}
</div>
)}
{view === 'forgot' && resetSent && (
<p className="text-sm text-teal-700 dark:text-teal-400">
{t('auth.resetSent')}
</p>
)}
{error && <p className="text-sm text-red-600 dark:text-red-300">{error}</p>}
{!(view === 'forgot' && resetSent) && (
<button
type="submit"
disabled={loading}
className="w-full py-2 rounded bg-teal-600 text-white text-sm font-medium hover:bg-teal-700 dark:hover:bg-teal-600 disabled:opacity-50 disabled:cursor-wait transition-colors"
>
{loading
? t('auth.pleaseWait')
: view === 'login'
? t('auth.logIn')
: view === 'register'
? t('auth.createAccount')
: t('auth.sendResetLink')}
</button>
)}
{view === 'forgot' && (
<button
type="button"
onClick={() => switchView('login')}
className="w-full text-center text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300"
>
{t('auth.backToLogin')}
</button>
)}
</form>
</div>
</div>
</div>
);
}