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:
Ra
2025-08-20 03:30:04 -07:00
parent 1a9e92d7bd
commit cf565df13e
47 changed files with 6654 additions and 2007 deletions

View File

@@ -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;