feat: major UI overhaul with new components and enhanced UX
- Add comprehensive Company Wiki feature with complete state management - CompanyWikiManager, empty states, invite modals - Implement new Chat system with enhanced layout and components - ChatLayout, ChatSidebar, MessageThread, FileUploadInput - Create modern Login and OTP verification flows - LoginNew page, OTPVerification component - Add new Employee Forms system with enhanced controller - Introduce Figma-based design components and multiple choice inputs - Add new font assets (NeueMontreal) and robot images for onboarding - Enhance existing components with improved styling and functionality - Update build configuration and dependencies - Remove deprecated ModernLogin component
This commit is contained in:
561
pages/Login.tsx
561
pages/Login.tsx
@@ -1,266 +1,403 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
import { Button } from '../components/UiKit';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
type AuthStep = 'email' | 'otp' | 'password-fallback';
|
||||
|
||||
const ModernLogin: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { inviteCode: routeInviteCode } = useParams<{ inviteCode: string }>();
|
||||
const [email, setEmail] = useState('demo@auditly.com');
|
||||
const [password, setPassword] = useState('demo123');
|
||||
|
||||
// Auth state
|
||||
const { signInWithGoogle, signInWithEmail, signUpWithEmail, user, loading, sendOTP: authSendOTP, verifyOTP: authVerifyOTP } = useAuth();
|
||||
|
||||
// Form state
|
||||
const [step, setStep] = useState<AuthStep>('email');
|
||||
const [email, setEmail] = useState('');
|
||||
const [otp, setOtp] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [inviteCode, setInviteCode] = useState<string | null>(null);
|
||||
const { signInWithGoogle, signInWithEmail, signUpWithEmail, user, loading } = useAuth();
|
||||
const { consumeInvite, org } = useOrg();
|
||||
const [resendCooldown, setResendCooldown] = useState(0);
|
||||
const [demoOTP, setDemoOTP] = useState<string | null>(null);
|
||||
|
||||
// Extract invite code from URL
|
||||
useEffect(() => {
|
||||
// Check for invite code in route params first, then fallback to query params
|
||||
if (routeInviteCode) {
|
||||
console.log('Invite code from route params:', routeInviteCode);
|
||||
setInviteCode(routeInviteCode);
|
||||
// Clear demo credentials for invite flow
|
||||
setEmail('');
|
||||
setPassword('');
|
||||
} else {
|
||||
// Extract query params from hash-based URL
|
||||
const hashSearch = location.hash.includes('?') ? location.hash.split('?')[1] : '';
|
||||
const searchParams = new URLSearchParams(hashSearch);
|
||||
const queryInvite = searchParams.get('invite');
|
||||
if (queryInvite) {
|
||||
console.log('Invite code from query params:', queryInvite);
|
||||
setInviteCode(queryInvite);
|
||||
// Clear demo credentials for invite flow
|
||||
setEmail('');
|
||||
setPassword('');
|
||||
}
|
||||
}
|
||||
}, [routeInviteCode, location]);
|
||||
|
||||
const handleSuccessfulLogin = async () => {
|
||||
if (inviteCode) {
|
||||
// Invite flow - redirect to org selection with invite code
|
||||
navigate(`/org-selection?invite=${inviteCode}`, { replace: true });
|
||||
} else {
|
||||
// Regular login - redirect to org selection to choose/create org
|
||||
navigate('/org-selection', { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle successful authentication
|
||||
useEffect(() => {
|
||||
if (user && !loading) {
|
||||
handleSuccessfulLogin();
|
||||
}
|
||||
}, [user, loading]);
|
||||
|
||||
const handleEmailLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
if (inviteCode) {
|
||||
// For invites, try to create account first since they're new users
|
||||
console.log('Invite flow: attempting to create account for', email);
|
||||
await signUpWithEmail(email, password, email.split('@')[0]);
|
||||
navigate(`/org-selection?invite=${inviteCode}`, { replace: true });
|
||||
} else {
|
||||
// Regular login
|
||||
await signInWithEmail(email, password);
|
||||
}
|
||||
// Success will be handled by the useEffect hook
|
||||
} catch (error) {
|
||||
console.error('Auth failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
if (inviteCode) {
|
||||
// For invite flow, if account creation failed, try login instead
|
||||
if (errorMessage.includes('User already exists') || errorMessage.includes('already-exists')) {
|
||||
try {
|
||||
console.log('Account exists, trying login instead...');
|
||||
await signInWithEmail(email, password);
|
||||
} catch (loginError) {
|
||||
console.error('Login also failed:', loginError);
|
||||
setError(`Account exists but password is incorrect. Please check your password or contact your administrator.`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
setError(`Failed to create account: ${errorMessage}. Please try a different email or contact your administrator.`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
// Regular login flow - try signup if user not found
|
||||
if (errorMessage.includes('User not found')) {
|
||||
try {
|
||||
console.log('User not found, attempting sign-up...');
|
||||
await signUpWithEmail(email, password, email.split('@')[0]);
|
||||
// Success will be handled by the useEffect hook
|
||||
} catch (signUpError) {
|
||||
console.error('Sign-up also failed:', signUpError);
|
||||
setError(`Failed to create account: ${signUpError instanceof Error ? signUpError.message : 'Unknown error'}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
setError(`Login failed: ${errorMessage}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
navigate('/org-selection', { replace: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [user, loading, navigate, inviteCode]);
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
// Resend cooldown timer
|
||||
useEffect(() => {
|
||||
if (resendCooldown > 0) {
|
||||
const timer = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [resendCooldown]);
|
||||
|
||||
const sendOTP = async (emailAddress: string) => {
|
||||
try {
|
||||
await signInWithGoogle();
|
||||
// Success will be handled by the useEffect hook
|
||||
} catch (error) {
|
||||
console.error('Google login failed:', error);
|
||||
setError(`Google login failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
setDemoOTP(null);
|
||||
|
||||
// Call auth context method
|
||||
const response = await authSendOTP(emailAddress, inviteCode || undefined);
|
||||
|
||||
// If OTP is returned in response (demo mode), display it
|
||||
if (response.otp) {
|
||||
setDemoOTP(response.otp);
|
||||
}
|
||||
|
||||
setStep('otp');
|
||||
setResendCooldown(60); // 60 second cooldown
|
||||
|
||||
} catch (err) {
|
||||
console.error('OTP send error:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to send verification code. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}; const verifyOTP = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
// Call auth context method
|
||||
await authVerifyOTP(email, otp, inviteCode || undefined);
|
||||
|
||||
// Success - user will be set in auth context and useEffect will handle navigation
|
||||
|
||||
} catch (err) {
|
||||
console.error('OTP verification error:', err);
|
||||
setError(err instanceof Error ? err.message : 'Invalid verification code. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[--background-primary] py-12 px-4 sm:px-6 lg:px-8">
|
||||
<Card className="max-w-md w-full space-y-8" padding="lg">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
||||
A
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-[--text-primary]">Welcome to Auditly</h2>
|
||||
<p className="text-[--text-secondary] mt-2">
|
||||
{inviteCode ? 'Complete your profile to join the team survey' : 'Sign in to your account'}
|
||||
</p>
|
||||
{inviteCode && (
|
||||
<div className="mt-3 p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<EFBFBD> <strong>Employee Survey Invitation</strong><br />
|
||||
No account needed! Just create a password to secure your responses and start the questionnaire.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="mt-3 p-3 bg-red-100 dark:bg-red-900 rounded-lg">
|
||||
<p className="text-sm text-red-800 dark:text-red-200">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
const handleEmailSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!email.trim()) return;
|
||||
|
||||
<form onSubmit={handleEmailLogin} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Email {inviteCode && <span className="text-gray-500 dark:text-gray-400">(use your work email)</span>}
|
||||
</label>
|
||||
await sendOTP(email);
|
||||
};
|
||||
|
||||
const handleOTPSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!otp.trim()) return;
|
||||
|
||||
await verifyOTP();
|
||||
};
|
||||
|
||||
const handlePasswordFallback = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!password.trim()) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
// Try login first, then signup if user doesn't exist
|
||||
try {
|
||||
await signInWithEmail(email, password);
|
||||
} catch (loginError) {
|
||||
// If login fails, try creating account
|
||||
await signUpWithEmail(email, password, email.split('@')[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Password auth error:', err);
|
||||
setError(err instanceof Error ? err.message : 'Authentication failed');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleAuth = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
await signInWithGoogle();
|
||||
} catch (err) {
|
||||
console.error('Google auth error:', err);
|
||||
setError('Google authentication failed. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderEmailStep = () => (
|
||||
<div className="w-full max-w-md mx-auto bg-white rounded-xl shadow-lg p-8">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-white text-2xl font-bold">A</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{inviteCode ? 'Join Organization' : 'Welcome to Auditly'}
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
{inviteCode
|
||||
? 'Enter your email to join the organization'
|
||||
: 'Enter your email to get started'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleEmailSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.66675 5.83325L8.47085 10.5961C9.02182 10.9818 9.29731 11.1746 9.59697 11.2493C9.86166 11.3153 10.1385 11.3153 10.4032 11.2493C10.7029 11.1746 10.9783 10.9818 11.5293 10.5961L18.3334 5.83325M5.66675 16.6666H14.3334C15.7335 16.6666 16.4336 16.6666 16.9684 16.3941C17.4388 16.1544 17.8212 15.772 18.0609 15.3016C18.3334 14.7668 18.3334 14.0667 18.3334 12.6666V7.33325C18.3334 5.93312 18.3334 5.23306 18.0609 4.69828C17.8212 4.22787 17.4388 3.84542 16.9684 3.60574C16.4336 3.33325 15.7335 3.33325 14.3334 3.33325H5.66675C4.26662 3.33325 3.56655 3.33325 3.03177 3.60574C2.56137 3.84542 2.17892 4.22787 1.93923 4.69828C1.66675 5.23306 1.66675 5.93312 1.66675 7.33325V12.6666C1.66675 14.0667 1.66675 14.7668 1.93923 15.3016C2.17892 15.772 2.56137 16.1544 3.03177 16.3941C3.56655 16.6666 4.26662 16.6666 5.66675 16.6666Z" stroke="#718096" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"
|
||||
placeholder="Enter your email"
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-gray-50 border text-gray-700 border-gray-200 rounded-full focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Password {inviteCode && <span className="text-gray-500 dark:text-gray-400">(create a new password)</span>}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"
|
||||
required
|
||||
/>
|
||||
{inviteCode && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Choose a secure password for your new account
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Processing...' : (inviteCode ? 'Create Account & Join Team' : 'Sign In')}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">Or continue with</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={handleGoogleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Sign in with Google
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Manual invite code entry - only show if no invite code in URL */}
|
||||
{!inviteCode && (
|
||||
<div className="border-t border-[--border-color] pt-6">
|
||||
<div className="text-center mb-4">
|
||||
<h3 className="text-sm font-medium text-[--text-primary] mb-2">Employee? Use Your Invite Code</h3>
|
||||
<p className="text-xs text-[--text-secondary]">
|
||||
Skip account creation - employees can go directly to their questionnaire
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter your invite code"
|
||||
className="flex-1 px-3 py-2 border border-[--input-border] rounded-lg bg-[--input-bg] text-[--text-primary] placeholder-[--input-placeholder] focus:outline-none focus:ring-2 focus:ring-[--accent] focus:border-[--accent]"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const code = (e.target as HTMLInputElement).value.trim();
|
||||
if (code) {
|
||||
window.location.href = `#/invite/${code}`;
|
||||
} else {
|
||||
alert('Please enter an invite code');
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const input = document.querySelector('input[placeholder="Enter your invite code"]') as HTMLInputElement;
|
||||
const code = input?.value.trim();
|
||||
if (code) {
|
||||
window.location.href = `#/invite/${code}`;
|
||||
} else {
|
||||
alert('Please enter an invite code');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Start Survey
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-[--text-secondary] mt-2 text-center">
|
||||
No account needed - just answer questions and submit
|
||||
</p>
|
||||
{error && (
|
||||
<div className="text-red-600 text-sm bg-red-50 border border-red-200 p-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-[--text-secondary]">
|
||||
{inviteCode ?
|
||||
'Demo mode: Enter any email and password to create your account.' :
|
||||
'Demo mode: No Firebase configuration detected.\nUse any email/password to continue.'
|
||||
}
|
||||
</p>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white py-3.5 rounded-full font-medium transition-all transform hover:scale-[1.02]"
|
||||
disabled={isLoading || !email.trim()}
|
||||
>
|
||||
{isLoading ? 'Sending...' : 'Continue with Email'}
|
||||
</Button>
|
||||
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-200" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-3 bg-white text-gray-500">or</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="w-full border-gray-200 py-3.5 rounded-full transition-colors"
|
||||
onClick={handleGoogleAuth}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
|
||||
<div className="text-center mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep('password-fallback')}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
Use password instead
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderOTPStep = () => (
|
||||
<div className="w-full max-w-md mx-auto bg-white rounded-xl shadow-lg p-8">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Check your email</h1>
|
||||
<p className="text-gray-600">
|
||||
We sent a verification code to <br />
|
||||
<strong className="text-gray-900">{email}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleOTPSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2 text-center">
|
||||
Verification Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={otp}
|
||||
onChange={(e) => setOtp(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
placeholder="000000"
|
||||
className="w-full px-4 py-4 bg-gray-50 border border-gray-200 text-gray-700 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-center text-3xl tracking-[0.5em] font-mono outline-none transition-all"
|
||||
maxLength={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{demoOTP && (
|
||||
<div className="bg-gradient-to-r from-yellow-50 to-orange-50 border border-yellow-200 rounded-xl p-4">
|
||||
<div className="text-yellow-800 text-sm text-center">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<strong>Demo Mode</strong>
|
||||
</div>
|
||||
Your verification code is <strong className="text-2xl font-mono bg-yellow-100 px-2 py-1 rounded">{demoOTP}</strong>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-red-600 text-sm bg-red-50 border border-red-200 p-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white py-4 rounded-xl font-medium transition-all transform hover:scale-[1.02]"
|
||||
disabled={isLoading || otp.length !== 6}
|
||||
>
|
||||
{isLoading ? 'Verifying...' : 'Verify Code'}
|
||||
</Button>
|
||||
|
||||
<div className="text-center space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => sendOTP(email)}
|
||||
disabled={resendCooldown > 0 || isLoading}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 disabled:text-gray-400 transition-colors"
|
||||
>
|
||||
{resendCooldown > 0
|
||||
? `Resend code in ${resendCooldown}s`
|
||||
: 'Resend code'
|
||||
}
|
||||
</button>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setStep('email'); setError(''); setOtp(''); }}
|
||||
className="text-sm text-gray-600 hover:text-gray-800 transition-colors"
|
||||
>
|
||||
← Change email address
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderPasswordStep = () => (
|
||||
<div className="w-full max-w-md mx-auto bg-white rounded-xl shadow-lg p-8">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Sign in with password</h1>
|
||||
<p className="text-gray-600">
|
||||
Enter your password for <br />
|
||||
<strong className="text-gray-900">{email}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handlePasswordFallback} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-full focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-600 text-sm bg-red-50 border border-red-200 p-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white py-3.5 rounded-full font-medium transition-all transform hover:scale-[1.02]"
|
||||
disabled={isLoading || !password.trim()}
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setStep('email'); setError(''); setPassword(''); }}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
← Back to email verification
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 flex items-center justify-center p-4">
|
||||
{step === 'email' && renderEmailStep()}
|
||||
{step === 'otp' && renderOTPStep()}
|
||||
{step === 'password-fallback' && renderPasswordStep()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
export default ModernLogin;
|
||||
|
||||
Reference in New Issue
Block a user