groupe-meditation/frontend/src/pages/Login.jsx

238 lines
9.8 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { useStore } from '../stores/store';
import { api } from '../stores/api';
const jourLabel = (jour) => {
if (!jour) return 'Jour à préciser';
return jour.charAt(0).toUpperCase() + jour.slice(1);
};
export default function Login() {
const { login, loading, error, groupes, loadGroupes, setPage } = useStore();
const [courriel, setCourriel] = useState(localStorage.getItem('gm_courriel') || '');
const [pin, setPin] = useState('');
const [groupeId, setGroupeId] = useState(localStorage.getItem('gm_groupe_id') || '');
const [groupeOuvert, setGroupeOuvert] = useState('');
const [seSouvenir, setSeSouvenir] = useState(!!localStorage.getItem('gm_courriel'));
const [sysadminOuvert, setSysadminOuvert] = useState(false);
const [sysadminPin, setSysadminPin] = useState('');
useEffect(() => {
if (groupes.length === 0) loadGroupes();
}, []);
useEffect(() => {
if (groupes.length === 0) return;
const groupeExiste = groupes.some((g) => g.id === groupeId);
if (groupeId && groupeExiste) return;
const prochainGroupeId = groupes.length === 1 ? groupes[0].id : '';
setGroupeId(prochainGroupeId);
if (prochainGroupeId) {
localStorage.setItem('gm_groupe_id', prochainGroupeId);
} else {
localStorage.removeItem('gm_groupe_id');
}
}, [groupes, groupeId]);
const ouvrirGroupe = (id) => {
const prochain = groupeOuvert === id ? '' : id;
setGroupeOuvert(prochain);
setGroupeId(id);
localStorage.setItem('gm_groupe_id', id);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!courriel || !pin || !groupeId) return;
try {
await login(courriel, pin, groupeId, seSouvenir);
} catch {}
};
const ouvrirSysadmin = async (e) => {
e.preventDefault();
if (sysadminPin.length !== 4) return;
try {
const data = await api.post('/admin/sysadmin-login', { pin: sysadminPin });
api.setToken(data.access_token, false);
localStorage.setItem('gm_sysadmin_instance', '1');
localStorage.removeItem('gm_groupe_id');
useStore.setState({
token: data.access_token,
membre: data.membre,
groupeId: null,
groupe: null,
postes: ['Sysadmin'],
estExecutif: true,
estSysadmin: true,
modulesConsultation: [],
modulesSaisie: [],
modulesGestion: [],
sysadminInstance: true,
error: null,
});
setPage('G14');
} catch (e) {
useStore.setState({ error: e.message });
} finally {
setSysadminPin('');
}
};
return (
<div className="min-h-screen bg-gradient-to-b from-aa-blue to-blue-900 p-4">
<div className="mx-auto flex min-h-screen w-full max-w-md flex-col py-6">
<div className="text-center mb-5">
<h1 className="text-2xl font-bold text-white">Choisir un groupe</h1>
<p className="text-blue-200 text-sm mt-1">District 87-16</p>
</div>
<div className="flex-1 space-y-3 pb-6">
{groupes.length === 0 ? (
<div className="rounded-2xl bg-white/10 p-5 text-center text-blue-100">
Chargement des groupes...
</div>
) : groupes.map((groupe) => {
const ouvert = groupeOuvert === groupe.id;
const horaire = `${jourLabel(groupe.jour_reunion)}${groupe.heure_reunion ? ` · ${groupe.heure_reunion}` : ''}`;
return (
<section key={groupe.id} className="overflow-hidden rounded-2xl bg-white shadow-xl">
<button
type="button"
onClick={() => ouvrirGroupe(groupe.id)}
className="flex w-full items-center justify-between gap-3 p-4 text-left active:bg-blue-50"
aria-expanded={ouvert}
>
<span className="min-w-0">
<span className="block truncate text-lg font-bold text-gray-900">{groupe.nom}</span>
<span className="mt-1 block text-sm font-semibold text-aa-blue">{horaire}</span>
</span>
<span className={`shrink-0 text-2xl text-aa-blue transition ${ouvert ? 'rotate-180' : ''}`}>⌄</span>
</button>
{ouvert && (
<div className="border-t border-gray-100 p-4 pt-3">
<div className="mb-4 space-y-2 rounded-xl bg-gray-50 p-3 text-sm text-gray-700">
{groupe.adresse_local && (
<p><span className="font-semibold text-gray-900">Lieu : </span>{groupe.adresse_local}</p>
)}
{groupe.format_reunion && (
<p><span className="font-semibold text-gray-900">Format : </span>{groupe.format_reunion}</p>
)}
{groupe.district && (
<p><span className="font-semibold text-gray-900">District : </span>{groupe.district}</p>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
placeholder="Adresse courriel ou identifiant"
value={courriel}
onChange={(e) => setCourriel(e.target.value)}
className="w-full rounded-xl border-0 bg-gray-50 p-4 text-lg focus:ring-2 focus:ring-aa-blue"
autoComplete="username"
inputMode="email"
/>
<input
type="password"
inputMode="numeric"
pattern="[0-9]*"
maxLength={4}
placeholder="PIN"
value={pin}
onChange={(e) => setPin(e.target.value.replace(/\D/g, '').slice(0, 4))}
className="w-full rounded-xl border-0 bg-gray-50 p-4 text-center text-lg tracking-[0.8em] focus:ring-2 focus:ring-aa-blue"
/>
<label className="flex cursor-pointer items-center gap-2 text-sm text-gray-500">
<input
type="checkbox"
checked={seSouvenir}
onChange={(e) => setSeSouvenir(e.target.checked)}
className="rounded border-gray-300 text-aa-blue focus:ring-aa-blue"
/>
Se souvenir de moi
</label>
{error && (
<p className="rounded-lg bg-red-50 p-3 text-center text-sm text-red-500">{error}</p>
)}
<button
type="submit"
disabled={loading || !courriel || pin.length !== 4 || !groupeId}
className="w-full rounded-xl bg-aa-blue p-4 text-lg font-bold text-white transition active:scale-[0.97] disabled:opacity-40"
>
{loading ? '...' : 'Connexion'}
</button>
<button
type="button"
onClick={() => setPage('inscription')}
className="w-full p-3 text-sm font-medium text-aa-blue"
>
J'ai un code d'invitation →
</button>
</form>
</div>
)}
</section>
);
})}
</div>
<a
href="/presentation.html"
className="mb-4 block w-full rounded-xl bg-white/95 p-4 text-center text-sm font-bold text-aa-blue shadow-lg active:scale-[0.98]"
>
Découvrir l'application
</a>
{sysadminOuvert && (
<form onSubmit={ouvrirSysadmin} className="mb-3 rounded-2xl bg-white/95 p-3 shadow-lg">
<input
type="password"
inputMode="numeric"
pattern="[0-9]*"
maxLength={4}
placeholder="PIN sysadmin"
value={sysadminPin}
onChange={(e) => setSysadminPin(e.target.value.replace(/\D/g, '').slice(0, 4))}
className="mb-2 w-full rounded-xl border-0 bg-gray-50 p-3 text-center text-lg tracking-[0.8em] focus:ring-2 focus:ring-aa-blue"
autoFocus
/>
<div className="grid grid-cols-2 gap-2">
<button type="submit" disabled={sysadminPin.length !== 4} className="rounded-xl bg-aa-blue p-3 text-sm font-bold text-white disabled:opacity-40">
Ouvrir
</button>
<button type="button" onClick={() => { setSysadminOuvert(false); setSysadminPin(''); }} className="rounded-xl bg-gray-100 p-3 text-sm font-bold text-gray-600">
Annuler
</button>
</div>
</form>
)}
{!sysadminOuvert && (
<button
type="button"
onClick={() => setSysadminOuvert(true)}
className="mx-auto mb-3 block px-3 py-2 text-xs font-semibold text-blue-200/80"
>
Sysadmin
</button>
)}
<div className="mb-3 flex items-center justify-center gap-4 text-xs">
<button type="button" onClick={() => setPage('charte')} className="font-medium text-blue-200/75 underline-offset-4 active:text-white">
Charte d'utilisation
</button>
<button type="button" onClick={() => setPage('confidentialite')} className="font-medium text-blue-200/75 underline-offset-4 active:text-white">
Confidentialité
</button>
</div>
<p className="text-center text-xs text-blue-300">100% logiciel libre · Hébergé au Québec</p>
</div>
</div>
);
}