SMPT, SMS, Rejestracja + GUS

This commit is contained in:
2026-08-09 21:12:16 +02:00
parent a0c9d7947b
commit c464c5f708
78 changed files with 5841 additions and 258 deletions
+211
View File
@@ -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>
);
}