267 lines
13 KiB
TypeScript
267 lines
13 KiB
TypeScript
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';
|
||
|
||
const Login: 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');
|
||
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();
|
||
|
||
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 });
|
||
}
|
||
};
|
||
|
||
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]);
|
||
} 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);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleGoogleLogin = async () => {
|
||
setIsLoading(true);
|
||
setError('');
|
||
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(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>
|
||
|
||
<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>
|
||
<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"
|
||
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>
|
||
</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>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Login;
|