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

@@ -60,10 +60,20 @@ Use this as your shared context when proposing changes. Keep edits minimal and m
- Vite React app; proxies `/api` and `/screenshots` to API in dev (`vite.config.ts`).
- Uses Syncfusion components; Vite config pre-bundles specific packages to avoid alias issues.
- Environment: `VITE_API_URL` provided at build/run; in dev compose, proxy handles `/api` so local fetches can use relative `/api/...` paths.
- Theming: Syncfusion Material 3 theme is used. All component CSS is imported centrally in `dashboard/src/main.tsx` (base, navigations, buttons, inputs, dropdowns, popups, kanban, grids, schedule, filemanager, notifications, layouts, lists, calendars, splitbuttons, icons). Tailwind CSS has been removed.
- Scheduler (appointments page): top bar includes Group and Academic Period selectors (Syncfusion DropDownList). Selecting a period calls `POST /api/academic_periods/active`, moves the calendar to todays month/day within the period year, and refreshes a right-aligned indicator row showing:
- Holidays present in the current view (count)
- Period label (display_name or name) with a badge indicating whether any holidays exist in that period (overlap check)
- Program info page (`dashboard/src/programminfo.tsx`):
- Loads data from `dashboard/public/program-info.json` (app name, version, build info, tech stack, changelog).
- Uses Syncfusion card classes (`e-card`, `e-card-header`, `e-card-title`, `e-card-content`) for consistent styling.
- Changelog is paginated with `PagerComponent` (from `@syncfusion/ej2-react-grids`), default page size 5; adjust `pageSize` or add a selector as needed.
- Groups page (`dashboard/src/infoscreen_groups.tsx`):
- Migrated to Syncfusion inputs and popups: Buttons, TextBox, DropDownList, Dialog; Kanban remains for drag/drop.
- Unified toast/dialog wording; replaced legacy alerts with toasts; spacing handled via inline styles to avoid Tailwind dependency.
Note: Syncfusion usage in the dashboard is already documented above; if a UI for conversion status/downloads is added later, link its routes and components here.
## Local development
@@ -116,3 +126,13 @@ Questions or unclear areas? Tell us if you need: exact devcontainer debugging st
- **Usage**: New events/media can optionally reference `academic_period_id` for better organization and filtering.
- **Constraints**: Only one period can be active at a time; use `init_academic_periods.py` for Austrian school year setup.
- **UI Integration**: The dashboard highlights the currently selected period and whether a holiday plan exists within that date range. Holiday linkage currently uses date overlap with `school_holidays`; an explicit `academic_period_id` on `school_holidays` can be added later if tighter association is required.
## Changelog Style Guide (Program info)
- Source: `dashboard/public/program-info.json`; newest entry first
- Fields per release: `version`, `date` (YYYY-MM-DD), `changes` (array of short bullets)
- Tone: concise, user-facing; German wording; area prefixes allowed (e.g., “UI: …”, “API: …”)
- Categories via emoji or words: Added (🆕/✨), Changed (🛠️), Fixed (✅/🐛), Removed (🗑️), Security (🔒), Deprecated (⚠️)
- Breaking changes must be prefixed with `BREAKING:`
- Keep ≤ 810 bullets; summarize or group micro-changes
- JSON hygiene: valid JSON, no trailing commas, dont edit historical entries except typos

View File

@@ -132,7 +132,8 @@ For detailed deployment instructions, see:
### 🖥️ **Dashboard** (`dashboard/`)
- **Technology**: React 19 + TypeScript + Vite
- **UI Framework**: Syncfusion components + Tailwind CSS
- **UI Framework**: Syncfusion components (Material 3 theme)
- **Styling**: Centralized Syncfusion Material 3 CSS imports in `dashboard/src/main.tsx`
- **Features**: Responsive design, real-time updates, file management
- **Port**: 5173 (dev), served via Nginx (prod)
@@ -270,13 +271,15 @@ mosquitto_sub -h localhost -t "infoscreen/+/heartbeat" -v
## 🎨 Frontend Features
### Syncfusion Components Used
### Syncfusion Components Used (Material 3)
- **Schedule**: Event calendar with drag-drop support
- **Grid**: Data tables with filtering and sorting
- **DropDownList**: Group and period selectors
- **FileManager**: Media upload and organization
- **Kanban**: Task management views
- **Notifications**: Toast messages and alerts
- **Pager**: Used on Programinfo changelog for pagination
- **Cards (layouts)**: Programinfo sections styled with Syncfusion card classes
### Pages Overview
- **Dashboard**: System overview and statistics
@@ -286,6 +289,7 @@ mosquitto_sub -h localhost -t "infoscreen/+/heartbeat" -v
- **Media**: File upload and conversion
- **Settings**: System configuration
- **Holidays**: Academic calendar management
- **Program info**: Version, build info, tech stack and paginated changelog (reads `dashboard/public/program-info.json`)
## 🔒 Security & Authentication
@@ -415,3 +419,35 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
---
For detailed technical documentation, deployment guides, and API specifications, please refer to the additional documentation files in this repository.
Notes:
- Tailwind CSS was removed. Styling is managed via Syncfusion Material 3 theme imports in `dashboard/src/main.tsx`.
## 🧭 Changelog Style Guide
When adding entries to `dashboard/public/program-info.json` (displayed on the Program info page):
- Structure per release
- `version` (e.g., `2025.1.0-alpha.8`)
- `date` in `YYYY-MM-DD` (ISO format)
- `changes`: array of short bullet strings
- Categories (Keep a Changelog inspired)
- Prefer starting bullets with an implicit category or an emoji, e.g.:
- Added (🆕/✨), Changed (🔧/🛠️), Fixed (🐛/✅), Removed (🗑️), Security (🔒), Deprecated (⚠️)
- Writing rules
- Keep bullets concise (ideally one line) and user-facing; avoid internal IDs or jargon
- Put the affected area first when helpful (e.g., “UI: …”, “API: …”, “Scheduler: …”)
- Highlight breaking changes with “BREAKING:”
- Prefer German wording consistently; dates are localized at runtime for display
- Ordering and size
- Newest release first in the array
- Aim for ≤ 810 bullets per release; group or summarize if longer
- JSON hygiene
- Valid JSON only (no trailing commas); escape quotes as needed
- One release object per version; do not modify historical entries unless to correct typos
The Program info page paginates older entries (default page size 5). Keep highlights at the top of each release for scanability.

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,144 +370,146 @@ 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>
</div>
</div>
</ButtonComponent>
<ButtonComponent onClick={() => setShowDialog(false)}>{de.cancel}</ButtonComponent>
</div>
)}
{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="mt-2">
<TextBoxComponent
value={newGroupName}
placeholder="Raumname"
floatLabelType="Auto"
change={(args: TextBoxChangedArgs) => setNewGroupName(String(args.value ?? ''))}
/>
<div className="flex gap-2">
<button
className="bg-blue-500 text-white px-4 py-2 rounded"
</div>
</DialogComponent>
)}
{renameDialog.open && (
<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>
</div>
</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>
</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}
</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={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="350px"
width="380px"
header={de.confirmDelete}
visible={showDeleteConfirm}
showCloseIcon={true}
close={() => setShowDeleteConfirm(false)}
target="#dialog-target"
footerTemplate={() => (
<div className="flex gap-2 justify-end">
<button
className="bg-red-500 text-white px-4 py-2 rounded"
<ButtonComponent
cssClass="e-danger"
onClick={() => {
handleDeleteGroup(deleteDialog.groupName);
handleDeleteGroup(deleteDialog.groupName!);
setShowDeleteConfirm(false);
}}
>
{de.yesDelete}
</button>
<button
className="bg-gray-300 px-4 py-2 rounded"
</ButtonComponent>
<ButtonComponent
onClick={() => {
setShowDeleteConfirm(false);
setDeleteDialog({ open: false, groupName: '' });
}}
>
{de.cancel}
</button>
</ButtonComponent>
</div>
)}
>
@@ -511,8 +521,6 @@ const Infoscreen_groups: React.FC = () => {
</DialogComponent>
)}
</div>
)}
</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,24 +60,32 @@ 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">
<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>
@@ -81,68 +94,116 @@ const Programminfo: React.FC = () => {
</p>
<p>
<strong>Support:</strong>{' '}
<a href={`mailto:${info.supportContact}`} className="text-blue-600 hover:underline">
<a href={`mailto:${info.supportContact}`} style={{ color: '#2563eb', textDecoration: 'none' }}>
{info.supportContact}
</a>
</p>
<hr className="my-4" />
<h4 className="font-semibold">Build-Informationen</h4>
<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')}
<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">
<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">
<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 className="font-semibold capitalize">{key}:</span> {value}
<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">
<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 className="text-sm font-normal text-gray-500">
<span style={{ fontSize: '0.875rem', fontWeight: 400, color: '#6b7280' }}>
- {new Date(log.date).toLocaleDateString('de-DE')}
</span>
</h4>
<ul className="list-disc list-inside space-y-1 text-gray-700">
</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">
<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)
@@ -150,11 +211,19 @@ const Programminfo: React.FC = () => {
))}
</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">
<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)
@@ -162,6 +231,8 @@ const Programminfo: React.FC = () => {
))}
</ul>
</div>
</div>
</div>
)}
</div>
</div>