import { useCallback, useEffect, useRef, useState } from "react"; import { Link, Navigate, useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { ApiError, forgotPassword, register, resendVerification, resetPassword, verifyEmail, } from "../api"; import { useAuth } from "../app/auth/AuthContext"; function errorMessage(error: unknown, fallback: string): string { if (error instanceof ApiError) { return error.details.length > 0 ? error.details.join("; ") : error.message; } return fallback; } function TurnstileField(props: { onToken(token: string): void }): JSX.Element { const ref = useRef(null); const siteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY; const { onToken } = props; useEffect(() => { if (!siteKey || !ref.current) { onToken("mock-turnstile-token"); return; } const scriptId = "turnstile-script"; if (!document.getElementById(scriptId)) { const script = document.createElement("script"); script.id = scriptId; script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"; script.async = true; script.defer = true; document.head.appendChild(script); } let widgetId: string | null = null; const timer = window.setInterval(() => { if (window.turnstile && ref.current && !widgetId) { widgetId = window.turnstile.render(ref.current, { sitekey: siteKey, callback: onToken, "expired-callback": () => onToken(""), }); window.clearInterval(timer); } }, 100); return () => { window.clearInterval(timer); if (widgetId && window.turnstile) { window.turnstile.remove(widgetId); } }; }, [onToken, siteKey]); return
; } export function LoginPage(): JSX.Element { const { user, login } = useAuth(); const navigate = useNavigate(); const location = useLocation(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); if (user?.emailVerifiedAt) { return ; } const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); setIsSubmitting(true); setError(null); try { await login(email, password); const from = (location.state as { from?: Location } | null)?.from?.pathname ?? "/"; navigate(from, { replace: true }); } catch (err) { setError(errorMessage(err, "Не удалось войти.")); } finally { setIsSubmitting(false); } }; return (

Вход

{error ?

{error}

: null}

Регистрация Забыли пароль?

); } export function RegisterPage(): JSX.Element { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [turnstileToken, setTurnstileToken] = useState(""); const [message, setMessage] = useState(null); const [error, setError] = useState(null); const handleToken = useCallback((token: string) => setTurnstileToken(token), []); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); setError(null); setMessage(null); try { await register({ email, password, turnstileToken }); setMessage("Проверьте почту: мы отправили ссылку для подтверждения email."); } catch (err) { setError(errorMessage(err, "Не удалось зарегистрироваться.")); } }; return (

Регистрация

{message ?

{message}

: null} {error ?

{error}

: null}

Уже есть аккаунт

); } export function VerifyEmailPage(): JSX.Element { const { user, refresh } = useAuth(); const [params] = useSearchParams(); const [email, setEmail] = useState(user?.email ?? ""); const [message, setMessage] = useState(null); const [error, setError] = useState(null); useEffect(() => { const token = params.get("token"); if (!token) { return; } void verifyEmail(token) .then(async () => { await refresh(); setMessage("Email подтверждён. Теперь можно пользоваться календарём."); }) .catch((err) => setError(errorMessage(err, "Ссылка недействительна."))); }, [params, refresh]); const resend = async () => { setError(null); await resendVerification(email); setMessage("Если email зарегистрирован и ещё не подтверждён, письмо отправлено."); }; return (

Подтверждение email

Для доступа к календарю подтвердите email по ссылке из письма.

{ e.preventDefault(); void resend(); }}> {message ?

{message}

: null} {error ?

{error}

: null}
); } export function ForgotPasswordPage(): JSX.Element { const [email, setEmail] = useState(""); const [message, setMessage] = useState(null); return (

Сброс пароля

{ e.preventDefault(); await forgotPassword(email); setMessage("Если email зарегистрирован, ссылка отправлена."); }}> {message ?

{message}

: null}
); } export function ResetPasswordPage(): JSX.Element { const [params] = useSearchParams(); const [password, setPassword] = useState(""); const [message, setMessage] = useState(null); const [error, setError] = useState(null); return (

Новый пароль

{ e.preventDefault(); const token = params.get("token") ?? ""; try { await resetPassword(token, password); setMessage("Пароль обновлён. Теперь войдите заново."); } catch (err) { setError(errorMessage(err, "Не удалось обновить пароль.")); } }}> {message ?

{message}

: null} {error ?

{error}

: null}
); }