213 lines
7.6 KiB
TypeScript
213 lines
7.6 KiB
TypeScript
// 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>
|
||
);
|
||
}
|