Récupère la FAQ de la page d'accueil et la refonte de la page Contact, ainsi que la chaîne CI/CD et l'infrastructure VPS HTTPS venues de main. Résolution des 13 conflits : - Footer : les deux corrections sont conservées. FooterLink sort du composant (Alexis), la grille passe en une colonne sous 640 px et l'adresse email peut se couper (audit responsive). - Admin : le BOM du CSV adopte la séquence échappée d'Alexis, plus lisible que le caractère invisible et sans règle de lint à désactiver. - Contact : version d'Alexis retenue, plus complète, avec son formulaire. - Connexion, Inscription : logos SVG conservés dans leur version nettoyée, avec aria-hidden ; les gestionnaires OAuth et les champs d'inscription restent en place. - UserNav, Profile, services/admin : versions locales conservées. - docker-compose : structure Traefik et domaines HTTPS d'Alexis, sur lesquels sont regreffés FRONTEND_URL et les identifiants OAuth par substitution. - Les deux package-lock régénérés depuis les package.json fusionnés. Aligne au passage les derniers chemins d'API restés relatifs sur VITE_API_URL, sans quoi le bouton Google et la configuration du jeu visaient le domaine du front en déploiement. Fiabilise trois tests de tirage au sort : la table GrandPrizeWinner est unique pour tout le jeu et le vivier contient de vrais comptes, donc les suites mettent désormais un éventuel tirage réel de côté et n'attendent plus de connaître le gagnant à l'avance.
167 lines
7.1 KiB
JavaScript
167 lines
7.1 KiB
JavaScript
import { useState } from 'react'
|
|
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
|
import { loginApi } from '../services/auth'
|
|
import { useAuth } from '../context/AuthContext'
|
|
|
|
export default function Connexion() {
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
const { login } = useAuth()
|
|
const justRegistered = location.state?.registered === true
|
|
const [form, setForm] = useState({ email: '', password: '' })
|
|
const [showPassword, setShowPassword] = useState(false)
|
|
const [error, setError] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const handleChange = (e) => setForm({ ...form, [e.target.name]: e.target.value })
|
|
|
|
// Navigation pleine page (et non fetch) : le flux OAuth passe par des
|
|
// redirections vers Google puis retour sur /auth/callback.
|
|
const handleGoogleLogin = () => {
|
|
const apiUrl = import.meta.env.VITE_API_URL || ''
|
|
window.location.href = `${apiUrl}/api/auth/google`
|
|
}
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
setLoading(true)
|
|
try {
|
|
const { token, user } = await loginApi(form)
|
|
login(user, token)
|
|
if (user.role === 'ADMIN') navigate('/admin')
|
|
else if (user.role === 'EMPLOYEE') navigate('/employe')
|
|
else navigate('/mes-gains')
|
|
} catch (err) {
|
|
setError(err.message)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white flex flex-col justify-center px-8 py-12 max-w-sm mx-auto">
|
|
|
|
<div className="mb-8">
|
|
<h1 className="text-4xl font-bold italic font-serif text-[#006B6B]">Se connecter</h1>
|
|
<p className="text-gray-500 mt-1 text-sm">Connexion à votre espace</p>
|
|
</div>
|
|
|
|
{justRegistered && (
|
|
<div className="mb-4 px-4 py-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm">
|
|
Compte créé avec succès ! Connectez-vous.
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="mb-4 px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-red-600 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-gray-800 mb-1">
|
|
Adresse email <span className="text-gray-800">*</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
name="email"
|
|
value={form.email}
|
|
onChange={handleChange}
|
|
required
|
|
placeholder="Votre email"
|
|
className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="password" className="block text-sm font-medium text-gray-800 mb-1">
|
|
Mot de passe <span className="text-gray-800">*</span>
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showPassword ? 'text' : 'password'}
|
|
id="password"
|
|
name="password"
|
|
value={form.password}
|
|
onChange={handleChange}
|
|
required
|
|
placeholder="Votre mot de passe"
|
|
className="w-full border border-gray-200 rounded-lg px-4 py-3 pr-12 text-sm text-gray-700 placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-600"
|
|
aria-label="Afficher/masquer le mot de passe"
|
|
>
|
|
<div className="w-6 h-6 rounded-full border-2 border-gray-400 flex items-center justify-center">
|
|
{showPassword && (
|
|
<div className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
<div className="text-right mt-1">
|
|
<button type="button" className="text-xs text-gray-500 hover:text-[#006B6B] hover:underline">
|
|
Mot de passe oublié ?
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full bg-[#006B6B] hover:bg-[#005959] disabled:opacity-60 text-white font-medium py-3 rounded-lg transition-colors"
|
|
>
|
|
{loading ? 'Connexion...' : 'Se connecter'}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="flex items-center gap-3 my-6">
|
|
<div className="flex-1 h-px bg-gray-200" />
|
|
<span className="text-sm text-gray-500">Ou</span>
|
|
<div className="flex-1 h-px bg-gray-200" />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={handleGoogleLogin}
|
|
className="flex items-center justify-center gap-2 border border-gray-200 rounded-lg py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 48 48" aria-hidden="true">
|
|
<path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"></path>
|
|
<path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"></path>
|
|
<path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"></path>
|
|
<path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"></path>
|
|
</svg>
|
|
Google
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled
|
|
title="Connexion Facebook bientôt disponible"
|
|
className="flex items-center justify-center gap-2 border border-gray-200 rounded-lg py-3 text-sm text-gray-500 cursor-not-allowed opacity-60"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" aria-hidden="true">
|
|
<circle cx="12" cy="12" r="12" fill="#1877F2" />
|
|
<path fill="#FFFFFF" d="M13.5 21v-8h2.7l.4-3h-3.1V8.1c0-.9.3-1.6 1.7-1.6h1.8V3.8c-.3 0-1.4-.1-2.6-.1-2.6 0-4.4 1.6-4.4 4.5V10H7.2v3H10v8h3.5z" />
|
|
</svg>
|
|
Facebook
|
|
</button>
|
|
</div>
|
|
|
|
<p className="text-center text-sm text-gray-500 mt-8">
|
|
Pas encore de compte ?{' '}
|
|
<Link to="/inscription" className="text-gray-700 font-medium hover:underline">
|
|
S'inscrire
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|