272 lines
11 KiB
TypeScript
272 lines
11 KiB
TypeScript
import AuthLayout from "@/components/layout/AuthLayout";
|
|
import { motion } from 'framer-motion';
|
|
|
|
import { useState } from 'react'
|
|
import { loginInterface } from '@/types/userInterface'
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
import { sageService } from "@/service/sageService";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { Button } from '@/components/ui/buttonTsx';
|
|
import AuthInput from '@/components/ui/AuthInput';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { ArrowRight, Building2, Eye, EyeOff, Loader2, Lock, Mail, ShieldCheck } from "lucide-react";
|
|
import { useAppDispatch } from "@/store/hooks";
|
|
import { authMe } from "@/store/features/user/thunk";
|
|
|
|
export default function Login() {
|
|
|
|
const dispatch = useAppDispatch();
|
|
const navigate = useNavigate();
|
|
const { toast } = useToast();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
const [loginData, setLoginData] = useState<loginInterface>({ email: "", password: "", rememberMe: false });
|
|
|
|
const [errors, setErrors] = useState({}) as any;
|
|
const [errorFom, setErrorForm] = useState('')
|
|
|
|
const validateForm = () => {
|
|
const newErrors = {} as any;
|
|
if (!loginData.email) {
|
|
newErrors.email = "L'adresse email est requise";
|
|
} else if (!/\S+@\S+\.\S+/.test(loginData.email)) {
|
|
newErrors.email = "Format d'email invalide";
|
|
}
|
|
|
|
if (!loginData.password) {
|
|
newErrors.password = "Le mot de passe est requis";
|
|
} else if (loginData.password.length < 8) {
|
|
newErrors.password = "Le mot de passe doit contenir au moins 8 caractères";
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleLogin = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!validateForm()) return;
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await sageService.login(loginData);
|
|
|
|
const access_token = response.access_token;
|
|
const refresh_token = response.refresh_token;
|
|
const expires_in = response.expires_in;
|
|
|
|
if (access_token) {
|
|
document.cookie = `access_token=${access_token}; path=/; max-age=${expires_in}`;
|
|
document.cookie = `refresh_token=${refresh_token}; path=/; max-age=${expires_in * 2}`;
|
|
|
|
try {
|
|
await dispatch(authMe()).unwrap();
|
|
navigate("/home");
|
|
} catch (error) {
|
|
setErrorForm("Impossible de vérifier votre accès. Veuillez réessayer.");
|
|
}
|
|
} else {
|
|
setErrors("Email ou mot de passe incorrect")
|
|
setErrorForm("Email ou mot de passe incorrect")
|
|
}
|
|
|
|
setIsLoading(false);
|
|
|
|
} catch (err: any) {
|
|
setErrors(`${err.response.data.detail || "Email ou mot de passe incorrect!"}`);
|
|
setErrorForm(`${err.response.data.detail || "Email ou mot de passe incorrect!"}`);
|
|
setIsLoading(false);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSSOLogin = (provider : any) => {
|
|
toast({
|
|
title: "Redirection SSO",
|
|
description: `Connexion avec ${provider} en cours...`,
|
|
});
|
|
// Implementation would go here
|
|
};
|
|
|
|
// Animation variants
|
|
const containerVariants = {
|
|
hidden: { opacity: 0 },
|
|
visible: {
|
|
opacity: 1,
|
|
transition: {
|
|
staggerChildren: 0.1,
|
|
delayChildren: 0.2
|
|
}
|
|
}
|
|
};
|
|
|
|
const itemVariants = {
|
|
hidden: { opacity: 0, y: 20 },
|
|
visible: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 300, damping: 24 } }
|
|
};
|
|
|
|
|
|
return (
|
|
<AuthLayout>
|
|
<motion.div
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
animate="visible"
|
|
className="w-full max-w-[420px]"
|
|
>
|
|
{/* Mobile Logo */}
|
|
<div className="lg:hidden flex justify-center mb-8">
|
|
<div className="w-12 h-12 rounded-xl bg-[#338660] flex items-center justify-center shadow-lg shadow-[#338660]/30">
|
|
<Building2 className="w-7 h-7 text-white" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<motion.div variants={itemVariants} className="text-center mb-10">
|
|
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">Bon retour</h2>
|
|
<p className="text-gray-500 dark:text-gray-400">
|
|
Connectez-vous pour accéder à votre espace
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* SSO Buttons */}
|
|
<motion.div variants={itemVariants} className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
|
<button
|
|
onClick={() => handleSSOLogin('Microsoft')}
|
|
className="flex items-center justify-center gap-3 px-4 py-3 bg-[#2F2F2F] hover:bg-[#1a1a1a] text-white rounded-xl font-medium transition-all hover:shadow-lg hover:-translate-y-0.5 duration-200 border border-transparent"
|
|
>
|
|
<svg className="w-5 h-5" viewBox="0 0 23 23">
|
|
<path fill="#f35325" d="M1 1h10v10H1z"/>
|
|
<path fill="#81bc06" d="M12 1h10v10H12z"/>
|
|
<path fill="#05a6f0" d="M1 12h10v10H1z"/>
|
|
<path fill="#ffba08" d="M12 12h10v10H12z"/>
|
|
</svg>
|
|
<span>Microsoft</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleSSOLogin('Google')}
|
|
className="flex items-center justify-center gap-3 px-4 py-3 bg-white hover:bg-gray-50 text-gray-700 rounded-xl font-medium transition-all hover:shadow-lg hover:-translate-y-0.5 duration-200 border border-gray-200"
|
|
>
|
|
<svg className="w-5 h-5" 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.26.81-.58z"/>
|
|
<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>
|
|
<span>Google</span>
|
|
</button>
|
|
</motion.div>
|
|
|
|
<motion.div variants={itemVariants} className="relative mb-8">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<span className="w-full border-t border-gray-200 dark:border-gray-800" />
|
|
</div>
|
|
<div className="relative flex justify-center text-sm">
|
|
<span className="px-4 bg-white dark:bg-gray-950 text-gray-500">ou avec email</span>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Login Form */}
|
|
{errorFom && (
|
|
<div className="p-3 text-sm text-red-600 bg-red-50 rounded-lg">
|
|
{errorFom}
|
|
</div>
|
|
)}
|
|
<motion.form variants={itemVariants} onSubmit={handleLogin} className="space-y-6">
|
|
<AuthInput
|
|
icon={Mail}
|
|
type="email"
|
|
label="Adresse email"
|
|
placeholder="nom@entreprise.com"
|
|
value={loginData.email}
|
|
onChange={(e) => setLoginData({...loginData, email: e.target.value})}
|
|
error={errors.email}
|
|
/>
|
|
|
|
<AuthInput
|
|
icon={Lock}
|
|
type={showPassword ? "text" : "password"}
|
|
label="Mot de passe"
|
|
placeholder="••••••••"
|
|
value={loginData.password}
|
|
onChange={(e) => setLoginData({...loginData, password: e.target.value})}
|
|
error={errors.password}
|
|
rightElement={
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="text-gray-400 hover:text-gray-600 focus:outline-none"
|
|
>
|
|
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
|
</button>
|
|
}
|
|
/>
|
|
|
|
<div className="flex items-center justify-between pt-2">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="remember"
|
|
checked={loginData.rememberMe}
|
|
onCheckedChange={(checked) =>
|
|
setLoginData({
|
|
...loginData,
|
|
rememberMe: checked === true
|
|
})
|
|
}
|
|
/>
|
|
<label
|
|
htmlFor="remember"
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-gray-600 dark:text-gray-400 cursor-pointer"
|
|
>
|
|
Se souvenir de moi
|
|
</label>
|
|
</div>
|
|
<Link
|
|
to="/forgot"
|
|
className="text-sm font-medium text-[#338660] hover:text-[#2A6F4F] hover:underline"
|
|
>
|
|
Mot de passe oublié ?
|
|
</Link>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className="w-full h-12 text-base font-bold bg-[#338660] hover:bg-[#2A6F4F] transition-all duration-300 shadow-[0_4px_14px_0_rgba(51,134,96,0.39)] hover:shadow-[0_6px_20px_rgba(51,134,96,0.23)] hover:-translate-y-0.5 rounded-xl"
|
|
>
|
|
{isLoading ? (
|
|
<span className="flex items-center gap-2">
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
Connexion en cours...
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-2">
|
|
Se connecter
|
|
<ArrowRight className="w-5 h-5" />
|
|
</span>
|
|
)}
|
|
</Button>
|
|
</motion.form>
|
|
|
|
{/* Footer Trust Elements */}
|
|
<motion.div variants={itemVariants} className="mt-12 text-center space-y-4">
|
|
<div className="flex items-center justify-center gap-2 text-xs text-gray-500 bg-gray-50 dark:bg-gray-900/50 py-2 px-4 rounded-full mx-auto">
|
|
<ShieldCheck className="w-3.5 h-3.5 text-[#338660]" />
|
|
<span>Connexion chiffrée SSL • Données sécurisées</span>
|
|
</div>
|
|
|
|
<p className="text-xs text-gray-400">
|
|
En vous connectant, vous acceptez nos{' '}
|
|
<Link to="/" className="text-gray-500 hover:text-[#338660] underline">Conditions</Link>
|
|
{' '}et{' '}
|
|
<Link to="/privacy" className="text-gray-500 hover:text-[#338660] underline">Politique de confidentialité</Link>.
|
|
</p>
|
|
</motion.div>
|
|
</motion.div>
|
|
</AuthLayout>
|
|
)
|
|
}
|