feat(mui): migrate Login (auth shell + 2FA) onto MUI kit; drop login.css
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,6 @@ import "./datepicker.css";
|
|||||||
import "./filemanager.css";
|
import "./filemanager.css";
|
||||||
import "./pagination.css";
|
import "./pagination.css";
|
||||||
import "./responsive.css";
|
import "./responsive.css";
|
||||||
import "./login.css";
|
|
||||||
import "./dashboard.css";
|
import "./dashboard.css";
|
||||||
import "./attendance.css";
|
import "./attendance.css";
|
||||||
import "./plan.css";
|
import "./plan.css";
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
/* ============================================================================
|
|
||||||
Login Page
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
.admin-login {
|
|
||||||
min-height: 100vh;
|
|
||||||
min-height: 100dvh;
|
|
||||||
max-height: 100vh;
|
|
||||||
max-height: 100dvh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 1rem;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login .bg-orb {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
filter: blur(80px);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login .bg-orb-1 {
|
|
||||||
width: 400px;
|
|
||||||
height: 400px;
|
|
||||||
background: var(--orb-color-1);
|
|
||||||
top: 10%;
|
|
||||||
left: 20%;
|
|
||||||
animation: float 20s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login .bg-orb-2 {
|
|
||||||
width: 320px;
|
|
||||||
height: 320px;
|
|
||||||
background: var(--orb-color-2);
|
|
||||||
bottom: 20%;
|
|
||||||
right: 15%;
|
|
||||||
animation: float 25s ease-in-out infinite reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login-theme-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
padding: 0;
|
|
||||||
background: var(--glass-bg);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: var(--transition);
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login-theme-btn:hover {
|
|
||||||
background: var(--glass-bg-solid);
|
|
||||||
color: var(--text-primary);
|
|
||||||
border-color: var(--border-color-hover);
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login-card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 420px;
|
|
||||||
max-height: calc(100vh - 2rem);
|
|
||||||
max-height: calc(100dvh - 2rem);
|
|
||||||
padding: 2rem;
|
|
||||||
background: var(--glass-bg);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
box-shadow: var(--glass-shadow);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.admin-login-card {
|
|
||||||
padding: 2.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login-logo {
|
|
||||||
height: 48px;
|
|
||||||
width: auto;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login-2fa-icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--accent-light);
|
|
||||||
color: var(--accent-color);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login-title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-family: var(--font-heading);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-login-subtitle {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-back-link {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-back-link:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
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 { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useTheme } from "../../context/ThemeContext";
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
@@ -8,7 +12,7 @@ import {
|
|||||||
shouldShowSessionExpiredAlert,
|
shouldShowSessionExpiredAlert,
|
||||||
shouldShowLogoutAlert,
|
shouldShowLogoutAlert,
|
||||||
} from "../utils/api";
|
} from "../utils/api";
|
||||||
import FormField from "../components/FormField";
|
import { TextField, Button, CheckboxField, Field } from "../ui";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { login, verify2FA, isAuthenticated, loading: authLoading } = useAuth();
|
const { login, verify2FA, isAuthenticated, loading: authLoading } = useAuth();
|
||||||
@@ -132,11 +136,17 @@ export default function Login() {
|
|||||||
|
|
||||||
if (authLoading) {
|
if (authLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-login">
|
<Box
|
||||||
<div className="admin-loading">
|
sx={{
|
||||||
<div className="admin-spinner" />
|
minHeight: ["100vh", "100dvh"],
|
||||||
</div>
|
display: "flex",
|
||||||
</div>
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: "var(--bg-primary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,32 +154,115 @@ export default function Login() {
|
|||||||
return <Navigate to="/" replace />;
|
return <Navigate to="/" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared glass card sx (replaces .admin-login-card)
|
||||||
|
const cardSx = {
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: 420,
|
||||||
|
maxHeight: "calc(100dvh - 2rem)",
|
||||||
|
p: { xs: 4, sm: 5 },
|
||||||
|
background: "var(--glass-bg)",
|
||||||
|
backdropFilter: "blur(20px)",
|
||||||
|
border: "1px solid var(--glass-border)",
|
||||||
|
borderRadius: "var(--border-radius-lg)",
|
||||||
|
boxShadow: "var(--glass-shadow)",
|
||||||
|
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 (
|
return (
|
||||||
<motion.div
|
<Box
|
||||||
className="admin-login"
|
component={motion.div}
|
||||||
initial={{ opacity: 0, scale: 0.98 }}
|
initial={{ opacity: 0, scale: 0.98 }}
|
||||||
animate={
|
animate={
|
||||||
animatingOut
|
animatingOut
|
||||||
? { scale: 1.5, opacity: 0, filter: "blur(12px)" }
|
? { scale: 1.5, opacity: 0, filter: "blur(12px)" }
|
||||||
: { scale: 1, opacity: 1, filter: "none" }
|
: { scale: 1, opacity: 1, filter: "none" }
|
||||||
}
|
}
|
||||||
transition={
|
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
|
||||||
animatingOut
|
sx={{
|
||||||
? { duration: 0.25, ease: [0.4, 0, 0.2, 1] }
|
minHeight: ["100vh", "100dvh"],
|
||||||
: { duration: 0.25, ease: [0.4, 0, 0.2, 1] }
|
maxHeight: ["100vh", "100dvh"],
|
||||||
}
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
p: 2,
|
||||||
|
background: "var(--bg-primary)",
|
||||||
|
position: "relative",
|
||||||
|
overflow: "hidden",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-orb bg-orb-1" />
|
{/* Animated background orbs (replaces .bg-orb) */}
|
||||||
<div className="bg-orb bg-orb-2" />
|
<Box
|
||||||
|
aria-hidden
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
borderRadius: "50%",
|
||||||
|
filter: "blur(80px)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
width: 400,
|
||||||
|
height: 400,
|
||||||
|
background: "var(--orb-color-1)",
|
||||||
|
top: "10%",
|
||||||
|
left: "20%",
|
||||||
|
animation: "float 20s ease-in-out infinite",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
aria-hidden
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
borderRadius: "50%",
|
||||||
|
filter: "blur(80px)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
width: 320,
|
||||||
|
height: 320,
|
||||||
|
background: "var(--orb-color-2)",
|
||||||
|
bottom: "20%",
|
||||||
|
right: "15%",
|
||||||
|
animation: "float 25s ease-in-out infinite reverse",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<button
|
{/* Theme toggle (replaces .admin-login-theme-btn) */}
|
||||||
|
<IconButton
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
className="admin-login-theme-btn"
|
|
||||||
title={theme === "dark" ? "Světlý režim" : "Tmavý režim"}
|
title={theme === "dark" ? "Světlý režim" : "Tmavý režim"}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "1rem",
|
||||||
|
right: "1rem",
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
background: "var(--glass-bg)",
|
||||||
|
backdropFilter: "blur(10px)",
|
||||||
|
border: "1px solid var(--glass-border)",
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
overflow: "hidden",
|
||||||
|
zIndex: 10,
|
||||||
|
transition: "var(--transition)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "var(--glass-bg-solid)",
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
borderColor: "var(--border-color-hover)",
|
||||||
|
transform: "scale(1.05)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span
|
{theme === "light" ? (
|
||||||
className={`admin-theme-icon ${theme === "light" ? "visible" : ""}`}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
@@ -181,10 +274,7 @@ export default function Login() {
|
|||||||
<circle cx="12" cy="12" r="5" />
|
<circle cx="12" cy="12" r="5" />
|
||||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
) : (
|
||||||
<span
|
|
||||||
className={`admin-theme-icon ${theme === "dark" ? "visible" : ""}`}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
@@ -195,122 +285,114 @@ export default function Login() {
|
|||||||
>
|
>
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
)}
|
||||||
</button>
|
</IconButton>
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{!show2FA ? (
|
{!show2FA ? (
|
||||||
<motion.div
|
<Box key="login" component={motion.div} {...cardMotion} sx={cardSx}>
|
||||||
key="login"
|
<Box sx={{ textAlign: "center", mb: 4 }}>
|
||||||
className="admin-login-card"
|
<Box
|
||||||
initial={{ opacity: 0, y: 30 }}
|
component="img"
|
||||||
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" } }
|
|
||||||
: { duration: 0.3 }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="admin-login-header">
|
|
||||||
<img
|
|
||||||
src={
|
src={
|
||||||
theme === "dark"
|
theme === "dark"
|
||||||
? "/api/admin/company-settings/logo?variant=dark"
|
? "/api/admin/company-settings/logo?variant=dark"
|
||||||
: "/api/admin/company-settings/logo?variant=light"
|
: "/api/admin/company-settings/logo?variant=light"
|
||||||
}
|
}
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
className="admin-login-logo"
|
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).src =
|
(e.target as HTMLImageElement).src =
|
||||||
theme === "dark"
|
theme === "dark"
|
||||||
? "/images/logo-dark.png"
|
? "/images/logo-dark.png"
|
||||||
: "/images/logo-light.png";
|
: "/images/logo-light.png";
|
||||||
}}
|
}}
|
||||||
|
sx={{ height: 48, width: "auto", mb: 2 }}
|
||||||
/>
|
/>
|
||||||
<h1 className="admin-login-title">Interní systém</h1>
|
<Typography
|
||||||
<p className="admin-login-subtitle">Přihlaste se ke svému účtu</p>
|
variant="h1"
|
||||||
</div>
|
sx={{
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
fontFamily: "var(--font-heading)",
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Interní systém
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{ color: "var(--text-secondary)", fontSize: "0.95rem" }}
|
||||||
|
>
|
||||||
|
Přihlaste se ke svému účtu
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="admin-form">
|
<Box component="form" onSubmit={handleSubmit}>
|
||||||
<FormField label="Uživatelské jméno nebo e-mail">
|
<Field label="Uživatelské jméno nebo e-mail">
|
||||||
<input
|
<TextField
|
||||||
id="username"
|
id="username"
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
required
|
required
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
className="admin-form-input"
|
|
||||||
placeholder="Zadejte uživatelské jméno"
|
placeholder="Zadejte uživatelské jméno"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</Field>
|
||||||
|
|
||||||
<FormField label="Heslo">
|
<Field label="Heslo">
|
||||||
<input
|
<TextField
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
className="admin-form-input"
|
|
||||||
placeholder="Zadejte heslo"
|
placeholder="Zadejte heslo"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</Field>
|
||||||
|
|
||||||
<label className="admin-form-checkbox">
|
<Box sx={{ mb: 2 }}>
|
||||||
<input
|
<CheckboxField
|
||||||
type="checkbox"
|
label="Zapamatovat si mě"
|
||||||
checked={remember}
|
checked={remember}
|
||||||
onChange={(e) => setRemember(e.target.checked)}
|
onChange={setRemember}
|
||||||
/>
|
/>
|
||||||
<span>Zapamatovat si mě</span>
|
</Box>
|
||||||
</label>
|
|
||||||
|
|
||||||
<button
|
<Button type="submit" disabled={loading} fullWidth>
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<CircularProgress
|
||||||
className="admin-spinner"
|
size={20}
|
||||||
style={{ width: 20, height: 20, borderWidth: 2 }}
|
thickness={5}
|
||||||
|
color="inherit"
|
||||||
|
sx={{ mr: 1 }}
|
||||||
/>
|
/>
|
||||||
Přihlašování...
|
Přihlašování...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Přihlásit se"
|
"Přihlásit se"
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</Box>
|
||||||
</motion.div>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<motion.div
|
<Box key="2fa" component={motion.div} {...cardMotion} sx={cardSx}>
|
||||||
key="2fa"
|
<Box sx={{ textAlign: "center", mb: 4 }}>
|
||||||
className="admin-login-card"
|
<Box
|
||||||
initial={{ opacity: 0, y: 30 }}
|
sx={{
|
||||||
animate={
|
display: "inline-flex",
|
||||||
shake
|
alignItems: "center",
|
||||||
? { opacity: 1, y: 0, x: [0, -12, 12, -8, 8, -4, 4, 0] }
|
justifyContent: "center",
|
||||||
: { opacity: 1, y: 0 }
|
width: 56,
|
||||||
}
|
height: 56,
|
||||||
exit={{ opacity: 0, y: -20 }}
|
borderRadius: "50%",
|
||||||
transition={
|
background: "var(--accent-light)",
|
||||||
shake
|
color: "var(--accent-color)",
|
||||||
? { x: { duration: 0.5, ease: "easeOut" } }
|
mb: 2,
|
||||||
: { duration: 0.3 }
|
}}
|
||||||
}
|
>
|
||||||
>
|
|
||||||
<div className="admin-login-header">
|
|
||||||
<div className="admin-login-2fa-icon">
|
|
||||||
<svg
|
<svg
|
||||||
width="32"
|
width="32"
|
||||||
height="32"
|
height="32"
|
||||||
@@ -322,26 +404,34 @@ export default function Login() {
|
|||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
||||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</Box>
|
||||||
<h1 className="admin-login-title">Dvoufaktorové ověření</h1>
|
<Typography
|
||||||
<p className="admin-login-subtitle">
|
variant="h1"
|
||||||
|
sx={{
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
fontFamily: "var(--font-heading)",
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dvoufaktorové ověření
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{ color: "var(--text-secondary)", fontSize: "0.95rem" }}
|
||||||
|
>
|
||||||
{useBackupCode
|
{useBackupCode
|
||||||
? "Zadejte jeden ze záložních kódů"
|
? "Zadejte jeden ze záložních kódů"
|
||||||
: "Zadejte 6místný kód z autentizační aplikace"}
|
: "Zadejte 6místný kód z autentizační aplikace"}
|
||||||
</p>
|
</Typography>
|
||||||
</div>
|
</Box>
|
||||||
|
|
||||||
<form onSubmit={handle2FASubmit} className="admin-form">
|
<Box component="form" onSubmit={handle2FASubmit}>
|
||||||
<FormField
|
<Field label={useBackupCode ? "Záložní kód" : "Ověřovací kód"}>
|
||||||
label={useBackupCode ? "Záložní kód" : "Ověřovací kód"}
|
<TextField
|
||||||
>
|
inputRef={totpInputRef}
|
||||||
<input
|
|
||||||
ref={totpInputRef}
|
|
||||||
id="totp-code"
|
id="totp-code"
|
||||||
type="text"
|
type="text"
|
||||||
inputMode={useBackupCode ? "text" : "numeric"}
|
|
||||||
pattern={useBackupCode ? undefined : "[0-9]*"}
|
|
||||||
maxLength={useBackupCode ? 8 : 6}
|
|
||||||
value={totpCode}
|
value={totpCode}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = useBackupCode
|
const val = useBackupCode
|
||||||
@@ -351,80 +441,93 @@ export default function Login() {
|
|||||||
}}
|
}}
|
||||||
required
|
required
|
||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
className="admin-form-input"
|
|
||||||
placeholder={useBackupCode ? "XXXXXXXX" : "000000"}
|
placeholder={useBackupCode ? "XXXXXXXX" : "000000"}
|
||||||
style={
|
slotProps={{
|
||||||
useBackupCode
|
htmlInput: {
|
||||||
? {}
|
inputMode: useBackupCode ? "text" : "numeric",
|
||||||
: {
|
pattern: useBackupCode ? undefined : "[0-9]*",
|
||||||
textAlign: "center",
|
maxLength: useBackupCode ? 8 : 6,
|
||||||
fontSize: "1.5rem",
|
style: useBackupCode
|
||||||
letterSpacing: "0.5rem",
|
? {}
|
||||||
fontFamily: "monospace",
|
: {
|
||||||
}
|
textAlign: "center",
|
||||||
}
|
fontSize: "1.5rem",
|
||||||
|
letterSpacing: "0.5rem",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</Field>
|
||||||
|
|
||||||
<button
|
<Button type="submit" disabled={loading} fullWidth>
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<CircularProgress
|
||||||
className="admin-spinner"
|
size={20}
|
||||||
style={{ width: 20, height: 20, borderWidth: 2 }}
|
thickness={5}
|
||||||
|
color="inherit"
|
||||||
|
sx={{ mr: 1 }}
|
||||||
/>
|
/>
|
||||||
Ověřování...
|
Ověřování...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Ověřit"
|
"Ověřit"
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</Box>
|
||||||
|
|
||||||
<div
|
<Box
|
||||||
style={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "0.5rem",
|
gap: 1,
|
||||||
marginTop: "0.5rem",
|
mt: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<Button
|
||||||
|
variant="text"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUseBackupCode(!useBackupCode);
|
setUseBackupCode(!useBackupCode);
|
||||||
setTotpCode("");
|
setTotpCode("");
|
||||||
}}
|
}}
|
||||||
className="admin-back-link"
|
sx={{
|
||||||
style={{
|
color: "var(--text-secondary)",
|
||||||
border: "none",
|
fontSize: "0.875rem",
|
||||||
background: "none",
|
fontWeight: 400,
|
||||||
cursor: "pointer",
|
textTransform: "none",
|
||||||
|
"&:hover": {
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
textDecoration: "underline",
|
||||||
|
background: "transparent",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{useBackupCode
|
{useBackupCode
|
||||||
? "Použít autentizační aplikaci"
|
? "Použít autentizační aplikaci"
|
||||||
: "Použít záložní kód"}
|
: "Použít záložní kód"}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant="text"
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
className="admin-back-link"
|
sx={{
|
||||||
style={{
|
color: "var(--text-secondary)",
|
||||||
border: "none",
|
fontSize: "0.875rem",
|
||||||
background: "none",
|
fontWeight: 400,
|
||||||
cursor: "pointer",
|
textTransform: "none",
|
||||||
|
"&:hover": {
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
textDecoration: "underline",
|
||||||
|
background: "transparent",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
← Zpět na přihlášení
|
← Zpět na přihlášení
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Box>
|
||||||
</motion.div>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</motion.div>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user