This commit is contained in:
Joshua Schmucker
2026-01-30 14:32:53 +01:00
parent 941bc7e5e4
commit bf4136b8dc
22 changed files with 3090 additions and 76 deletions
+74
View File
@@ -0,0 +1,74 @@
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends React.Component<
React.PropsWithChildren<{}>,
ErrorBoundaryState
> {
constructor(props: React.PropsWithChildren<{}>) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{
padding: '2rem',
textAlign: 'center',
backgroundColor: '#f8d7da',
color: '#721c24',
margin: '1rem',
borderRadius: '4px',
border: '1px solid #f5c6cb'
}}>
<h2>Something went wrong</h2>
<p>The application encountered an error. Please try refreshing the page.</p>
<details style={{ marginTop: '1rem', textAlign: 'left' }}>
<summary>Error details</summary>
<pre style={{
background: '#f8f9fa',
padding: '1rem',
borderRadius: '4px',
overflow: 'auto',
fontSize: '0.8rem',
marginTop: '0.5rem'
}}>
{this.state.error?.stack}
</pre>
</details>
<button
onClick={() => window.location.reload()}
style={{
marginTop: '1rem',
padding: '0.75rem 1.5rem',
backgroundColor: '#e74c3c',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Refresh Page
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
+218
View File
@@ -0,0 +1,218 @@
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.navbar {
background-color: #2c3e50;
color: white;
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
}
.nav-brand {
text-decoration: none;
color: white;
transition: background-color 0.2s;
border-radius: 4px;
padding: 0.25rem 0.5rem;
}
.nav-brand:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.nav-brand h1 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
/* Desktop Navigation */
.nav-links-desktop {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 2rem;
}
/* Mobile Menu Button */
.mobile-menu-btn {
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
}
.mobile-menu-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.hamburger-line {
width: 20px;
height: 2px;
background-color: white;
margin: 2px 0;
transition: all 0.3s ease;
transform-origin: center;
}
.mobile-menu-btn[aria-expanded="true"] .hamburger-line:nth-child(1) {
transform: rotate(45deg) translate(5px, 5px);
}
.mobile-menu-btn[aria-expanded="true"] .hamburger-line:nth-child(2) {
opacity: 0;
transform: translateX(10px);
}
.mobile-menu-btn[aria-expanded="true"] .hamburger-line:nth-child(3) {
transform: rotate(-45deg) translate(5px, -5px);
}
/* Mobile Menu */
.mobile-menu {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
}
.mobile-menu-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.nav-links-mobile {
position: absolute;
top: 0;
right: 0;
background-color: #2c3e50;
width: 280px;
height: 100vh;
list-style: none;
margin: 0;
padding: 0;
transform: translateX(100%);
transition: transform 0.3s ease;
box-shadow: -2px 0 10px rgba(0,0,0,0.1);
}
.mobile-menu.open .nav-links-mobile {
transform: translateX(0);
}
.nav-links-mobile li {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.nav-links-mobile li:last-child {
border-bottom: none;
}
/* Common Navigation Styles */
.nav-links a {
color: white;
text-decoration: none;
padding: 1rem 1.5rem;
border-radius: 4px;
transition: background-color 0.2s;
display: block;
font-size: 1rem;
}
.nav-links-desktop a {
padding: 0.5rem 1rem;
}
.nav-links a:hover,
.nav-links a.active {
background-color: #34495e;
}
.nav-brand:hover {
background-color: #34495e;
}
.main-content {
flex: 1;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* Responsive Design */
@media (max-width: 768px) {
.navbar {
padding: 1rem;
}
.nav-brand h1 {
font-size: 1.1rem;
}
.nav-links-desktop {
display: none;
}
.mobile-menu-btn {
display: flex;
}
.mobile-menu {
display: block;
}
.main-content {
padding: 1rem;
}
}
@media (max-width: 480px) {
.navbar {
padding: 0.75rem 1rem;
}
.nav-brand h1 {
font-size: 1rem;
}
.mobile-menu-btn {
width: 28px;
height: 28px;
}
.hamburger-line {
width: 18px;
height: 2px;
}
.nav-links-mobile {
width: 250px;
}
.main-content {
padding: 0.75rem;
}
}
+112
View File
@@ -0,0 +1,112 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import './Layout.css';
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
const location = useLocation();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const toggleMobileMenu = () => {
try {
setIsMobileMenuOpen(!isMobileMenuOpen);
} catch (error) {
console.error('Error toggling mobile menu:', error);
}
};
const closeMobileMenu = () => {
try {
setIsMobileMenuOpen(false);
} catch (error) {
console.error('Error closing mobile menu:', error);
}
};
const navLinks = [
{
to: '/dashboard',
label: 'My Reservations',
isActive: location.pathname === '/dashboard'
},
{
to: '/resources',
label: 'Resources',
isActive: location.pathname.startsWith('/resources')
},
{
to: '/reservations/new',
label: 'Book Resource',
isActive: location.pathname === '/reservations/new'
},
{
to: '/admin',
label: 'Admin',
isActive: location.pathname === '/admin'
}
];
return (
<div className="layout">
<nav className="navbar">
<Link to="/dashboard" className="nav-brand" onClick={closeMobileMenu}>
<h1>LibreBooking UI</h1>
</Link>
{/* Mobile Menu Button */}
<button
className="mobile-menu-btn"
onClick={toggleMobileMenu}
aria-label="Toggle navigation menu"
aria-expanded={isMobileMenuOpen}
>
<span className="hamburger-line"></span>
<span className="hamburger-line"></span>
<span className="hamburger-line"></span>
</button>
{/* Desktop Navigation */}
<ul className="nav-links nav-links-desktop">
{navLinks.map((link) => (
<li key={link.to}>
<Link
to={link.to}
className={link.isActive ? 'active' : ''}
>
{link.label}
</Link>
</li>
))}
</ul>
{/* Mobile Navigation */}
{isMobileMenuOpen && (
<div className="mobile-menu open">
<div className="mobile-menu-backdrop" onClick={closeMobileMenu}></div>
<ul className="nav-links nav-links-mobile">
{navLinks.map((link) => (
<li key={link.to}>
<Link
to={link.to}
className={link.isActive ? 'active' : ''}
onClick={closeMobileMenu}
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
)}
</nav>
<main className="main-content">
{children}
</main>
</div>
);
};
export default Layout;