338 lines
10 KiB
TypeScript
338 lines
10 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from 'react-router-dom';
|
|
import { SidebarComponent } from '@syncfusion/ej2-react-navigations';
|
|
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
|
|
import { TooltipComponent } from '@syncfusion/ej2-react-popups';
|
|
import logo from './assets/logo.png';
|
|
import './App.css';
|
|
|
|
// Lucide Icons importieren
|
|
import {
|
|
LayoutDashboard,
|
|
Calendar,
|
|
Boxes,
|
|
Image,
|
|
User,
|
|
Settings,
|
|
Monitor,
|
|
MonitorDotIcon,
|
|
LogOut,
|
|
Wrench,
|
|
Info,
|
|
} from 'lucide-react';
|
|
import { ToastProvider } from './components/ToastProvider';
|
|
|
|
const sidebarItems = [
|
|
{ name: 'Dashboard', path: '/', icon: LayoutDashboard },
|
|
{ name: 'Termine', path: '/termine', icon: Calendar },
|
|
{ name: 'Ressourcen', path: '/ressourcen', icon: Boxes },
|
|
{ name: 'Raumgruppen', path: '/infoscr_groups', icon: MonitorDotIcon },
|
|
{ name: 'Infoscreen-Clients', path: '/clients', icon: Monitor },
|
|
{ name: 'Erweiterungsmodus', path: '/setup', icon: Wrench },
|
|
{ name: 'Medien', path: '/medien', icon: Image },
|
|
{ name: 'Benutzer', path: '/benutzer', icon: User },
|
|
{ name: 'Einstellungen', path: '/einstellungen', icon: Settings },
|
|
{ name: 'Programminfo', path: '/programminfo', icon: Info },
|
|
];
|
|
|
|
// Dummy Components (können in eigene Dateien ausgelagert werden)
|
|
import Dashboard from './dashboard';
|
|
import Appointments from './appointments';
|
|
import Ressourcen from './ressourcen';
|
|
import Infoscreens from './clients';
|
|
import Infoscreen_groups from './infoscreen_groups';
|
|
import Media from './media';
|
|
import Benutzer from './benutzer';
|
|
import Einstellungen from './einstellungen';
|
|
import SetupMode from './SetupMode';
|
|
import Programminfo from './programminfo';
|
|
import Logout from './logout';
|
|
|
|
// ENV aus .env holen (Platzhalter, im echten Projekt über process.env oder API)
|
|
// const ENV = import.meta.env.VITE_ENV || 'development';
|
|
|
|
const Layout: React.FC = () => {
|
|
const [version, setVersion] = useState('');
|
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
let sidebarRef: SidebarComponent | null;
|
|
|
|
React.useEffect(() => {
|
|
fetch('/program-info.json')
|
|
.then(res => res.json())
|
|
.then(data => setVersion(data.version))
|
|
.catch(err => console.error('Failed to load version info:', err));
|
|
}, []);
|
|
|
|
const toggleSidebar = () => {
|
|
if (sidebarRef) {
|
|
sidebarRef.toggle();
|
|
}
|
|
};
|
|
|
|
const onSidebarChange = () => {
|
|
// Syncfusion unterscheidet zwischen isOpen (true/false) und dem Dock-Modus
|
|
// Im Dock-Modus ist isOpen=true, aber die Sidebar ist kollabiert
|
|
const sidebar = sidebarRef?.element;
|
|
if (sidebar) {
|
|
const currentWidth = sidebar.style.width;
|
|
const newCollapsedState = currentWidth === '60px' || currentWidth.includes('60');
|
|
|
|
setIsCollapsed(newCollapsedState);
|
|
}
|
|
};
|
|
|
|
const sidebarTemplate = () => (
|
|
<div
|
|
className={`sidebar-theme ${isCollapsed ? 'collapsed' : 'expanded'}`}
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
height: '100vh',
|
|
minHeight: '100vh',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
borderColor: 'var(--sidebar-border)',
|
|
height: '68px',
|
|
flexShrink: 0,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderBottom: '1px solid var(--sidebar-border)',
|
|
margin: 0,
|
|
padding: 0,
|
|
}}
|
|
>
|
|
<img
|
|
src={logo}
|
|
alt="Logo"
|
|
style={{
|
|
height: '64px',
|
|
maxHeight: '60px',
|
|
display: 'block',
|
|
margin: '0 auto',
|
|
}}
|
|
/>
|
|
</div>
|
|
<nav
|
|
style={{
|
|
flex: '1 1 auto',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
marginTop: '1rem',
|
|
overflowY: 'auto',
|
|
minHeight: 0, // Wichtig für Flex-Shrinking
|
|
}}
|
|
>
|
|
{sidebarItems.map(item => {
|
|
const Icon = item.icon;
|
|
const linkContent = (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
className="sidebar-link no-underline w-full"
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'flex-start',
|
|
gap: '8px',
|
|
padding: '12px 24px',
|
|
transition: 'background 0.2s, color 0.2s, justify-content 0.3s',
|
|
textDecoration: 'none',
|
|
color: 'var(--sidebar-fg)',
|
|
backgroundColor: 'var(--sidebar-bg)',
|
|
}}
|
|
>
|
|
<Icon size={22} style={{ flexShrink: 0, marginRight: 0 }} />
|
|
<span className="sidebar-text" style={{ marginLeft: 0, transition: 'opacity 0.3s' }}>
|
|
{item.name}
|
|
</span>
|
|
</Link>
|
|
);
|
|
|
|
// Syncfusion Tooltip nur im collapsed state
|
|
return isCollapsed ? (
|
|
<TooltipComponent
|
|
key={item.path}
|
|
content={item.name}
|
|
position="RightCenter"
|
|
opensOn="Hover"
|
|
showTipPointer={true}
|
|
animation={{
|
|
open: { effect: 'FadeIn', duration: 200 },
|
|
close: { effect: 'FadeOut', duration: 200 },
|
|
}}
|
|
>
|
|
{linkContent}
|
|
</TooltipComponent>
|
|
) : (
|
|
linkContent
|
|
);
|
|
})}
|
|
</nav>
|
|
<div
|
|
style={{
|
|
flexShrink: 0,
|
|
marginTop: 'auto',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
minHeight: 'auto',
|
|
}}
|
|
>
|
|
{(() => {
|
|
const logoutContent = (
|
|
<Link
|
|
to="/logout"
|
|
className="sidebar-logout no-underline w-full"
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'flex-start',
|
|
gap: '8px',
|
|
padding: '12px 24px',
|
|
transition: 'background 0.2s, color 0.2s, justify-content 0.3s',
|
|
textDecoration: 'none',
|
|
color: 'var(--sidebar-fg)',
|
|
backgroundColor: 'var(--sidebar-bg)',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
fontSize: '1.15rem',
|
|
}}
|
|
>
|
|
<LogOut size={22} style={{ flexShrink: 0, marginRight: 0 }} />
|
|
<span className="sidebar-text" style={{ marginLeft: 0, transition: 'opacity 0.3s' }}>
|
|
Abmelden
|
|
</span>
|
|
</Link>
|
|
);
|
|
|
|
// Syncfusion Tooltip nur im collapsed state
|
|
return isCollapsed ? (
|
|
<TooltipComponent
|
|
content="Abmelden"
|
|
position="RightCenter"
|
|
opensOn="Hover"
|
|
showTipPointer={true}
|
|
animation={{
|
|
open: { effect: 'FadeIn', duration: 200 },
|
|
close: { effect: 'FadeOut', duration: 200 },
|
|
}}
|
|
>
|
|
{logoutContent}
|
|
</TooltipComponent>
|
|
) : (
|
|
logoutContent
|
|
);
|
|
})()}
|
|
{version && (
|
|
<div
|
|
className="version-info px-6 py-2 text-xs text-center opacity-70 border-t"
|
|
style={{ borderColor: 'var(--sidebar-border)' }}
|
|
>
|
|
Version {version}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="layout-container">
|
|
<SidebarComponent
|
|
id="sidebar"
|
|
ref={(sidebar: SidebarComponent | null) => {
|
|
sidebarRef = sidebar;
|
|
}}
|
|
width="256px"
|
|
target=".layout-container"
|
|
isOpen={true}
|
|
closeOnDocumentClick={false}
|
|
enableGestures={false}
|
|
type="Auto"
|
|
enableDock={true}
|
|
dockSize="60px"
|
|
change={onSidebarChange}
|
|
>
|
|
{sidebarTemplate()}
|
|
</SidebarComponent>
|
|
|
|
<div className="content-area">
|
|
<header
|
|
className="content-header flex items-center shadow"
|
|
style={{
|
|
backgroundColor: '#e5d8c7',
|
|
color: '#78591c',
|
|
height: '68px', // Exakt gleiche Höhe wie Sidebar-Header
|
|
fontSize: '1.15rem',
|
|
fontFamily:
|
|
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
|
|
margin: 0,
|
|
padding: '0 2rem 0 0', // Nur rechts Padding, links kein Padding
|
|
boxSizing: 'border-box',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<ButtonComponent
|
|
cssClass="e-inherit"
|
|
iconCss="e-icons e-menu"
|
|
onClick={toggleSidebar}
|
|
isToggle={true}
|
|
style={{
|
|
margin: '0 1rem 0 0', // Nur rechts Margin für Abstand zum Logo
|
|
padding: '8px 12px',
|
|
minWidth: '44px',
|
|
height: '44px',
|
|
flexShrink: 0,
|
|
}}
|
|
/>
|
|
<img src={logo} alt="Logo" className="h-16 mr-4" style={{ maxHeight: '60px' }} />
|
|
<span className="text-2xl font-bold mr-8" style={{ color: '#78591c' }}>
|
|
Infoscreen-Management
|
|
</span>
|
|
<span className="ml-auto text-lg font-medium" style={{ color: '#78591c' }}>
|
|
[Organisationsname]
|
|
</span>
|
|
</header>
|
|
<main className="page-content">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const App: React.FC = () => {
|
|
// Automatische Navigation zu /clients bei leerer Beschreibung entfernt
|
|
|
|
return (
|
|
<ToastProvider>
|
|
<Routes>
|
|
<Route path="/" element={<Layout />}>
|
|
<Route index element={<Dashboard />} />
|
|
<Route path="termine" element={<Appointments />} />
|
|
<Route path="ressourcen" element={<Ressourcen />} />
|
|
<Route path="infoscr_groups" element={<Infoscreen_groups />} />
|
|
<Route path="medien" element={<Media />} />
|
|
<Route path="benutzer" element={<Benutzer />} />
|
|
<Route path="einstellungen" element={<Einstellungen />} />
|
|
<Route path="clients" element={<Infoscreens />} />
|
|
<Route path="setup" element={<SetupMode />} />
|
|
<Route path="programminfo" element={<Programminfo />} />
|
|
</Route>
|
|
<Route path="/logout" element={<Logout />} />
|
|
</Routes>
|
|
</ToastProvider>
|
|
);
|
|
};
|
|
|
|
const AppWrapper: React.FC = () => (
|
|
<Router>
|
|
<App />
|
|
</Router>
|
|
);
|
|
|
|
export default AppWrapper;
|