Files
auditly/pages/Login.tsx
Ra cf565df13e 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
2025-08-20 04:06:49 -07:00

404 lines
19 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { Button } from '../components/UiKit';
type AuthStep = 'email' | 'otp' | 'password-fallback';
const ModernLogin: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { inviteCode: routeInviteCode } = useParams<{ inviteCode: string }>();
// 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 [resendCooldown, setResendCooldown] = useState(0);
const [demoOTP, setDemoOTP] = useState<string | null>(null);
// Extract invite code from URL
useEffect(() => {
if (routeInviteCode) {
setInviteCode(routeInviteCode);
} else {
const hashSearch = location.hash.includes('?') ? location.hash.split('?')[1] : '';
const searchParams = new URLSearchParams(hashSearch);
const queryInvite = searchParams.get('invite');
if (queryInvite) {
setInviteCode(queryInvite);
}
}
}, [routeInviteCode, location]);
// Handle successful authentication
useEffect(() => {
if (user && !loading) {
if (inviteCode) {
navigate(`/org-selection?invite=${inviteCode}`, { replace: true });
} else {
navigate('/org-selection', { replace: true });
}
}
}, [user, loading, navigate, inviteCode]);
// Resend cooldown timer
useEffect(() => {
if (resendCooldown > 0) {
const timer = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000);
return () => clearTimeout(timer);
}
}, [resendCooldown]);
const sendOTP = async (emailAddress: string) => {
try {
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);
}
};
const handleEmailSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email.trim()) return;
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
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
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>
{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 || !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>
<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 ModernLogin;