Modification des pages d'authentification
This commit is contained in:
+378
-35
@@ -1,44 +1,387 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext.jsx';
|
||||
import { api } from '../api.js';
|
||||
|
||||
export default function Login() {
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [err, setErr] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
const DEVICE_KEY = 'cl_device_token';
|
||||
|
||||
const submit = async (e) => {
|
||||
e.preventDefault();
|
||||
setErr(null); setBusy(true);
|
||||
try {
|
||||
await login(email, password);
|
||||
navigate('/');
|
||||
} catch (e) {
|
||||
setErr(e.message);
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
function fmtCountdown(sec) {
|
||||
const m = Math.floor(sec / 60).toString().padStart(2, '0');
|
||||
const s = (sec % 60).toString().padStart(2, '0');
|
||||
return `${m}:${s}`;
|
||||
}
|
||||
|
||||
// ── Colonne image gauche (partagée) ───────────────────────────────────────
|
||||
function BgCol({ appInfo }) {
|
||||
return (
|
||||
<div className="login-shell">
|
||||
<form className="card login-card" onSubmit={submit}>
|
||||
<h2 style={{ marginTop: 0 }}>Connexion</h2>
|
||||
{err && <div className="error">{err}</div>}
|
||||
<label>Email</label>
|
||||
<input type="email" required value={email} onChange={e => setEmail(e.target.value)} />
|
||||
<div style={{ height: 10 }} />
|
||||
<label>Mot de passe</label>
|
||||
<input type="password" required value={password} onChange={e => setPassword(e.target.value)} />
|
||||
<div style={{ height: 16 }} />
|
||||
<button className="primary" type="submit" disabled={busy} style={{ width: '100%' }}>
|
||||
{busy ? '…' : 'Se connecter'}
|
||||
</button>
|
||||
<p className="text-muted" style={{ marginTop: 16, textAlign: 'center' }}>
|
||||
Pas encore de compte ? <Link to="/register">Créer un compte</Link>
|
||||
</p>
|
||||
</form>
|
||||
<div className="login-bg-col" style={{
|
||||
flex: '1 1 50%', display: 'none',
|
||||
position: 'relative', overflow: 'hidden', background: '#0d0d0d',
|
||||
}}>
|
||||
<img src="/login-bg.jpg" alt="" aria-hidden="true"
|
||||
onError={e => { e.target.style.display = 'none'; }}
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', opacity: 0.85 }}
|
||||
/>
|
||||
<div style={{ position: 'absolute', bottom: 40, left: 40, color: '#fff', display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{appInfo.iconUrl && <img src={appInfo.iconUrl} alt="" width={36} height={36} style={{ borderRadius: 8, flexShrink: 0 }} />}
|
||||
<span style={{ fontSize: 18, fontWeight: 600, letterSpacing: '-0.3px' }}>{appInfo.appName}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Header logo (colonne droite) ──────────────────────────────────────────
|
||||
function AppHeader({ appInfo }) {
|
||||
return (
|
||||
<div className="login-mobile-header" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, marginBottom: 32 }}>
|
||||
{appInfo.iconUrl && <img src={appInfo.iconUrl} alt={appInfo.appName} width={52} height={52} style={{ borderRadius: 12 }} />}
|
||||
<span style={{ fontSize: 20, fontWeight: 700, color: 'var(--text)', letterSpacing: '-0.4px' }}>{appInfo.appName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Composant principal ────────────────────────────────────────────────────
|
||||
export default function Login() {
|
||||
const { login, completeLogin } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [appInfo, setAppInfo] = useState({ appName: 'Crowdlending Tracker', iconUrl: null });
|
||||
|
||||
// Étape : 'form' | 'method' | 'code'
|
||||
const [step, setStep] = useState('form');
|
||||
const [sessionToken, setSessionToken] = useState(null);
|
||||
const [method, setMethod] = useState(null); // 'totp' | 'email'
|
||||
|
||||
// Formulaire login
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [trustDev, setTrustDev] = useState(false);
|
||||
const [err, setErr] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [unverified, setUnverified] = useState(false);
|
||||
const [resendBusy, setResendBusy] = useState(false);
|
||||
const [resendDone, setResendDone] = useState(false);
|
||||
|
||||
// Code 2FA
|
||||
const [code, setCode] = useState('');
|
||||
const [codeErr, setCodeErr] = useState(null);
|
||||
const [codeBusy, setCodeBusy] = useState(false);
|
||||
const [countdown, setCountdown] = useState(300); // 5 min pour email
|
||||
const timerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/app-info').then(r => r.json()).then(d => setAppInfo(d)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Countdown pour OTP email
|
||||
useEffect(() => {
|
||||
if (step === 'code' && method === 'email') {
|
||||
setCountdown(300);
|
||||
timerRef.current = setInterval(() => {
|
||||
setCountdown(c => {
|
||||
if (c <= 1) { clearInterval(timerRef.current); return 0; }
|
||||
return c - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearInterval(timerRef.current);
|
||||
}, [step, method]);
|
||||
|
||||
// ── Soumission du formulaire ──────────────────────────────────────────────
|
||||
const submit = async (e) => {
|
||||
e.preventDefault();
|
||||
setErr(null); setUnverified(false); setResendDone(false); setBusy(true);
|
||||
try {
|
||||
const deviceToken = localStorage.getItem(DEVICE_KEY) || undefined;
|
||||
const result = await login(email, password, deviceToken);
|
||||
if (result?.requires2FA) {
|
||||
setSessionToken(result.sessionToken);
|
||||
setStep('method');
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.code === 'EMAIL_NOT_VERIFIED') {
|
||||
setUnverified(true);
|
||||
} else {
|
||||
setErr(e.message || 'Identifiants incorrects.');
|
||||
}
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const resend = async () => {
|
||||
setResendBusy(true);
|
||||
try {
|
||||
await fetch('/api/auth/resend-verification', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
setResendDone(true);
|
||||
} catch (_) {}
|
||||
finally { setResendBusy(false); }
|
||||
};
|
||||
|
||||
// ── Choix de méthode 2FA ──────────────────────────────────────────────────
|
||||
const chooseMethod = async (m) => {
|
||||
setMethod(m);
|
||||
setCodeErr(null);
|
||||
setCode('');
|
||||
if (m === 'email') {
|
||||
try {
|
||||
await api.post('/auth/2fa/send-email-code', { sessionToken });
|
||||
} catch (e) {
|
||||
setCodeErr(e.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setStep('code');
|
||||
};
|
||||
|
||||
// ── Vérification du code ──────────────────────────────────────────────────
|
||||
const verifyCode = async (e) => {
|
||||
e.preventDefault();
|
||||
setCodeErr(null); setCodeBusy(true);
|
||||
try {
|
||||
const result = await api.post('/auth/2fa/verify', {
|
||||
sessionToken, code, method, trustDevice: trustDev,
|
||||
});
|
||||
if (result.deviceToken) {
|
||||
localStorage.setItem(DEVICE_KEY, result.deviceToken);
|
||||
}
|
||||
completeLogin(result.token, result.user);
|
||||
navigate('/');
|
||||
} catch (e) {
|
||||
setCodeErr(e.message || 'Code invalide.');
|
||||
} finally { setCodeBusy(false); }
|
||||
};
|
||||
|
||||
const resendEmailCode = async () => {
|
||||
setCodeErr(null);
|
||||
try {
|
||||
await api.post('/auth/2fa/send-email-code', { sessionToken });
|
||||
setCountdown(300);
|
||||
timerRef.current = setInterval(() => {
|
||||
setCountdown(c => { if (c <= 1) { clearInterval(timerRef.current); return 0; } return c - 1; });
|
||||
}, 1000);
|
||||
} catch (e) { setCodeErr(e.message); }
|
||||
};
|
||||
|
||||
// ── Rendu ─────────────────────────────────────────────────────────────────
|
||||
const BtnPrimary = ({ children, disabled, onClick, type = 'submit' }) => (
|
||||
<button type={type} disabled={disabled} onClick={onClick} style={{
|
||||
width: '100%', padding: '11px 0',
|
||||
background: disabled ? 'var(--text-muted)' : 'var(--primary, #1e40af)',
|
||||
color: '#fff', border: 'none', borderRadius: 8,
|
||||
fontSize: 15, fontWeight: 600,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
transition: 'background 0.15s',
|
||||
}}>{children}</button>
|
||||
);
|
||||
|
||||
const ErrBox = ({ msg }) => msg ? (
|
||||
<div style={{ padding: '10px 14px', borderRadius: 8, fontSize: 14, background: 'var(--danger-bg, #fef2f2)', color: 'var(--danger, #dc2626)', border: '1px solid var(--danger-light, #fca5a5)' }}>
|
||||
{msg}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
||||
<BgCol appInfo={appInfo} />
|
||||
|
||||
<div style={{
|
||||
flex: '1 1 50%', display: 'flex', flexDirection: 'column',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
padding: '48px 24px', background: 'var(--background, #fff)', minWidth: 0,
|
||||
}}>
|
||||
<AppHeader appInfo={appInfo} />
|
||||
|
||||
<div style={{ width: '100%', maxWidth: 360 }}>
|
||||
|
||||
{/* ══ ÉTAPE 1 : Formulaire email/mot de passe ══════════════════ */}
|
||||
{step === 'form' && (
|
||||
<>
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<h1 style={{ margin: '0 0 6px', fontSize: 26, fontWeight: 700, letterSpacing: '-0.5px', color: 'var(--text)' }}>Bienvenue</h1>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: 14 }}>Connectez-vous à votre compte</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<ErrBox msg={err} />
|
||||
|
||||
{unverified && (
|
||||
<div style={{ padding: '12px 14px', borderRadius: 8, fontSize: 13, background: 'var(--warning-bg, #fffbeb)', color: 'var(--warning-text, #92400e)', border: '1px solid var(--warning-border, #fcd34d)', lineHeight: 1.5 }}>
|
||||
<strong>Email non vérifié.</strong> Vérifiez votre boîte mail et cliquez sur le lien reçu.
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{resendDone
|
||||
? <span style={{ color: 'var(--success, #16a34a)', fontWeight: 500 }}>✓ Email renvoyé !</span>
|
||||
: <button type="button" onClick={() => resend()} disabled={resendBusy} style={{ background: 'none', border: 'none', padding: 0, color: 'var(--primary, #1e40af)', fontWeight: 600, fontSize: 13, cursor: 'pointer', textDecoration: 'underline' }}>
|
||||
{resendBusy ? 'Envoi…' : "Renvoyer l'email de vérification"}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Adresse email</label>
|
||||
<input className="form-input" type="email" required autoComplete="email" placeholder="vous@exemple.com"
|
||||
value={email} onChange={e => setEmail(e.target.value)} style={{ width: '100%' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Mot de passe</label>
|
||||
<Link to="/forgot-password" style={{ fontSize: 13, color: 'var(--text-muted)', textDecoration: 'underline' }}>Mot de passe oublié ?</Link>
|
||||
</div>
|
||||
<input className="form-input" type="password" required autoComplete="current-password" placeholder="••••••••"
|
||||
value={password} onChange={e => setPassword(e.target.value)} style={{ width: '100%' }} />
|
||||
</div>
|
||||
|
||||
<BtnPrimary disabled={busy}>{busy ? 'Connexion…' : 'Se connecter'}</BtnPrimary>
|
||||
</form>
|
||||
|
||||
<p style={{ marginTop: 24, textAlign: 'center', fontSize: 13, color: 'var(--text-muted)' }}>
|
||||
Pas encore de compte ?{' '}
|
||||
<Link to="/register" style={{ color: 'var(--text)', fontWeight: 500, textDecoration: 'underline' }}>Créer un compte</Link>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ══ ÉTAPE 2 : Choix de méthode 2FA ══════════════════════════ */}
|
||||
{step === 'method' && (
|
||||
<>
|
||||
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
<div style={{ width: 52, height: 52, borderRadius: '50%', background: 'var(--primary-bg, #eff6ff)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="5" y="11" width="14" height="10" rx="2" stroke="var(--primary,#1e40af)" strokeWidth="2"/>
|
||||
<path d="M8 11V7a4 4 0 018 0v4" stroke="var(--primary,#1e40af)" strokeWidth="2" strokeLinecap="round"/>
|
||||
<circle cx="12" cy="16" r="1.5" fill="var(--primary,#1e40af)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 style={{ margin: '0 0 6px', fontSize: 22, fontWeight: 700, color: 'var(--text)' }}>Vérification en deux étapes</h1>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: 14 }}>Choisissez votre méthode de vérification</p>
|
||||
</div>
|
||||
|
||||
<ErrBox msg={codeErr} />
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: codeErr ? 16 : 0 }}>
|
||||
<button onClick={() => chooseMethod('totp')} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
padding: '14px 16px', borderRadius: 10,
|
||||
border: '1.5px solid var(--border)', background: 'var(--surface-2, #f9fafb)',
|
||||
cursor: 'pointer', textAlign: 'left', transition: 'border-color 0.15s',
|
||||
}} onMouseEnter={e => e.currentTarget.style.borderColor='var(--primary,#1e40af)'}
|
||||
onMouseLeave={e => e.currentTarget.style.borderColor='var(--border)'}>
|
||||
<div style={{ width: 40, height: 40, borderRadius: 8, background: 'var(--primary-bg,#eff6ff)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="7" y="2" width="10" height="20" rx="2" stroke="var(--primary,#1e40af)" strokeWidth="2"/>
|
||||
<line x1="12" y1="18" x2="12" y2="18" stroke="var(--primary,#1e40af)" strokeWidth="2.5" strokeLinecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>Application d'authentification</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 2 }}>Google Authenticator, Authy, etc.</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button onClick={() => chooseMethod('email')} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
padding: '14px 16px', borderRadius: 10,
|
||||
border: '1.5px solid var(--border)', background: 'var(--surface-2, #f9fafb)',
|
||||
cursor: 'pointer', textAlign: 'left', transition: 'border-color 0.15s',
|
||||
}} onMouseEnter={e => e.currentTarget.style.borderColor='var(--primary,#1e40af)'}
|
||||
onMouseLeave={e => e.currentTarget.style.borderColor='var(--border)'}>
|
||||
<div style={{ width: 40, height: 40, borderRadius: 8, background: 'var(--primary-bg,#eff6ff)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" stroke="var(--primary,#1e40af)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>Code par email</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 2 }}>Recevoir un code à {email}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="button" onClick={() => { setStep('form'); setErr(null); }} style={{ marginTop: 24, width: '100%', background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline' }}>
|
||||
← Retour à la connexion
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ══ ÉTAPE 3 : Saisie du code ═════════════════════════════════ */}
|
||||
{step === 'code' && (
|
||||
<>
|
||||
<div style={{ textAlign: 'center', marginBottom: 28 }}>
|
||||
<div style={{ width: 52, height: 52, borderRadius: '50%', background: 'var(--primary-bg,#eff6ff)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}>
|
||||
{method === 'totp'
|
||||
? <svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect x="7" y="2" width="10" height="20" rx="2" stroke="var(--primary,#1e40af)" strokeWidth="2"/><line x1="12" y1="18" x2="12" y2="18" stroke="var(--primary,#1e40af)" strokeWidth="2.5" strokeLinecap="round"/></svg>
|
||||
: <svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" stroke="var(--primary,#1e40af)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
}
|
||||
</div>
|
||||
<h1 style={{ margin: '0 0 6px', fontSize: 22, fontWeight: 700, color: 'var(--text)' }}>
|
||||
{method === 'totp' ? 'Code de l\'application' : 'Code par email'}
|
||||
</h1>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: 13, lineHeight: 1.5 }}>
|
||||
{method === 'totp'
|
||||
? 'Entrez le code à 6 chiffres affiché dans votre application d\'authentification.'
|
||||
: <>Code envoyé à <strong>{email}</strong>.</>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={verifyCode} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<ErrBox msg={codeErr} />
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Code de vérification</label>
|
||||
{method === 'email' && (
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: countdown > 0 ? 'var(--text-muted)' : 'var(--danger,#dc2626)' }}>
|
||||
{countdown > 0 ? fmtCountdown(countdown) : 'Expiré'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
className="form-input"
|
||||
type="text" inputMode="numeric" pattern="[0-9]{6}"
|
||||
maxLength={6} autoComplete="one-time-code"
|
||||
placeholder="000000"
|
||||
value={code} onChange={e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
style={{ width: '100%', fontSize: 24, letterSpacing: 8, textAlign: 'center', fontWeight: 700 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--text-muted)', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={trustDev} onChange={e => setTrustDev(e.target.checked)} style={{ width: 'auto' }} />
|
||||
Faire confiance à cet appareil pendant 30 jours
|
||||
</label>
|
||||
|
||||
<BtnPrimary disabled={codeBusy || code.length !== 6}>
|
||||
{codeBusy ? 'Vérification…' : 'Vérifier'}
|
||||
</BtnPrimary>
|
||||
|
||||
{method === 'email' && countdown === 0 && (
|
||||
<button type="button" onClick={resendEmailCode} style={{ background: 'none', border: 'none', color: 'var(--primary,#1e40af)', fontWeight: 600, fontSize: 13, cursor: 'pointer', textDecoration: 'underline' }}>
|
||||
Renvoyer le code
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<button type="button" onClick={() => { setStep('method'); setCode(''); setCodeErr(null); clearInterval(timerRef.current); }} style={{ marginTop: 20, width: '100%', background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline' }}>
|
||||
← Changer de méthode
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@media (min-width: 768px) { .login-bg-col { display: block !important; } }
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user