import { useState, useEffect, useRef, useCallback } from "react"; import { Navigate } from "react-router-dom"; import { motion, AnimatePresence } from "framer-motion"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; import CircularProgress from "@mui/material/CircularProgress"; import { keyframes } from "@mui/system"; import type { Theme } from "@mui/material/styles"; import { useAuth } from "../context/AuthContext"; import { useAlert } from "../context/AlertContext"; import { useTheme } from "../../context/ThemeContext"; import { shouldShowSessionExpiredAlert, shouldShowLogoutAlert, } from "../utils/api"; import { TextField, Button, CheckboxField, Field } from "../ui"; // Drift animation for the decorative background orbs (was @keyframes float). const float = keyframes({ "0%, 100%": { transform: "translate(0, 0)" }, "50%": { transform: "translate(30px, -30px)" }, }); export default function Login() { const { login, verify2FA, isAuthenticated, loading: authLoading } = useAuth(); const alert = useAlert(); const { theme, toggleTheme } = useTheme(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [remember, setRemember] = useState(false); const [loading, setLoading] = useState(false); const [shake, setShake] = useState(false); const [animatingOut, setAnimatingOut] = useState(false); // 2FA state const [show2FA, setShow2FA] = useState(false); const [loginToken, setLoginToken] = useState(null); const [totpCode, setTotpCode] = useState(""); const [useBackupCode, setUseBackupCode] = useState(false); const totpInputRef = useRef(null); useEffect(() => { if (shouldShowSessionExpiredAlert()) { alert.warning("Vaše relace vypršela. Přihlaste se prosím znovu."); } else if (shouldShowLogoutAlert()) { alert.success("Byli jste úspěšně odhlášeni."); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Auto-focus TOTP input useEffect(() => { if (show2FA && totpInputRef.current) { totpInputRef.current.focus(); } }, [show2FA, useBackupCode]); const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); const result = await login(username, password, remember); if (result.requires2FA) { setLoginToken(result.loginToken ?? null); setShow2FA(true); setTotpCode(""); setLoading(false); } else if (!result.success) { alert.error(result.error ?? "Chyba přihlášení"); setShake(true); setTimeout(() => setShake(false), 500); setLoading(false); } else { alert.success("Úspěšně přihlášeno"); setAnimatingOut(true); setTimeout(() => setAnimatingOut(false), 400); } }, [ username, password, remember, login, alert, setLoading, setShake, setAnimatingOut, setLoginToken, setShow2FA, setTotpCode, ], ); const handle2FASubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!totpCode.trim()) return; setLoading(true); const result = await verify2FA( loginToken!, totpCode.trim(), remember, useBackupCode, ); if (!result.success) { alert.error(result.error ?? "Chyba ověření"); setShake(true); setTimeout(() => setShake(false), 500); setTotpCode(""); if (totpInputRef.current) totpInputRef.current.focus(); setLoading(false); } else { alert.success("Úspěšně přihlášeno"); setAnimatingOut(true); setTimeout(() => setAnimatingOut(false), 400); } }, [ totpCode, loginToken, remember, useBackupCode, verify2FA, alert, setLoading, setShake, setTotpCode, setAnimatingOut, ], ); const handleBack = useCallback(() => { setShow2FA(false); setLoginToken(null); setTotpCode(""); setUseBackupCode(false); }, [setShow2FA, setLoginToken, setTotpCode, setUseBackupCode]); if (authLoading) { return ( ); } if (isAuthenticated && !animatingOut) { return ; } // Shared glass card sx (replaces .admin-login-card) const cardSx = (theme: Theme) => ({ width: "100%", maxWidth: 420, maxHeight: "calc(100dvh - 2rem)", p: { xs: 4, sm: 5 }, bgcolor: "background.paper", backdropFilter: "blur(20px)", border: "1px solid", borderColor: "divider", borderRadius: "16px", boxShadow: "0 1px 3px rgba(0,0,0,0.06), 0 4px 16px rgba(0,0,0,0.04)", ...theme.applyStyles("dark", { boxShadow: "0 1px 3px rgba(0,0,0,0.2), 0 4px 16px rgba(0,0,0,0.15)", }), position: "relative" as const, zIndex: 1, overflowY: "auto" as const, }); // Framer entrance + shake choreography for the card panels (unchanged) const cardMotion = { initial: { opacity: 0, y: 30 }, animate: shake ? { opacity: 1, y: 0, x: [0, -12, 12, -8, 8, -4, 4, 0] } : { opacity: 1, y: 0 }, exit: { opacity: 0, y: -20 }, transition: shake ? { x: { duration: 0.5, ease: "easeOut" as const } } : { duration: 0.3 }, }; return ( {/* Animated background orbs (replaces .bg-orb) */} {/* Theme toggle (replaces .admin-login-theme-btn) */} {theme === "light" ? ( ) : ( )} {!show2FA ? ( { (e.target as HTMLImageElement).src = theme === "dark" ? "/images/logo-dark.png" : "/images/logo-light.png"; }} sx={{ height: 48, width: "auto", mb: 2 }} /> Interní systém Přihlaste se ke svému účtu setUsername(e.target.value)} required autoComplete="username" placeholder="Zadejte uživatelské jméno" /> setPassword(e.target.value)} required autoComplete="current-password" placeholder="Zadejte heslo" /> ) : ( Dvoufaktorové ověření {useBackupCode ? "Zadejte jeden ze záložních kódů" : "Zadejte 6místný kód z autentizační aplikace"} { const val = useBackupCode ? e.target.value : e.target.value.replace(/\D/g, ""); setTotpCode(val); }} required autoComplete="one-time-code" placeholder={useBackupCode ? "XXXXXXXX" : "000000"} slotProps={{ htmlInput: { inputMode: useBackupCode ? "text" : "numeric", pattern: useBackupCode ? undefined : "[0-9]*", maxLength: useBackupCode ? 8 : 6, style: useBackupCode ? {} : { textAlign: "center", fontSize: "1.5rem", letterSpacing: "0.5rem", fontFamily: "monospace", }, }, }} /> )} ); }