UI: switch to Syncfusion M3, remove Tailwind;

paginate changelog; docs updated; bump to 2025.1.0-alpha.8
This commit is contained in:
RobbStarkAustria
2025-10-11 12:10:12 +00:00
parent 0601bac243
commit 4d807be6f8
11 changed files with 404 additions and 269 deletions

View File

@@ -1,7 +1,6 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-tailwindcss"
"stylelint-config-standard"
],
"rules": {
"at-rule-no-unknown": null

View File

@@ -45,9 +45,6 @@
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-router-dom": "^5.3.3",
@@ -66,8 +63,6 @@
"prettier": "^3.5.3",
"stylelint": "^16.21.0",
"stylelint-config-standard": "^38.0.0",
"stylelint-config-tailwindcss": "^1.0.0",
"tailwindcss": "^3.4.17",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"

View File

@@ -1,6 +1,5 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,6 +1,6 @@
{
"appName": "Infoscreen-Management",
"version": "2025.1.0-alpha.7",
"version": "2025.1.0-alpha.8",
"copyright": "© 2025 Third-Age-Applications",
"supportContact": "support@third-age-applications.com",
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
@@ -30,6 +30,17 @@
"commitId": "8d1df7199cb7"
},
"changelog": [
{
"version": "2025.1.0-alpha.8",
"date": "2025-10-11",
"changes": [
"🎨 Theme: Umstellung auf Syncfusion Material 3; zentrale CSS-Imports in main.tsx",
"🧹 Cleanup: Tailwind CSS komplett entfernt (Pakete, PostCSS, Stylelint, Konfigurationsdateien)",
"🧩 Gruppenverwaltung: \"infoscreen_groups\" auf Syncfusion-Komponenten (Buttons, Dialoge, DropDownList, TextBox) umgestellt; Abstände verbessert",
"🔔 Benachrichtigungen: Vereinheitlichte Toast-/Dialog-Texte; letzte Alert-Verwendung ersetzt",
"📖 Doku: README und Copilot-Anweisungen angepasst (Material 3, zentrale Styles, kein Tailwind)"
]
},
{
"version": "2025.1.0-alpha.7",
"date": "2025-09-21",

View File

@@ -1,19 +1,4 @@
@import "../node_modules/@syncfusion/ej2-base/styles/material.css";
@import "../node_modules/@syncfusion/ej2-buttons/styles/material.css";
@import "../node_modules/@syncfusion/ej2-calendars/styles/material.css";
@import "../node_modules/@syncfusion/ej2-dropdowns/styles/material.css";
@import "../node_modules/@syncfusion/ej2-inputs/styles/material.css";
@import "../node_modules/@syncfusion/ej2-lists/styles/material.css";
@import "../node_modules/@syncfusion/ej2-navigations/styles/material.css";
@import "../node_modules/@syncfusion/ej2-popups/styles/material.css";
@import "../node_modules/@syncfusion/ej2-splitbuttons/styles/material.css";
@import "../node_modules/@syncfusion/ej2-react-schedule/styles/material.css";
@import "../node_modules/@syncfusion/ej2-kanban/styles/material.css";
@import "../node_modules/@syncfusion/ej2-notifications/styles/material.css";
@import "../node_modules/@syncfusion/ej2-react-filemanager/styles/material.css";
@import "../node_modules/@syncfusion/ej2-layouts/styles/material.css";
@import "../node_modules/@syncfusion/ej2-grids/styles/material.css";
@import "../node_modules/@syncfusion/ej2-icons/styles/material.css";
/* Removed legacy Syncfusion material theme imports; using material3 imports in main.tsx */
body {
font-family: Inter, 'Segoe UI', Roboto, Arial, sans-serif;

View File

@@ -1,7 +1,4 @@
/* @tailwind base;
@tailwind components; */
/* @tailwind utilities; */
/* Tailwind removed: base/components/utilities directives no longer used. */
/* :root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;

View File

@@ -5,6 +5,11 @@ import { fetchGroups, createGroup, deleteGroup, renameGroup } from './apiGroups'
import type { Client } from './apiClients';
import type { KanbanComponent as KanbanComponentType } from '@syncfusion/ej2-react-kanban';
import { DialogComponent } from '@syncfusion/ej2-react-popups';
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns';
import type { ChangedEventArgs as TextBoxChangedArgs } from '@syncfusion/ej2-react-inputs';
import type { ChangeEventArgs as DropDownChangeArgs } from '@syncfusion/ej2-react-dropdowns';
import { useToast } from './components/ToastProvider';
import { L10n } from '@syncfusion/ej2-base';
@@ -41,10 +46,10 @@ const de = {
rename: 'Umbenennen',
confirmDelete: 'Löschbestätigung',
reallyDelete: (name: string) => `Möchten Sie die Gruppe <b>${name}</b> wirklich löschen?`,
clientsMoved: 'Alle Clients werden in "Nicht zugeordnet" verschoben.',
clientsMoved: 'Alle Clients werden nach "Nicht zugeordnet" verschoben.',
groupCreated: 'Gruppe angelegt',
groupDeleted: 'Gruppe gelöscht. Clients in "Nicht zugeordnet" verschoben',
groupRenamed: 'Gruppenname geändert',
groupDeleted: 'Gruppe gelöscht. Alle Clients wurden nach "Nicht zugeordnet" verschoben.',
groupRenamed: 'Gruppe umbenannt',
selectGroup: 'Gruppe wählen',
newName: 'Neuer Name',
warning: 'Achtung:',
@@ -312,7 +317,12 @@ const Infoscreen_groups: React.FC = () => {
});
});
} catch {
alert('Fehler beim Aktualisieren der Clients');
toast.show({
content: 'Fehler beim Aktualisieren der Clients',
cssClass: 'e-toast-danger',
timeOut: 0,
showCloseButton: true,
});
}
setDraggedCard(null);
};
@@ -326,25 +336,23 @@ const Infoscreen_groups: React.FC = () => {
return (
<div id="dialog-target">
<h2 className="text-xl font-bold mb-4">{de.title}</h2>
<div className="flex gap-2 mb-4">
<button
className="px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => setShowDialog(true)}
>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '12px',
marginBottom: '16px',
}}
>
<ButtonComponent cssClass="e-primary" onClick={() => setShowDialog(true)}>
{de.newGroup}
</button>
<button
className="px-4 py-2 bg-yellow-500 text-white rounded"
onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}
>
</ButtonComponent>
<ButtonComponent cssClass="e-warning" onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}>
{de.renameGroup}
</button>
<button
className="px-4 py-2 bg-red-500 text-white rounded"
onClick={() => setDeleteDialog({ open: true, groupName: '' })}
>
</ButtonComponent>
<ButtonComponent cssClass="e-danger" onClick={() => setDeleteDialog({ open: true, groupName: '' })}>
{de.deleteGroup}
</button>
</ButtonComponent>
</div>
<KanbanComponent
locale="de"
@@ -362,155 +370,155 @@ const Infoscreen_groups: React.FC = () => {
columns={kanbanColumns}
/>
{showDialog && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
<div className="bg-white p-6 rounded shadow">
<h3 className="mb-2 font-bold">{de.newGroup}</h3>
<input
className="border p-2 mb-2 w-full"
value={newGroupName}
onChange={e => setNewGroupName(e.target.value)}
placeholder="Raumname"
/>
<div className="flex gap-2">
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={handleAddGroup}>
<DialogComponent
visible={showDialog}
header={de.newGroup}
close={() => setShowDialog(false)}
target="#dialog-target"
width="420px"
footerTemplate={() => (
<div className="flex gap-2 justify-end">
<ButtonComponent cssClass="e-primary" onClick={handleAddGroup} disabled={!newGroupName.trim()}>
{de.add}
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => setShowDialog(false)}
>
{de.cancel}
</button>
</ButtonComponent>
<ButtonComponent onClick={() => setShowDialog(false)}>{de.cancel}</ButtonComponent>
</div>
)}
>
<div className="mt-2">
<TextBoxComponent
value={newGroupName}
placeholder="Raumname"
floatLabelType="Auto"
change={(args: TextBoxChangedArgs) => setNewGroupName(String(args.value ?? ''))}
/>
</div>
</div>
</DialogComponent>
)}
{renameDialog.open && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
<div className="bg-white p-6 rounded shadow">
<h3 className="mb-2 font-bold">{de.renameGroup}</h3>
<select
className="border p-2 mb-2 w-full"
value={renameDialog.oldName}
onChange={e =>
setRenameDialog({
...renameDialog,
oldName: e.target.value,
newName: e.target.value,
})
}
>
<option value="">{de.selectGroup}</option>
{groups
.filter(g => g.headerText !== 'Nicht zugeordnet')
.map(g => (
<option key={g.keyField} value={g.headerText}>
{g.headerText}
</option>
))}
</select>
<input
className="border p-2 mb-2 w-full"
value={renameDialog.newName}
onChange={e => setRenameDialog({ ...renameDialog, newName: e.target.value })}
placeholder={de.newName}
/>
<div className="flex gap-2">
<button
className="bg-blue-500 text-white px-4 py-2 rounded"
<DialogComponent
visible={renameDialog.open}
header={de.renameGroup}
showCloseIcon={true}
close={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
target="#dialog-target"
width="480px"
footerTemplate={() => (
<div className="flex gap-2 justify-end">
<ButtonComponent
cssClass="e-primary"
onClick={handleRenameGroup}
disabled={!renameDialog.oldName || !renameDialog.newName}
>
{de.rename}
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
>
</ButtonComponent>
<ButtonComponent onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}>
{de.cancel}
</button>
</ButtonComponent>
</div>
)}
>
<div className="flex flex-col gap-3 mt-2">
<DropDownListComponent
placeholder={de.selectGroup}
dataSource={groups.filter(g => g.headerText !== 'Nicht zugeordnet').map(g => g.headerText)}
value={renameDialog.oldName}
change={(e: DropDownChangeArgs) =>
setRenameDialog({
...renameDialog,
oldName: String(e.value ?? ''),
newName: String(e.value ?? ''),
})
}
/>
<TextBoxComponent
placeholder={de.newName}
value={renameDialog.newName}
floatLabelType="Auto"
change={(args: TextBoxChangedArgs) =>
setRenameDialog({ ...renameDialog, newName: String(args.value ?? '') })
}
/>
</div>
</div>
</DialogComponent>
)}
{deleteDialog.open && (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
<div className="bg-white p-6 rounded shadow">
<h3 className="mb-2 font-bold">{de.deleteGroup}</h3>
<select
className="border p-2 mb-2 w-full"
value={deleteDialog.groupName}
onChange={e => setDeleteDialog({ ...deleteDialog, groupName: e.target.value })}
>
<option value="">{de.selectGroup}</option>
{groups
.filter(g => g.headerText !== 'Nicht zugeordnet')
.map(g => (
<option key={g.keyField} value={g.headerText}>
{g.headerText}
</option>
))}
</select>
<p>{de.clientsMoved}</p>
{deleteDialog.groupName && (
<div className="bg-yellow-100 text-yellow-800 p-2 rounded mb-2 text-sm">
<strong>{de.warning}</strong> Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b>{' '}
wirklich löschen?
</div>
)}
<div className="flex gap-2 mt-2">
<button
className="bg-red-500 text-white px-4 py-2 rounded"
<DialogComponent
visible={deleteDialog.open}
header={de.deleteGroup}
showCloseIcon={true}
close={() => setDeleteDialog({ open: false, groupName: '' })}
target="#dialog-target"
width="520px"
footerTemplate={() => (
<div className="flex gap-2 justify-end">
<ButtonComponent
cssClass="e-danger"
onClick={() => setShowDeleteConfirm(true)}
disabled={!deleteDialog.groupName}
>
{de.deleteGroup}
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => setDeleteDialog({ open: false, groupName: '' })}
</ButtonComponent>
<ButtonComponent onClick={() => setDeleteDialog({ open: false, groupName: '' })}>
{de.cancel}
</ButtonComponent>
</div>
)}
>
<div className="flex flex-col gap-3 mt-2">
<DropDownListComponent
placeholder={de.selectGroup}
dataSource={groups.filter(g => g.headerText !== 'Nicht zugeordnet').map(g => g.headerText)}
value={deleteDialog.groupName}
change={(e: DropDownChangeArgs) =>
setDeleteDialog({ ...deleteDialog, groupName: String(e.value ?? '') })
}
/>
<p className="text-sm text-gray-600">{de.clientsMoved}</p>
{deleteDialog.groupName && (
<div className="bg-yellow-100 text-yellow-800 p-2 rounded text-sm">
<strong>{de.warning}</strong> Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b> wirklich löschen?
</div>
)}
</div>
</DialogComponent>
)}
{showDeleteConfirm && deleteDialog.groupName && (
<DialogComponent
width="380px"
header={de.confirmDelete}
visible={showDeleteConfirm}
showCloseIcon={true}
close={() => setShowDeleteConfirm(false)}
target="#dialog-target"
footerTemplate={() => (
<div className="flex gap-2 justify-end">
<ButtonComponent
cssClass="e-danger"
onClick={() => {
handleDeleteGroup(deleteDialog.groupName!);
setShowDeleteConfirm(false);
}}
>
{de.yesDelete}
</ButtonComponent>
<ButtonComponent
onClick={() => {
setShowDeleteConfirm(false);
setDeleteDialog({ open: false, groupName: '' });
}}
>
{de.cancel}
</button>
</ButtonComponent>
</div>
</div>
{showDeleteConfirm && deleteDialog.groupName && (
<DialogComponent
width="350px"
header={de.confirmDelete}
visible={showDeleteConfirm}
close={() => setShowDeleteConfirm(false)}
footerTemplate={() => (
<div className="flex gap-2 justify-end">
<button
className="bg-red-500 text-white px-4 py-2 rounded"
onClick={() => {
handleDeleteGroup(deleteDialog.groupName);
setShowDeleteConfirm(false);
}}
>
{de.yesDelete}
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
onClick={() => {
setShowDeleteConfirm(false);
setDeleteDialog({ open: false, groupName: '' });
}}
>
{de.cancel}
</button>
</div>
)}
>
<div>
Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b> wirklich löschen?
<br />
<span className="text-sm text-gray-500">{de.clientsMoved}</span>
</div>
</DialogComponent>
)}
</div>
>
<div>
Möchten Sie die Gruppe <b>{deleteDialog.groupName}</b> wirklich löschen?
<br />
<span className="text-sm text-gray-500">{de.clientsMoved}</span>
</div>
</DialogComponent>
)}
</div>
);

View File

@@ -6,6 +6,20 @@ import { registerLicense } from '@syncfusion/ej2-base';
import '@syncfusion/ej2-base/styles/material3.css';
import '@syncfusion/ej2-navigations/styles/material3.css';
import '@syncfusion/ej2-buttons/styles/material3.css';
import '@syncfusion/ej2-inputs/styles/material3.css';
import '@syncfusion/ej2-dropdowns/styles/material3.css';
import '@syncfusion/ej2-popups/styles/material3.css';
import '@syncfusion/ej2-kanban/styles/material3.css';
// Additional components used across the app
import '@syncfusion/ej2-grids/styles/material3.css';
import '@syncfusion/ej2-react-schedule/styles/material3.css';
import '@syncfusion/ej2-react-filemanager/styles/material3.css';
import '@syncfusion/ej2-notifications/styles/material3.css';
import '@syncfusion/ej2-layouts/styles/material3.css';
import '@syncfusion/ej2-lists/styles/material3.css';
import '@syncfusion/ej2-calendars/styles/material3.css';
import '@syncfusion/ej2-splitbuttons/styles/material3.css';
import '@syncfusion/ej2-icons/styles/material3.css';
// Setze hier deinen Lizenzschlüssel ein
registerLicense(

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { PagerComponent } from '@syncfusion/ej2-react-grids';
interface ProgramInfo {
appName: string;
@@ -6,9 +7,7 @@ interface ProgramInfo {
copyright: string;
supportContact: string;
description: string;
techStack: {
[key: string]: string;
};
techStack: Record<string, string>;
openSourceComponents: {
frontend: { name: string; license: string }[];
backend: { name: string; license: string }[];
@@ -27,26 +26,32 @@ interface ProgramInfo {
const Programminfo: React.FC = () => {
const [info, setInfo] = useState<ProgramInfo | null>(null);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState<number>(1);
const pageSize = 5;
useEffect(() => {
let isMounted = true;
fetch('/program-info.json')
.then(response => {
if (!response.ok) {
throw new Error('Netzwerk-Antwort war nicht ok');
}
return response.json();
.then(res => {
if (!res.ok) throw new Error('Netzwerk-Antwort war nicht ok');
return res.json();
})
.then(data => setInfo(data))
.catch(error => {
console.error('Fehler beim Laden der Programminformationen:', error);
setError('Informationen konnten nicht geladen werden.');
.then((data: ProgramInfo) => {
if (isMounted) setInfo(data);
})
.catch(err => {
console.error('Fehler beim Laden der Programminformationen:', err);
if (isMounted) setError('Informationen konnten nicht geladen werden.');
});
return () => {
isMounted = false;
};
}, []);
if (error) {
return (
<div>
<h2 className="text-xl font-bold mb-4 text-red-600">Fehler</h2>
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, marginBottom: '1rem', color: '#dc2626' }}>Fehler</h2>
<p>{error}</p>
</div>
);
@@ -55,112 +60,178 @@ const Programminfo: React.FC = () => {
if (!info) {
return (
<div>
<h2 className="text-xl font-bold mb-4">Programminfo</h2>
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, marginBottom: '1rem' }}>Programminfo</h2>
<p>Lade Informationen...</p>
</div>
);
}
const monoFont = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
return (
<div className="space-y-8">
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<div>
<h2 className="text-2xl font-bold mb-2">{info.appName}</h2>
<p className="text-gray-600">{info.description}</p>
<h2 style={{ fontSize: '1.75rem', fontWeight: 700, marginBottom: '0.5rem' }}>{info.appName}</h2>
<p style={{ color: '#4b5563' }}>{info.description}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2rem' }}>
{/* Allgemeine Infos & Build */}
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-xl font-semibold mb-4 border-b pb-2">Allgemein</h3>
<div className="space-y-3">
<p>
<strong>Version:</strong> {info.version}
</p>
<p>
<strong>Copyright:</strong> {info.copyright}
</p>
<p>
<strong>Support:</strong>{' '}
<a href={`mailto:${info.supportContact}`} className="text-blue-600 hover:underline">
{info.supportContact}
</a>
</p>
<hr className="my-4" />
<h4 className="font-semibold">Build-Informationen</h4>
<p>
<strong>Build-Datum:</strong>{' '}
{new Date(info.buildInfo.buildDate).toLocaleString('de-DE')}
</p>
<p>
<strong>Commit-ID:</strong>{' '}
<span className="font-mono text-sm bg-gray-100 p-1 rounded">
{info.buildInfo.commitId}
</span>
</p>
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
<div className="e-card">
<div className="e-card-header">
<div className="e-card-header-caption">
<div className="e-card-title">Allgemein</div>
</div>
</div>
<div className="e-card-content">
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<p>
<strong>Version:</strong> {info.version}
</p>
<p>
<strong>Copyright:</strong> {info.copyright}
</p>
<p>
<strong>Support:</strong>{' '}
<a href={`mailto:${info.supportContact}`} style={{ color: '#2563eb', textDecoration: 'none' }}>
{info.supportContact}
</a>
</p>
<hr style={{ margin: '1rem 0' }} />
<h4 style={{ fontWeight: 600 }}>Build-Informationen</h4>
<p>
<strong>Build-Datum:</strong> {new Date(info.buildInfo.buildDate).toLocaleString('de-DE')}
</p>
<p>
<strong>Commit-ID:</strong>{' '}
<span style={{ fontFamily: monoFont, fontSize: '0.875rem', background: '#f3f4f6', padding: '0.125rem 0.25rem', borderRadius: '0.25rem' }}>
{info.buildInfo.commitId}
</span>
</p>
</div>
</div>
</div>
</div>
{/* Technischer Stack */}
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-xl font-semibold mb-4 border-b pb-2">Technologie-Stack</h3>
<ul className="list-disc list-inside space-y-2">
{Object.entries(info.techStack).map(([key, value]) => (
<li key={key}>
<span className="font-semibold capitalize">{key}:</span> {value}
</li>
))}
</ul>
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
<div className="e-card">
<div className="e-card-header">
<div className="e-card-header-caption">
<div className="e-card-title">Technologie-Stack</div>
</div>
</div>
<div className="e-card-content">
<ul style={{ paddingLeft: '1rem', margin: 0, display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{Object.entries(info.techStack).map(([key, value]) => (
<li key={key}>
<span style={{ fontWeight: 600, textTransform: 'capitalize' }}>{key}:</span> {value}
</li>
))}
</ul>
</div>
</div>
</div>
</div>
{/* Changelog */}
<div>
<h3 className="text-xl font-semibold mb-4">Änderungsprotokoll (Changelog)</h3>
<div className="space-y-6">
{info.changelog.map(log => (
<div key={log.version} className="bg-white p-6 rounded-lg shadow">
<h4 className="font-bold text-lg mb-2">
Version {log.version}{' '}
<span className="text-sm font-normal text-gray-500">
- {new Date(log.date).toLocaleDateString('de-DE')}
</span>
</h4>
<ul className="list-disc list-inside space-y-1 text-gray-700">
{log.changes.map((change, index) => (
<li key={index}>{change}</li>
))}
</ul>
</div>
))}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '0.5rem' }}>
<h3 style={{ fontSize: '1.25rem', fontWeight: 600, margin: 0 }}>Änderungsprotokoll (Changelog)</h3>
<div style={{ marginLeft: 'auto' }}>
<span style={{ color: '#6b7280', fontSize: '0.875rem' }}>
Insgesamt {info.changelog.length} Einträge
</span>
</div>
</div>
<div style={{ marginBottom: '0.75rem' }}>
<PagerComponent
totalRecordsCount={info.changelog.length}
pageSize={pageSize}
pageCount={5}
currentPage={currentPage}
click={(args: { currentPage: number }) => setCurrentPage(args.currentPage)}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
{info.changelog
.slice((currentPage - 1) * pageSize, (currentPage - 1) * pageSize + pageSize)
.map(log => (
<div key={log.version} className="e-card">
<div className="e-card-header">
<div className="e-card-header-caption">
<div className="e-card-title">
Version {log.version}{' '}
<span style={{ fontSize: '0.875rem', fontWeight: 400, color: '#6b7280' }}>
- {new Date(log.date).toLocaleDateString('de-DE')}
</span>
</div>
</div>
</div>
<div className="e-card-content">
<ul style={{ color: '#374151', paddingLeft: '1rem', margin: 0, display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
{log.changes.map((change, index) => (
<li key={index}>{change}</li>
))}
</ul>
</div>
</div>
))}
</div>
<div style={{ marginTop: '0.75rem' }}>
<PagerComponent
totalRecordsCount={info.changelog.length}
pageSize={pageSize}
pageCount={5}
currentPage={currentPage}
click={(args: { currentPage: number }) => setCurrentPage(args.currentPage)}
/>
</div>
</div>
{/* Open Source Komponenten */}
<div>
<h3 className="text-xl font-semibold mb-4">Verwendete Open-Source-Komponenten</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<h3 style={{ fontSize: '1.25rem', fontWeight: 600, marginBottom: '1rem' }}>Verwendete Open-Source-Komponenten</h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2rem' }}>
{info.openSourceComponents.frontend && (
<div className="bg-white p-6 rounded-lg shadow">
<h4 className="font-bold mb-3">Frontend</h4>
<ul className="list-disc list-inside space-y-1">
{info.openSourceComponents.frontend.map(item => (
<li key={item.name}>
{item.name} ({item.license}-Lizenz)
</li>
))}
</ul>
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
<div className="e-card">
<div className="e-card-header">
<div className="e-card-header-caption">
<div className="e-card-title">Frontend</div>
</div>
</div>
<div className="e-card-content">
<ul style={{ paddingLeft: '1rem', margin: 0, display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
{info.openSourceComponents.frontend.map(item => (
<li key={item.name}>
{item.name} ({item.license}-Lizenz)
</li>
))}
</ul>
</div>
</div>
</div>
)}
{info.openSourceComponents.backend && (
<div className="bg-white p-6 rounded-lg shadow">
<h4 className="font-bold mb-3">Backend</h4>
<ul className="list-disc list-inside space-y-1">
{info.openSourceComponents.backend.map(item => (
<li key={item.name}>
{item.name} ({item.license}-Lizenz)
</li>
))}
</ul>
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
<div className="e-card">
<div className="e-card-header">
<div className="e-card-header-caption">
<div className="e-card-title">Backend</div>
</div>
</div>
<div className="e-card-content">
<ul style={{ paddingLeft: '1rem', margin: 0, display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
{info.openSourceComponents.backend.map(item => (
<li key={item.name}>
{item.name} ({item.license}-Lizenz)
</li>
))}
</ul>
</div>
</div>
</div>
)}
</div>