diff --git a/src/admin/AdminApp.jsx b/src/admin/AdminApp.jsx index 39dd8ac..acfc312 100644 --- a/src/admin/AdminApp.jsx +++ b/src/admin/AdminApp.jsx @@ -1,5 +1,5 @@ import { lazy, Suspense } from 'react' -import { Routes, Route, Navigate } from 'react-router-dom' +import { Routes, Route } from 'react-router-dom' import { AuthProvider } from './context/AuthContext' import { AlertProvider } from './context/AlertContext' import ErrorBoundary from './components/ErrorBoundary' @@ -45,6 +45,7 @@ const Invoices = lazy(() => import('./pages/Invoices')) const InvoiceCreate = lazy(() => import('./pages/InvoiceCreate')) const InvoiceDetail = lazy(() => import('./pages/InvoiceDetail')) const Settings = lazy(() => import('./pages/Settings')) +const NotFound = lazy(() => import('./pages/NotFound')) export default function AdminApp() { return ( @@ -86,7 +87,7 @@ export default function AdminApp() { } /> } /> - } /> + } /> diff --git a/src/admin/admin.css b/src/admin/admin.css index aad68d5..12b311a 100644 --- a/src/admin/admin.css +++ b/src/admin/admin.css @@ -2419,6 +2419,25 @@ img { } } +/* Error stack (DEV only) */ +.admin-error-stack { + max-width: 600px; + max-height: 200px; + overflow: auto; + padding: 0.75rem 1rem; + margin: 0; + border-radius: var(--border-radius-sm); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--danger-color); + font-family: var(--font-mono); + font-size: 11px; + line-height: 1.5; + text-align: left; + white-space: pre-wrap; + word-break: break-word; +} + /* Keyboard shortcut badge */ .admin-kbd { display: inline-block; diff --git a/src/admin/components/ConfirmModal.jsx b/src/admin/components/ConfirmModal.jsx index a8b8c7d..ebed234 100644 --- a/src/admin/components/ConfirmModal.jsx +++ b/src/admin/components/ConfirmModal.jsx @@ -1,6 +1,7 @@ import { useEffect } from 'react' import { motion, AnimatePresence } from 'framer-motion' import useModalLock from '../hooks/useModalLock' +import useFocusTrap from '../hooks/useFocusTrap' export default function ConfirmModal({ isOpen, @@ -14,6 +15,7 @@ export default function ConfirmModal({ loading = false }) { useModalLock(isOpen) + const trapRef = useFocusTrap(isOpen) useEffect(() => { if (!isOpen) return @@ -70,6 +72,7 @@ export default function ConfirmModal({ >
-

Něco se pokazilo při načítání stránky.

- +
+
+ + + + + +
+

Něco se pokazilo při načítání stránky.

+ {import.meta.env.DEV && this.state.error && ( +
+              {this.state.error.message}
+              {this.state.error.stack && `\n${this.state.error.stack}`}
+            
+ )} +
+ + Zpět na Dashboard + + +
) } diff --git a/src/admin/hooks/useFocusTrap.js b/src/admin/hooks/useFocusTrap.js new file mode 100644 index 0000000..9c4e282 --- /dev/null +++ b/src/admin/hooks/useFocusTrap.js @@ -0,0 +1,53 @@ +import { useEffect, useRef } from 'react' + +const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' + +/** + * Focus trap pro modaly - drzi focus uvnitr prvku. + * Vraci ref ktery se pripoji na modal kontejner. + */ +export default function useFocusTrap(isOpen) { + const ref = useRef(null) + + useEffect(() => { + if (!isOpen || !ref.current) { return } + + const container = ref.current + const previouslyFocused = document.activeElement + + // Focus prvni focusable prvek v modalu + const focusable = container.querySelectorAll(FOCUSABLE) + if (focusable.length > 0) { + focusable[0].focus() + } + + const handleKeyDown = (e) => { + if (e.key !== 'Tab') { return } + + const nodes = container.querySelectorAll(FOCUSABLE) + if (nodes.length === 0) { return } + + const first = nodes[0] + const last = nodes[nodes.length - 1] + + if (e.shiftKey && document.activeElement === first) { + e.preventDefault() + last.focus() + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault() + first.focus() + } + } + + document.addEventListener('keydown', handleKeyDown) + + return () => { + document.removeEventListener('keydown', handleKeyDown) + if (previouslyFocused && typeof previouslyFocused.focus === 'function') { + previouslyFocused.focus() + } + } + }, [isOpen]) + + return ref +} diff --git a/src/admin/pages/NotFound.jsx b/src/admin/pages/NotFound.jsx new file mode 100644 index 0000000..73a599e --- /dev/null +++ b/src/admin/pages/NotFound.jsx @@ -0,0 +1,30 @@ +import { Link } from 'react-router-dom' +import { motion } from 'framer-motion' + +export default function NotFound() { + return ( + +
+ + + + + + +
+

+ 404 +

+

Stránka nebyla nalezena.

+ + Zpět na Dashboard + +
+ ) +}