"use client"; import Link from "next/link"; import { useCallback, useId, useState } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useTranslation } from "../../../contexts/MessagesContext"; import Button from "../../buttons/Button"; import TextInput from "../../controls/TextInput"; import ContentLockup from "../../type/ContentLockup"; import { requestMagicLink } from "../../../../lib/create/api"; import { safeInternalPath } from "../../../../lib/safeInternalPath"; /** Mail icon for login modal (inline SVG; same pattern as InfoMessageBox ExclamationIconInline). */ function MailIconInline() { return ( ); } const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; export default function LoginForm() { const t = useTranslation("pages.login"); const tFooter = useTranslation("footer"); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const formAlertId = useId(); const emailErrorId = useId(); const [email, setEmail] = useState(""); const [submitting, setSubmitting] = useState(false); const [emailError, setEmailError] = useState(""); const [formError, setFormError] = useState(""); const [sent, setSent] = useState(false); const nextParam = searchParams.get("next"); const errorParam = searchParams.get("error"); /** Drop `error` from the URL so URL-driven messages don’t linger after a new attempt. */ const stripErrorQuery = useCallback(() => { if (!searchParams.get("error")) return; const params = new URLSearchParams(searchParams.toString()); params.delete("error"); const q = params.toString(); router.replace(q ? `${pathname}?${q}` : pathname, { scroll: false }); }, [pathname, router, searchParams]); const sendLink = useCallback(async () => { stripErrorQuery(); setEmailError(""); setFormError(""); const trimmed = email.trim().toLowerCase(); if (!EMAIL_PATTERN.test(trimmed)) { setEmailError(t("errors.emailInvalid")); return; } setSubmitting(true); try { const nextPath = safeInternalPath(nextParam); const result = await requestMagicLink(trimmed, nextPath); if (result.ok === false) { if (result.retryAfterMs != null && result.retryAfterMs > 0) { const seconds = Math.ceil(result.retryAfterMs / 1000); setFormError( t("errors.rateLimited").replace("{seconds}", String(seconds)), ); } else { setFormError(result.error || t("errors.generic")); } return; } setEmail(trimmed); setSent(true); } catch { setFormError(t("errors.network")); } finally { setSubmitting(false); } }, [email, nextParam, stripErrorQuery, t]); const urlErrorMessage = errorParam === "expired_link" ? t("errors.expiredLink") : errorParam === "invalid_link" || errorParam === "server" ? errorParam === "server" ? t("errors.serverError") : t("errors.invalidLink") : ""; return (
{urlErrorMessage}
) : null} {formError ? ({formError}
) : null} {!sent ? ( ) : null}