Files
smoothschedule/frontend/src/pages/LoginPage.tsx
2025-11-27 12:36:23 -05:00

254 lines
11 KiB
TypeScript

/**
* Login Page Component
* Professional login form connected to the API with visual improvements
*/
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLogin } from '../hooks/useAuth';
import { useNavigate } from 'react-router-dom';
import SmoothScheduleLogo from '../components/SmoothScheduleLogo';
import OAuthButtons from '../components/OAuthButtons';
import LanguageSelector from '../components/LanguageSelector';
import { DevQuickLogin } from '../components/DevQuickLogin';
import { AlertCircle, Loader2, User, Lock, ArrowRight } from 'lucide-react';
const LoginPage: React.FC = () => {
const { t } = useTranslation();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const loginMutation = useLogin();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
loginMutation.mutate(
{ username, password },
{
onSuccess: (data) => {
const user = data.user;
const currentHostname = window.location.hostname;
const currentPort = window.location.port;
// Check if we're on the root domain (no subdomain)
const isRootDomain = currentHostname === 'lvh.me' || currentHostname === 'localhost';
// Roles allowed to login at the root domain
const rootAllowedRoles = ['superuser', 'platform_manager', 'platform_support', 'owner'];
// If on root domain, only allow specific roles
if (isRootDomain && !rootAllowedRoles.includes(user.role)) {
setError(t('auth.loginAtSubdomain'));
return;
}
// Determine the correct subdomain based on user role
let targetSubdomain: string | null = null;
// Platform users (superuser, platform_manager, platform_support)
if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) {
targetSubdomain = 'platform';
}
// Business users - redirect to their business subdomain
else if (user.business_subdomain) {
targetSubdomain = user.business_subdomain;
}
// Check if we need to redirect to a different subdomain
// Need to redirect if we have a target subdomain AND we're not already on it
const isOnTargetSubdomain = currentHostname === `${targetSubdomain}.lvh.me`;
const needsRedirect = targetSubdomain && !isOnTargetSubdomain;
if (needsRedirect) {
// Pass tokens in URL to ensure they're available immediately on the new subdomain
// This avoids race conditions where cookies might not be set before the page loads
const portStr = currentPort ? `:${currentPort}` : '';
window.location.href = `http://${targetSubdomain}.lvh.me${portStr}/?access_token=${data.access}&refresh_token=${data.refresh}`;
return;
}
// Already on correct subdomain - navigate to dashboard
navigate('/');
},
onError: (err: any) => {
setError(err.response?.data?.error || t('auth.invalidCredentials'));
},
}
);
};
return (
<div className="min-h-screen flex bg-white dark:bg-gray-900 transition-colors duration-200">
{/* Left Side - Image & Branding (Hidden on mobile) */}
<div className="hidden lg:flex lg:w-1/2 relative bg-gray-900 text-white overflow-hidden">
<div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1497215728101-856f4ea42174?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1950&q=80')] bg-cover bg-center opacity-40"></div>
<div className="absolute inset-0 bg-gradient-to-t from-gray-900 via-transparent to-gray-900/50"></div>
<div className="relative z-10 flex flex-col justify-between w-full p-12">
<div>
<div className="flex items-center gap-3 text-white/90">
<SmoothScheduleLogo className="w-8 h-8 text-brand-500" />
<span className="font-bold text-xl tracking-tight">Smooth Schedule</span>
</div>
</div>
<div className="space-y-6 max-w-md">
<h1 className="text-4xl font-extrabold tracking-tight leading-tight">
{t('marketing.tagline')}
</h1>
<p className="text-lg text-gray-300">
{t('marketing.description')}
</p>
<div className="flex gap-2 pt-4">
<div className="h-1 w-12 bg-brand-500 rounded-full"></div>
<div className="h-1 w-4 bg-gray-600 rounded-full"></div>
<div className="h-1 w-4 bg-gray-600 rounded-full"></div>
</div>
</div>
<div className="text-sm text-gray-500">
© {new Date().getFullYear()} {t('marketing.copyright')}
</div>
</div>
</div>
{/* Right Side - Login Form */}
<div className="flex-1 flex flex-col justify-center py-12 px-4 sm:px-6 lg:px-8 lg:w-1/2 xl:px-24 bg-gray-50 dark:bg-gray-900">
<div className="mx-auto w-full max-w-sm lg:max-w-md">
<div className="text-center lg:text-left mb-10">
<div className="lg:hidden flex justify-center mb-6">
<SmoothScheduleLogo className="w-12 h-12 text-brand-600" />
</div>
<h2 className="text-3xl font-extrabold text-gray-900 dark:text-white tracking-tight">
{t('auth.welcomeBack')}
</h2>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t('auth.pleaseEnterDetails')}
</p>
</div>
{error && (
<div className="mb-6 rounded-lg bg-red-50 dark:bg-red-900/20 p-4 border border-red-100 dark:border-red-800/50 animate-in fade-in slide-in-from-top-2">
<div className="flex">
<div className="flex-shrink-0">
<AlertCircle className="h-5 w-5 text-red-500 dark:text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
{t('auth.authError')}
</h3>
<div className="mt-1 text-sm text-red-700 dark:text-red-300">
{error}
</div>
</div>
</div>
</div>
)}
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4">
{/* Username */}
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('auth.username')}
</label>
<div className="relative rounded-md shadow-sm">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
id="username"
name="username"
type="text"
autoComplete="username"
required
className="focus:ring-brand-500 focus:border-brand-500 block w-full pl-10 sm:text-sm border-gray-300 dark:border-gray-700 rounded-lg py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 transition-colors"
placeholder={t('auth.enterUsername')}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
</div>
{/* Password */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('auth.password')}
</label>
<div className="relative rounded-md shadow-sm">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="focus:ring-brand-500 focus:border-brand-500 block w-full pl-10 sm:text-sm border-gray-300 dark:border-gray-700 rounded-lg py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 transition-colors"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
</div>
<button
type="submit"
disabled={loginMutation.isPending}
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-brand-600 hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-500 disabled:opacity-70 disabled:cursor-not-allowed transition-all duration-200 ease-in-out transform active:scale-[0.98]"
>
{loginMutation.isPending ? (
<span className="flex items-center gap-2">
<Loader2 className="animate-spin h-5 w-5" />
{t('auth.signingIn')}
</span>
) : (
<span className="flex items-center gap-2">
{t('auth.signIn')}
<ArrowRight className="h-4 w-4" />
</span>
)}
</button>
</form>
{/* OAuth Divider and Buttons */}
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300 dark:border-gray-700"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400">
{t('auth.orContinueWith')}
</span>
</div>
</div>
<div className="mt-6">
<OAuthButtons
disabled={loginMutation.isPending}
/>
</div>
</div>
{/* Language Selector */}
<div className="mt-8 flex justify-center">
<LanguageSelector />
</div>
{/* Dev Quick Login */}
<DevQuickLogin embedded />
</div>
</div>
</div>
);
};
export default LoginPage;