import { type FormEvent, useCallback, useEffect, useRef, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { useTranslation } from "@/contexts/i18n-context"; import { useAuth } from "@/hooks/use-auth"; import { setToken } from "@/lib/api"; import { format } from "@/lib/format"; function RotatingPhrase() { const { t } = useTranslation(); const phrases = t.auth.rotatingPhrases; const [index, setIndex] = useState(0); const [visible, setVisible] = useState(true); const advance = useCallback(() => { setVisible(false); setTimeout(() => { setIndex((i) => (i + 1) % phrases.length); setVisible(true); }, 300); }, [phrases.length]); useEffect(() => { const timer = setInterval(advance, 3000); return () => clearInterval(timer); }, [advance]); return ( {phrases[index]} ); } function LanguageSelector() { const { locale, setLocale, supportedLocales } = useTranslation(); const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { function handleClickOutside(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); } } if (open) { document.addEventListener("mousedown", handleClickOutside); } return () => document.removeEventListener("mousedown", handleClickOutside); }, [open]); const current = supportedLocales.find((l) => l.code === locale); return (
{open && (
{supportedLocales.map((l) => ( ))}
)}
); } export function LoginPage() { const { t } = useTranslation(); const { oidcEnabled, oidcProviderName } = useAuth(); const [searchParams] = useSearchParams(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); useEffect(() => { const oidcError = searchParams.get("error"); if (oidcError) { const errorMessages: Record = { oidc_auth_failed: t.auth.oidcAuthFailed, oidc_provider_unreachable: t.auth.oidcProviderUnreachable, oidc_session_expired: t.auth.oidcSessionExpired, oidc_user_not_authorized: t.auth.oidcUserNotAuthorized, oidc_user_limit_reached: t.auth.oidcUserLimitReached, }; setError(errorMessages[oidcError] || t.auth.oidcGenericError); } }, [searchParams, t]); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setLoading(true); setError(""); try { const res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); setError(t.auth.invalidCredentials); return; } const data = await res.json(); setToken(data.token); localStorage.setItem("snapotter-username", data.user?.username || username); if (data.user?.mustChangePassword) { window.location.href = "/change-password"; } else { window.location.href = "/"; } } catch { setError(t.auth.connectionError); } finally { setLoading(false); } }; return (

SnapOtter

{t.auth.login}

setUsername(e.target.value)} placeholder={t.auth.enterUsername} className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20" required />
setPassword(e.target.value)} placeholder={t.auth.enterPassword} className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20" required />
{error &&

{error}

}
{oidcEnabled && ( <>

{t.auth.heroTitle}

{t.auth.heroSubtitle}

); }