Files
polskalokalnie/frontend/src/AuthPages.tsx
T
2026-08-12 17:03:22 +02:00

213 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.');
// Zgodne z odstepem wymuszanym przez backend (RESEND_COOLDOWN_SECONDS).
setCooldown(60);
} 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>
);
}