UI: switch to Syncfusion M3, remove Tailwind;
paginate changelog; docs updated; bump to 2025.1.0-alpha.8
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-tailwindcss"
|
||||
"stylelint-config-standard"
|
||||
],
|
||||
"rules": {
|
||||
"at-rule-no-unknown": null
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user