SMPT, SMS, Rejestracja + GUS
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
// Strony i komponenty procesu konta: aktywacja przez link, ustawienie nowego hasla, okno OTP telefonu.
|
||||
import { useEffect, useRef, useState, type FormEvent, type ReactNode } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { ROUTES } from './routes';
|
||||
import { useAuth } from './auth';
|
||||
|
||||
const errMsg = (e: unknown) => (e instanceof Error ? e.message : 'Wystąpił błąd. Spróbuj ponownie.');
|
||||
|
||||
function AuthStatusCard({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<section className="auth-status-page">
|
||||
<div className="auth-status-card">
|
||||
<Link to={ROUTES.home} className="auth-status-logo">Polska <span>Lokalnie</span></Link>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// /aktywacja/:token - potwierdzenie adresu e-mail z linku.
|
||||
export function ActivationPage() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const { activateAccount } = useAuth();
|
||||
const [state, setState] = useState<'loading' | 'ok' | 'error'>('loading');
|
||||
const [message, setMessage] = useState('');
|
||||
const ran = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (ran.current) {
|
||||
return;
|
||||
}
|
||||
ran.current = true;
|
||||
(async () => {
|
||||
try {
|
||||
await activateAccount(token ?? '');
|
||||
setState('ok');
|
||||
} catch (e) {
|
||||
setState('error');
|
||||
setMessage(errMsg(e));
|
||||
}
|
||||
})();
|
||||
}, [token, activateAccount]);
|
||||
|
||||
return (
|
||||
<AuthStatusCard>
|
||||
{state === 'loading' && <p className="auth-status-lead">Aktywujemy Twoje konto...</p>}
|
||||
{state === 'ok' && (
|
||||
<>
|
||||
<div className="auth-status-icon ok">✓</div>
|
||||
<h1>Konto zostało aktywowane!</h1>
|
||||
<p className="auth-status-lead">Możesz się teraz zalogować i korzystać ze wszystkich funkcji serwisu.</p>
|
||||
<Link className="auth-status-btn" to={ROUTES.login}>Przejdź do logowania</Link>
|
||||
</>
|
||||
)}
|
||||
{state === 'error' && (
|
||||
<>
|
||||
<div className="auth-status-icon err">!</div>
|
||||
<h1>Nie udało się aktywować konta</h1>
|
||||
<p className="auth-status-lead">{message || 'Link aktywacyjny jest nieprawidłowy lub wygasł.'}</p>
|
||||
<p className="auth-status-hint">Zaloguj się i poproś o ponowne wysłanie linku aktywacyjnego.</p>
|
||||
<Link className="auth-status-btn" to={ROUTES.login}>Przejdź do logowania</Link>
|
||||
</>
|
||||
)}
|
||||
</AuthStatusCard>
|
||||
);
|
||||
}
|
||||
|
||||
// /reset-hasla/:token - ustawienie nowego hasla.
|
||||
export function PasswordResetPage() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const { resetPassword } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirm, setConfirm] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [done, setDone] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const submit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
if (password.length < 8) {
|
||||
setError('Hasło musi mieć co najmniej 8 znaków.');
|
||||
return;
|
||||
}
|
||||
if (password !== confirm) {
|
||||
setError('Hasła nie są identyczne.');
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await resetPassword(token ?? '', password);
|
||||
setDone(true);
|
||||
setTimeout(() => navigate(ROUTES.login), 2500);
|
||||
} catch (e) {
|
||||
setError(errMsg(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthStatusCard>
|
||||
{done ? (
|
||||
<>
|
||||
<div className="auth-status-icon ok">✓</div>
|
||||
<h1>Hasło zostało zmienione</h1>
|
||||
<p className="auth-status-lead">Za chwilę przekierujemy Cię do logowania.</p>
|
||||
<Link className="auth-status-btn" to={ROUTES.login}>Przejdź do logowania</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h1>Ustaw nowe hasło</h1>
|
||||
<p className="auth-status-lead">Wpisz nowe hasło do swojego konta. Dotychczasowe hasło działa do momentu jego zmiany.</p>
|
||||
<form className="auth-status-form" onSubmit={submit}>
|
||||
<label>Nowe hasło
|
||||
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Min. 8 znaków" autoComplete="new-password" required />
|
||||
</label>
|
||||
<label>Powtórz hasło
|
||||
<input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} placeholder="Powtórz nowe hasło" autoComplete="new-password" required />
|
||||
</label>
|
||||
{error && <p className="auth-status-error" role="alert">{error}</p>}
|
||||
<button className="auth-status-btn" type="submit" disabled={busy}>{busy ? 'Zapisywanie...' : 'Ustaw nowe hasło'}</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</AuthStatusCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Okno potwierdzenia numeru telefonu kodem SMS (OTP), pokazywane po rejestracji z podanym telefonem.
|
||||
export function PhoneOtpModal({ email, phone, onClose, onVerified }: {
|
||||
email: string;
|
||||
phone: string;
|
||||
onClose: () => void;
|
||||
onVerified: () => void;
|
||||
}) {
|
||||
const { verifyPhone, resendPhoneOtp } = useAuth();
|
||||
const [code, setCode] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [info, setInfo] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [cooldown, setCooldown] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (cooldown <= 0) {
|
||||
return;
|
||||
}
|
||||
const t = window.setTimeout(() => setCooldown((c) => c - 1), 1000);
|
||||
return () => window.clearTimeout(t);
|
||||
}, [cooldown]);
|
||||
|
||||
const submit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
setInfo(null);
|
||||
setBusy(true);
|
||||
try {
|
||||
await verifyPhone(email, code.trim());
|
||||
onVerified();
|
||||
} catch (e) {
|
||||
setError(errMsg(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resend = async () => {
|
||||
setError(null);
|
||||
try {
|
||||
await resendPhoneOtp(email);
|
||||
setInfo('Wysłaliśmy nowy kod SMS.');
|
||||
setCooldown(30);
|
||||
} catch (e) {
|
||||
setError(errMsg(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="listing-delete-overlay" role="dialog" aria-modal="true" aria-label="Potwierdź numer telefonu">
|
||||
<div className="otp-modal">
|
||||
<button type="button" className="listing-delete-close" aria-label="Zamknij" onClick={onClose}>×</button>
|
||||
<div className="otp-modal-icon">📱</div>
|
||||
<h2>Potwierdź numer telefonu</h2>
|
||||
<p className="otp-modal-lead">Wysłaliśmy kod SMS na numer <strong>{phone}</strong>. Wpisz go poniżej, aby potwierdzić poprawność numeru.</p>
|
||||
<form className="otp-modal-form" onSubmit={submit}>
|
||||
<input
|
||||
className="otp-input"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
maxLength={6}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
placeholder="______"
|
||||
aria-label="Kod z SMS"
|
||||
autoFocus
|
||||
/>
|
||||
{error && <p className="auth-status-error" role="alert">{error}</p>}
|
||||
{info && <p className="otp-modal-ok">{info}</p>}
|
||||
<button className="auth-status-btn" type="submit" disabled={busy || code.length < 6}>{busy ? 'Sprawdzanie...' : 'Potwierdź numer'}</button>
|
||||
</form>
|
||||
<div className="otp-modal-actions">
|
||||
<button type="button" className="otp-link" disabled={cooldown > 0} onClick={resend}>
|
||||
{cooldown > 0 ? `Wyślij ponownie (${cooldown}s)` : 'Wyślij kod ponownie'}
|
||||
</button>
|
||||
<button type="button" className="otp-link" onClick={onClose}>Zrobię to później</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user