infoscreen-overview in cards

This commit is contained in:
2025-06-23 16:06:00 +00:00
parent 76f6baf533
commit 6e38ca477a
18 changed files with 476 additions and 124 deletions

1
.gitignore vendored
View File

@@ -73,3 +73,4 @@ dashboard/pages/test.py
dashboard/sidebar_test.py dashboard/sidebar_test.py
dashboard/assets/responsive-sidebar.css dashboard/assets/responsive-sidebar.css
certs/ certs/
sync.ffs_db

View File

@@ -13,8 +13,10 @@
"@syncfusion/ej2-react-grids": "^29.2.11", "@syncfusion/ej2-react-grids": "^29.2.11",
"@syncfusion/ej2-react-schedule": "^29.2.10", "@syncfusion/ej2-react-schedule": "^29.2.10",
"cldr-data": "^36.0.4", "cldr-data": "^36.0.4",
"lucide-react": "^0.522.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0",
"react-router-dom": "^7.6.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",
@@ -23,6 +25,7 @@
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@types/react-router-dom": "^5.3.3",
"@typescript-eslint/eslint-plugin": "^8.34.1", "@typescript-eslint/eslint-plugin": "^8.34.1",
"@typescript-eslint/parser": "^8.34.1", "@typescript-eslint/parser": "^8.34.1",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",
@@ -1985,6 +1988,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2012,6 +2022,29 @@
"@types/react": "^19.0.0" "@types/react": "^19.0.0"
} }
}, },
"node_modules/@types/react-router": {
"version": "5.1.20",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.34.1", "version": "8.34.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
@@ -3038,6 +3071,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
@@ -5357,6 +5399,15 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lucide-react": {
"version": "0.522.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.522.0.tgz",
"integrity": "sha512-jnJbw974yZ7rQHHEFKJOlWAefG3ATSCZHANZxIdx8Rk/16siuwjgA4fBULpXEAWx/RlTs3FzmKW/udWUuO0aRw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -6267,6 +6318,44 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz",
"integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz",
"integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==",
"license": "MIT",
"dependencies": {
"react-router": "7.6.2"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -6521,6 +6610,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",

View File

@@ -15,8 +15,10 @@
"@syncfusion/ej2-react-grids": "^29.2.11", "@syncfusion/ej2-react-grids": "^29.2.11",
"@syncfusion/ej2-react-schedule": "^29.2.10", "@syncfusion/ej2-react-schedule": "^29.2.10",
"cldr-data": "^36.0.4", "cldr-data": "^36.0.4",
"lucide-react": "^0.522.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0",
"react-router-dom": "^7.6.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",
@@ -25,6 +27,7 @@
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@types/react-router-dom": "^5.3.3",
"@typescript-eslint/eslint-plugin": "^8.34.1", "@typescript-eslint/eslint-plugin": "^8.34.1",
"@typescript-eslint/parser": "^8.34.1", "@typescript-eslint/parser": "^8.34.1",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",

View File

@@ -1,113 +1,156 @@
// import 'react-app-polyfill/ie11'; // optional, falls benötigt import React, { useState } from 'react';
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from 'react-router-dom';
import logo from './assets/logo.png';
import './App.css'; import './App.css';
import React from 'react';
// Lucide Icons importieren
import { import {
ScheduleComponent, LayoutDashboard,
Day, Calendar,
Week, Boxes,
WorkWeek, Users,
Month, UserSquare,
Agenda, Image,
TimelineViews, User,
TimelineMonth, Settings,
Inject, } from 'lucide-react';
ViewsDirective,
ViewDirective,
ResourcesDirective,
ResourceDirective,
} from '@syncfusion/ej2-react-schedule';
import { L10n, loadCldr, setCulture } from '@syncfusion/ej2-base';
import * as de from 'cldr-data/main/de/ca-gregorian.json';
import * as numbers from 'cldr-data/main/de/numbers.json';
import * as timeZoneNames from 'cldr-data/main/de/timeZoneNames.json';
import * as numberingSystems from 'cldr-data/supplemental/numberingSystems.json';
// CLDR-Daten laden const sidebarItems = [
loadCldr( { name: 'Dashboard', path: '/', icon: LayoutDashboard },
(de as unknown as { default: object }).default, { name: 'Termine', path: '/termine', icon: Calendar },
(numbers as unknown as { default: object }).default, { name: 'Ressourcen', path: '/ressourcen', icon: Boxes },
(timeZoneNames as unknown as { default: object }).default, { name: 'Infoscreens', path: '/Infoscreens', icon: Users },
(numberingSystems as unknown as { default: object }).default { name: 'Gruppen', path: '/gruppen', icon: UserSquare },
); { name: 'Medien', path: '/medien', icon: Image },
{ name: 'Benutzer', path: '/benutzer', icon: User },
// Deutsche Lokalisierung für den Scheduler { name: 'Einstellungen', path: '/einstellungen', icon: Settings },
L10n.load({
de: {
schedule: {
day: 'Tag',
week: 'Woche',
workWeek: 'Arbeitswoche',
month: 'Monat',
agenda: 'Agenda',
today: 'Heute',
noEvents: 'Keine Termine',
},
},
});
// Kultur setzen
setCulture('de');
// Ressourcen-Daten
const resources = [
{ text: 'Raum A', id: 1, color: '#1aaa55' },
{ text: 'Raum B', id: 2, color: '#357cd2' },
{ text: 'Raum C', id: 3, color: '#7fa900' },
]; ];
// Dummy-Termine generieren const Layout: React.FC = () => {
const now = new Date(); const [collapsed, setCollapsed] = useState(false);
const appointments = Array.from({ length: 10 }).map((_, i) => {
const dayOffset = Math.floor(i * 1.4); // verteilt auf 14 Tage
const start = new Date(now);
start.setDate(now.getDate() + dayOffset);
start.setHours(9 + (i % 4), 0, 0, 0);
const end = new Date(start);
end.setHours(start.getHours() + 1);
return {
Id: i + 1,
Subject: `Termin ${i + 1}`,
StartTime: start,
EndTime: end,
ResourceId: (i % 3) + 1,
Location: resources[i % 3].text,
};
});
const App: React.FC = () => {
return ( return (
<div className="p-8 bg-gray-100 min-h-screen"> <div className="flex min-h-screen">
<h1 className="text-2xl font-bold mb-4">Infoscreen Kalendersteuerung</h1> {/* Sidebar */}
<ScheduleComponent <aside
height="650px" className={`flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-30'}`}
locale="de" style={{
currentView="TimelineWeek" backgroundColor: '#e5d8c7',
eventSettings={{ dataSource: appointments }} color: '#78591c',
group={{ resources: ['Räume'] }} fontSize: '1.15rem',
fontFamily:
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
}}
> >
<ViewsDirective> <div
<ViewDirective option="Week" /> className="h-20 flex items-center justify-center border-b"
<ViewDirective option="TimelineWeek" /> style={{ borderColor: '#d6c3a6' }}
<ViewDirective option="Month" /> >
<ViewDirective option="TimelineMonth" /> <img
</ViewsDirective> src={logo}
<ResourcesDirective> alt="Logo"
<ResourceDirective className="h-12"
field="ResourceId" style={{ display: collapsed ? 'none' : 'block' }}
title="Räume"
name="Räume"
allowMultiple={false}
dataSource={resources}
textField="text"
idField="id"
colorField="color"
/> />
</ResourcesDirective> </div>
<Inject services={[Day, Week, WorkWeek, Month, Agenda, TimelineViews, TimelineMonth]} /> <button
</ScheduleComponent> className="p-2 focus:outline-none hover:bg-[#d6c3a6] transition-colors"
onClick={() => setCollapsed(!collapsed)}
aria-label={collapsed ? 'Sidebar ausklappen' : 'Sidebar einklappen'}
>
<span style={{ fontSize: 20 }}>{collapsed ? '▶' : '◀'}</span>
</button>
<nav className="flex-1 mt-4">
{sidebarItems.map(item => {
const Icon = item.icon;
return (
<Link
key={item.path}
to={item.path}
className="flex items-center gap-3 px-6 py-3 transition-colors no-underline"
style={{
color: '#78591c',
textDecoration: 'none',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontWeight: 500,
}}
title={collapsed ? item.name : undefined}
onMouseEnter={e => {
e.currentTarget.style.backgroundColor = '#78591c';
e.currentTarget.style.color = '#e5d8c7';
}}
onMouseLeave={e => {
e.currentTarget.style.backgroundColor = '';
e.currentTarget.style.color = '#78591c';
}}
>
<Icon size={22} />
{!collapsed && item.name}
</Link>
);
})}
</nav>
</aside>
{/* Main Content */}
<div className="flex-1 flex flex-col">
{/* Header */}
<header
className="flex items-center px-8 shadow"
style={{
backgroundColor: '#e5d8c7',
color: '#78591c',
height: 'calc(48px + 20px)',
fontSize: '1.15rem',
fontFamily:
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
}}
>
<img
src={logo}
alt="Logo"
className="h-12 mr-4"
style={{ marginTop: 10, marginBottom: 10 }}
/>
<span className="text-2xl font-bold mr-8">Infoscreen-Management</span>
<span className="ml-auto" style={{ color: '#78591c' }}>
[Organisationsname]
</span>
</header>
<main className="flex-1 p-8 bg-gray-100">
<Outlet />
</main>
</div>
</div> </div>
); );
}; };
const App: React.FC = () => (
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Dashboard />} />
<Route path="termine" element={<Appointments />} />
<Route path="ressourcen" element={<Ressourcen />} />
<Route path="Infoscreens" element={<Infoscreens />} />
<Route path="gruppen" element={<Gruppen />} />
<Route path="medien" element={<Medien />} />
<Route path="benutzer" element={<Benutzer />} />
<Route path="einstellungen" element={<Einstellungen />} />
</Route>
</Routes>
</Router>
);
export default App; export default App;
// 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 Gruppen from './gruppen';
import Medien from './medien';
import Benutzer from './benutzer';
import Einstellungen from './einstellungen';

View File

@@ -0,0 +1,17 @@
// Funktion zum Laden der Clients von der API
export interface Client {
uuid: string;
location: string;
hardware_hash: string;
ip_address: string;
last_alive: string | null;
}
export async function fetchClients(): Promise<Client[]> {
const response = await fetch('/api/clients');
if (!response.ok) {
throw new Error('Fehler beim Laden der Clients');
}
return await response.json();
}

View File

@@ -0,0 +1,106 @@
import React from 'react';
import {
ScheduleComponent,
Day,
Week,
WorkWeek,
Month,
Agenda,
TimelineViews,
TimelineMonth,
Inject,
ViewsDirective,
ViewDirective,
ResourcesDirective,
ResourceDirective,
} from '@syncfusion/ej2-react-schedule';
import { L10n, loadCldr, setCulture } from '@syncfusion/ej2-base';
import * as de from 'cldr-data/main/de/ca-gregorian.json';
import * as numbers from 'cldr-data/main/de/numbers.json';
import * as timeZoneNames from 'cldr-data/main/de/timeZoneNames.json';
import * as numberingSystems from 'cldr-data/supplemental/numberingSystems.json';
// CLDR-Daten laden
loadCldr(
(de as unknown as { default: object }).default,
(numbers as unknown as { default: object }).default,
(timeZoneNames as unknown as { default: object }).default,
(numberingSystems as unknown as { default: object }).default
);
// Deutsche Lokalisierung für den Scheduler
L10n.load({
de: {
schedule: {
day: 'Tag',
week: 'Woche',
workWeek: 'Arbeitswoche',
month: 'Monat',
agenda: 'Agenda',
today: 'Heute',
noEvents: 'Keine Termine',
},
},
});
// Kultur setzen
setCulture('de');
// Ressourcen-Daten
const resources = [
{ text: 'Raum A', id: 1, color: '#1aaa55' },
{ text: 'Raum B', id: 2, color: '#357cd2' },
{ text: 'Raum C', id: 3, color: '#7fa900' },
];
// Dummy-Termine generieren
const now = new Date();
const appointments = Array.from({ length: 10 }).map((_, i) => {
const dayOffset = Math.floor(i * 1.4); // verteilt auf 14 Tage
const start = new Date(now);
start.setDate(now.getDate() + dayOffset);
start.setHours(9 + (i % 4), 0, 0, 0);
const end = new Date(start);
end.setHours(start.getHours() + 1);
return {
Id: i + 1,
Subject: `Termin ${i + 1}`,
StartTime: start,
EndTime: end,
ResourceId: (i % 3) + 1,
Location: resources[i % 3].text,
};
});
const Appointments: React.FC = () => (
<ScheduleComponent
height="650px"
locale="de"
currentView="TimelineWeek"
eventSettings={{ dataSource: appointments }}
group={{ resources: ['Räume'] }}
>
<ViewsDirective>
<ViewDirective option="Week" />
<ViewDirective option="TimelineWeek" />
<ViewDirective option="Month" />
<ViewDirective option="TimelineMonth" />
</ViewsDirective>
<ResourcesDirective>
<ResourceDirective
field="ResourceId"
title="Räume"
name="Räume"
allowMultiple={false}
dataSource={resources}
textField="text"
idField="id"
colorField="color"
/>
</ResourcesDirective>
<Inject services={[Day, Week, WorkWeek, Month, Agenda, TimelineViews, TimelineMonth]} />
</ScheduleComponent>
);
export default Appointments;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

View File

@@ -0,0 +1,8 @@
import React from 'react';
const Benutzer: React.FC = () => (
<div>
<h2 className="text-xl font-bold mb-4">Benutzer</h2>
<p>Willkommen im Infoscreen-Management Benutzer.</p>
</div>
);
export default Benutzer;

View File

@@ -0,0 +1,8 @@
import React from 'react';
const Infoscreens: React.FC = () => (
<div>
<h2 className="text-xl font-bold mb-4">Infoscreens</h2>
<p>Willkommen im Infoscreen-Management Infoscreens.</p>
</div>
);
export default Infoscreens;

View File

@@ -0,0 +1,46 @@
import React, { useEffect, useState } from 'react';
import { fetchClients } from './apiClients';
import type { Client } from './apiClients';
const Dashboard: React.FC = () => {
const [clients, setClients] = useState<Client[]>([]);
useEffect(() => {
fetchClients().then(setClients).catch(console.error);
}, []);
return (
<div>
<header className="mb-8 pb-4 border-b-2 border-[#d6c3a6]">
<h2 className="text-3xl font-extrabold mb-2">Dashboard</h2>
<p className="text-lg">Willkommen im Infoscreen-Management Dashboard.</p>
</header>
<h3 className="text-lg font-semibold mt-6 mb-4">Clients</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{clients.map(client => (
<div key={client.uuid} className="bg-white rounded shadow p-4 flex flex-col items-center">
<h4 className="text-lg font-bold mb-2">{client.location || 'Unbekannter Standort'}</h4>
<img
src={`/screenshots/${client.uuid}`}
alt={`Screenshot ${client.location}`}
className="w-full h-48 object-contain bg-gray-100 mb-2"
onError={e => (e.currentTarget.style.display = 'none')}
/>
<div className="text-sm text-gray-700 mb-1">
<span className="font-semibold">IP:</span> {client.ip_address}
</div>
<div className="text-sm text-gray-700">
<span className="font-semibold">Letztes Lebenszeichen:</span>{' '}
{client.last_alive ? new Date(client.last_alive).toLocaleString() : '-'}
</div>
</div>
))}
{clients.length === 0 && (
<div className="col-span-full text-center text-gray-400">Keine Clients gefunden.</div>
)}
</div>
</div>
);
};
export default Dashboard;

View File

@@ -0,0 +1,8 @@
import React from 'react';
const Einstellungen: React.FC = () => (
<div>
<h2 className="text-xl font-bold mb-4">Einstellungen</h2>
<p>Willkommen im Infoscreen-Management Einstellungen.</p>
</div>
);
export default Einstellungen;

View File

@@ -0,0 +1,8 @@
import React from 'react';
const Gruppen: React.FC = () => (
<div>
<h2 className="text-xl font-bold mb-4">Gruppen</h2>
<p>Willkommen im Infoscreen-Management Gruppen.</p>
</div>
);
export default Gruppen;

8
dashboard/src/medien.tsx Normal file
View File

@@ -0,0 +1,8 @@
import React from 'react';
const Medien: React.FC = () => (
<div>
<h2 className="text-xl font-bold mb-4">Medien</h2>
<p>Willkommen im Infoscreen-Management Medien.</p>
</div>
);
export default Medien;

View File

@@ -0,0 +1,8 @@
import React from 'react';
const Ressourcen: React.FC = () => (
<div>
<h2 className="text-xl font-bold mb-4">Ressourcen</h2>
<p>Willkommen im Infoscreen-Management Ressourcen.</p>
</div>
);
export default Ressourcen;

View File

View File

@@ -1,7 +1,13 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
}) server: {
proxy: {
'/api': 'http://localhost:8000',
'/screenshots': 'http://localhost:8000',
},
},
});

View File

@@ -3,34 +3,21 @@ http {
upstream dashboard { upstream dashboard {
server 127.0.0.1:3000; server 127.0.0.1:3000;
} }
upstream infoscreen_api {
server infoscreen-api:8000;
}
server { server {
listen 80; listen 80;
server_name _; server_name _;
# Optional: HTTP auf HTTPS weiterleiten # Leitet /api/ und /screenshots/ an den API-Server weiter
# return 301 https://$host$request_uri;
location / {
proxy_pass http://dashboard;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/nginx/certs/dev.crt;
ssl_certificate_key /etc/nginx/certs/dev.key;
location /api/ { location /api/ {
proxy_pass http://infoscreen-api:8000/api/; proxy_pass http://infoscreen_api/api/;
} }
location /screenshots/ { location /screenshots/ {
proxy_pass http://infoscreen-api:8000/screenshots/; proxy_pass http://infoscreen_api/screenshots/;
} }
# Alles andere geht ans Frontend
location / { location / {
proxy_pass http://dashboard; proxy_pass http://dashboard;
proxy_set_header Host $host; proxy_set_header Host $host;