UX améliorations

This commit is contained in:
2026-07-03 19:02:44 +02:00
parent b481c4cc1d
commit e11c767c1c
3 changed files with 110 additions and 91 deletions
+4 -1
View File
@@ -71,9 +71,12 @@ const iconsDir2 = path.join(_dataDir, 'icons');
app.use('/api/icons-files', express.static(iconsDir2, { maxAge: '1h', fallthrough: false }));
// Basic rate-limit on auth endpoints
// Seuil relevé (50 → 300) : usage local mono-utilisateur/multi-investisseur avec
// beaucoup d'allers-retours de test (login, 2FA, création de comptes admin/invitation…),
// l'ancien seuil de 50 req/15min était atteint trop facilement en usage normal.
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 50,
max: 300,
standardHeaders: true,
legacyHeaders: false,
});
+95 -90
View File
@@ -43,7 +43,7 @@ function ProfileSelect({ label, options, value, onChange }) {
return (
<div ref={ref} className="profile-field">
<span className="profile-label">{label}</span>
<label>{label}</label>
<div className={`profile-select-trigger${open ? ' open' : ''}`}
onClick={() => setOpen(o => !o)} role="button" tabIndex={0}
onKeyDown={e => e.key === 'Enter' && setOpen(o => !o)}>
@@ -132,25 +132,31 @@ function EmailChangeForm({ onDone }) {
};
return (
<form onSubmit={submit} style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 12 }}>
<form onSubmit={submit} style={{
marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--border)',
display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 400,
}}>
<p style={{ margin: 0, fontSize: 13, color: 'var(--text-muted)' }}>
Après le changement, un email de vérification sera envoyé à la nouvelle adresse.
</p>
{err && <div className="error">{err}</div>}
{msg && <div className="success-msg">{msg}</div>}
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<label className="profile-label">Nouvelle adresse email</label>
<input className="profile-input" type="email" required value={newEmail} onChange={e => setNewEmail(e.target.value)} placeholder="nouvelle@email.com" />
<div>
<label>Nouvelle adresse email</label>
<input type="email" required value={newEmail} onChange={e => setNewEmail(e.target.value)} placeholder="nouvelle@email.com" />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<label className="profile-label">Mot de passe actuel (confirmation)</label>
<PasswordInput className="profile-input" required value={pwd} onChange={e => setPwd(e.target.value)} placeholder="••••••••" />
<div>
<label>Mot de passe actuel (confirmation)</label>
<PasswordInput required value={pwd} onChange={e => setPwd(e.target.value)} placeholder="••••••••" />
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button type="submit" className="btn btn-primary" disabled={busy} style={{ fontSize: 13 }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 12 }}>
<button type="submit" className="primary" disabled={busy}>
{busy ? 'Enregistrement…' : 'Confirmer le changement'}
</button>
<button type="button" className="btn btn-outline" onClick={() => onDone()} style={{ fontSize: 13 }}>Annuler</button>
<button type="button" onClick={() => onDone()}
style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
Annuler
</button>
</div>
</form>
);
@@ -191,64 +197,73 @@ function AccountForm() {
const handleBlur = () => save();
return (
<div className="profile-page">
<>
{/* ── Mon profil ──────────────────────────────────────── */}
<section className="profile-section">
<h2 className="profile-section-title">Mon profil</h2>
<div className="card">
<h3 style={{ margin: '0 0 4px' }}>Mon profil</h3>
<p className="text-muted" style={{ margin: '0 0 16px', fontSize: 'var(--fs-sm)' }}>
Vos informations personnelles et votre adresse email de connexion.
</p>
{infoErr && <div className="error" style={{ marginBottom: 12 }}>{infoErr}</div>}
{infoMsg && <div className="success-msg" style={{ marginBottom: 12 }}>{infoMsg}</div>}
<div className="profile-grid-2">
<div className="profile-field">
<span className="profile-label">Prénom</span>
<input className="profile-input" value={prenom}
onChange={e => setPrenom(e.target.value)}
onBlur={handleBlur}
placeholder="Prénom" />
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 400 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<label>Prénom</label>
<input value={prenom}
onChange={e => setPrenom(e.target.value)}
onBlur={handleBlur}
placeholder="Prénom" />
</div>
<div>
<label>Nom</label>
<input value={nom}
onChange={e => setNom(e.target.value)}
onBlur={handleBlur}
placeholder="NOM" />
</div>
</div>
<div className="profile-field">
<span className="profile-label">Nom</span>
<input className="profile-input" value={nom}
onChange={e => setNom(e.target.value)}
onBlur={handleBlur}
placeholder="NOM" />
<div>
<label>Mon email</label>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<span style={{ fontSize: 'var(--fs-base)', color: 'var(--text)' }}>{user?.email}</span>
{user?.email_verified
? <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--success-bg, #f0fdf4)', color: 'var(--success, #16a34a)', border: '1px solid #bbf7d0' }}> Vérifié</span>
: <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--warning-bg, #fffbeb)', color: 'var(--warning-text, #92400e)', border: '1px solid #fcd34d' }}>Non vérifié</span>
}
</div>
{!user?.email_verified && <EmailResendBlock email={user?.email} />}
</div>
</div>
<div className="profile-field profile-field-full">
<span className="profile-label">Mon email</span>
<div className="profile-email-row" style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<span className="profile-email-value">{user?.email}</span>
{user?.email_verified
? <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--success-bg, #f0fdf4)', color: 'var(--success, #16a34a)', border: '1px solid #bbf7d0' }}> Vérifié</span>
: <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--warning-bg, #fffbeb)', color: 'var(--warning-text, #92400e)', border: '1px solid #fcd34d' }}>Non vérifié</span>
}
{!changingEmail && (
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
<button className="primary" type="button" onClick={() => setChangingEmail(true)}>
Changer mon email
</button>
</div>
{!user?.email_verified && <EmailResendBlock email={user?.email} />}
</div>
<div style={{ marginTop: 20 }}>
<button className="profile-manage-btn" type="button" onClick={() => setChangingEmail(v => !v)}>
{changingEmail ? 'Annuler' : 'Changer mon email'}
</button>
</div>
)}
{changingEmail && <EmailChangeForm onDone={() => setChangingEmail(false)} />}
</section>
{loading && <p className="text-muted" style={{ fontSize: 'var(--fs-sm)', marginTop: 12 }}>Enregistrement</p>}
</div>
{/* ── Préférences ─────────────────────────────────────── */}
<section className="profile-section">
<h2 className="profile-section-title">Préférences</h2>
<div className="profile-grid-2">
<div className="card" style={{ marginTop: 20 }}>
<h3 style={{ margin: '0 0 4px' }}>Préférences</h3>
<p className="text-muted" style={{ margin: '0 0 16px', fontSize: 'var(--fs-sm)' }}>
Langue et devise utilisées dans l'application.
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, maxWidth: 400 }}>
<ProfileSelect label="Langue" options={LANGUES} value={langue} onChange={setLangue} />
<ProfileSelect label="Devise" options={DEVISES} value={devise} onChange={setDevise} />
</div>
</section>
{loading && <p className="text-muted" style={{ fontSize: 'var(--fs-sm)' }}>Enregistrement</p>}
</div>
</div>
</>
);
}
@@ -322,15 +337,19 @@ function TwoFASection({ user }) {
{/* ── État normal ── */}
{status === 'idle' && !enabled && (
<button className="primary" onClick={() => startSetup()} disabled={busy}>
{busy ? 'Chargement…' : 'Activer le 2FA'}
</button>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button className="primary" onClick={() => startSetup()} disabled={busy}>
{busy ? 'Chargement' : 'Activer le 2FA'}
</button>
</div>
)}
{status === 'idle' && enabled && (
<button className="btn btn-outline" style={{ color: 'var(--danger,#dc2626)', borderColor: 'var(--danger,#dc2626)' }}
onClick={() => { setStatus('disabling'); setErr(null); setMsg(null); }}>
Désactiver le 2FA
</button>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button className="btn btn-outline" style={{ color: 'var(--danger,#dc2626)', borderColor: 'var(--danger,#dc2626)' }}
onClick={() => { setStatus('disabling'); setErr(null); setMsg(null); }}>
Désactiver le 2FA
</button>
</div>
)}
{/* ── Setup : QR code + vérification ── */}
@@ -374,10 +393,12 @@ function TwoFASection({ user }) {
{busy ? '…' : 'Confirmer'}
</button>
</form>
<button type="button" onClick={() => { setStatus('idle'); setSetupData(null); setCode(''); setErr(null); }}
style={{ marginTop: 12, background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
Annuler
</button>
<div style={{ display: 'flex', justifyContent: 'flex-end', maxWidth: 320, marginTop: 12 }}>
<button type="button" onClick={() => { setStatus('idle'); setSetupData(null); setCode(''); setErr(null); }}
style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
Annuler
</button>
</div>
</div>
)}
@@ -399,10 +420,12 @@ function TwoFASection({ user }) {
{busy ? '…' : 'Désactiver'}
</button>
</div>
<button type="button" onClick={() => { setStatus('idle'); setErr(null); setDisablePwd(''); }}
style={{ marginTop: 10, background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
Annuler
</button>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 10 }}>
<button type="button" onClick={() => { setStatus('idle'); setErr(null); setDisablePwd(''); }}
style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
Annuler
</button>
</div>
</form>
)}
</div>
@@ -438,26 +461,6 @@ function SecurityForm() {
finally { setPwdLoading(false); }
};
const handleBackfillComptes = async () => {
setLoadingBackfill(true);
setErrorMsg(null);
setSuccessMsg(null);
try {
const { updated, total } = await api.post('/remboursements/backfill-comptes', {});
if (updated === 0) {
setSuccessMsg(`Aucun remboursement à corriger (${total} vérifié${total > 1 ? 's' : ''}).`);
} else {
setSuccessMsg(`${updated} remboursement${updated > 1 ? 's' : ''} mis à jour sur ${total} vérifié${total > 1 ? 's' : ''}.`);
}
setShowBackfillModal(false);
} catch (err) {
setErrorMsg(err.message || 'Une erreur est survenue.');
setShowBackfillModal(false);
} finally {
setLoadingBackfill(false);
}
};
return (
<div className="card">
<h3 style={{ margin: '0 0 4px' }}>Mot de passe</h3>
@@ -742,9 +745,11 @@ export default function MonCompte() {
))}
</aside>
{/* ── Contenu ─────────────────────────────────────── */}
<div className="account-content">
{section === 'profil' && <AccountForm />}
{section === 'securite' && <><SecurityForm /><TwoFASection user={user} /><TrustedDevicesSection /></>}
<div className="account-content account-content-center">
<div className="account-content-narrow">
{section === 'profil' && <AccountForm />}
{section === 'securite' && <><SecurityForm /><TwoFASection user={user} /><TrustedDevicesSection /></>}
</div>
</div>
</div>
+11
View File
@@ -1338,6 +1338,17 @@ tr:hover td { background: var(--surface-2); }
padding: 24px 0 32px 28px;
min-width: 0;
}
/* Variante pour les pages "compte" à faible densité de contenu (MonCompte) :
évite que les cards s'étirent sur toute la largeur d'un grand écran.
Centrage via flex (plus fiable qu'un margin:auto sur un enfant de grid). */
.account-content-center {
display: flex;
justify-content: center;
}
.account-content-narrow {
width: 100%;
max-width: 760px;
}
/* AccountForm interne — profil + préférences côte-à-côte */
.profile-page {