UI: switch to Syncfusion M3, remove Tailwind;
paginate changelog; docs updated; bump to 2025.1.0-alpha.8
This commit is contained in:
20
.github/copilot-instructions.md
vendored
20
.github/copilot-instructions.md
vendored
@@ -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`).
|
- 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.
|
- 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.
|
- 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 today’s month/day within the period year, and refreshes a right-aligned indicator row showing:
|
- 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 today’s month/day within the period year, and refreshes a right-aligned indicator row showing:
|
||||||
- Holidays present in the current view (count)
|
- 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)
|
- 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.
|
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
|
## 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.
|
- **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.
|
- **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.
|
- **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 ≤ 8–10 bullets; summarize or group micro-changes
|
||||||
|
- JSON hygiene: valid JSON, no trailing commas, don’t edit historical entries except typos
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -132,7 +132,8 @@ For detailed deployment instructions, see:
|
|||||||
|
|
||||||
### 🖥️ **Dashboard** (`dashboard/`)
|
### 🖥️ **Dashboard** (`dashboard/`)
|
||||||
- **Technology**: React 19 + TypeScript + Vite
|
- **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
|
- **Features**: Responsive design, real-time updates, file management
|
||||||
- **Port**: 5173 (dev), served via Nginx (prod)
|
- **Port**: 5173 (dev), served via Nginx (prod)
|
||||||
|
|
||||||
@@ -270,13 +271,15 @@ mosquitto_sub -h localhost -t "infoscreen/+/heartbeat" -v
|
|||||||
|
|
||||||
## 🎨 Frontend Features
|
## 🎨 Frontend Features
|
||||||
|
|
||||||
### Syncfusion Components Used
|
### Syncfusion Components Used (Material 3)
|
||||||
- **Schedule**: Event calendar with drag-drop support
|
- **Schedule**: Event calendar with drag-drop support
|
||||||
- **Grid**: Data tables with filtering and sorting
|
- **Grid**: Data tables with filtering and sorting
|
||||||
- **DropDownList**: Group and period selectors
|
- **DropDownList**: Group and period selectors
|
||||||
- **FileManager**: Media upload and organization
|
- **FileManager**: Media upload and organization
|
||||||
- **Kanban**: Task management views
|
- **Kanban**: Task management views
|
||||||
- **Notifications**: Toast messages and alerts
|
- **Notifications**: Toast messages and alerts
|
||||||
|
- **Pager**: Used on Programinfo changelog for pagination
|
||||||
|
- **Cards (layouts)**: Programinfo sections styled with Syncfusion card classes
|
||||||
|
|
||||||
### Pages Overview
|
### Pages Overview
|
||||||
- **Dashboard**: System overview and statistics
|
- **Dashboard**: System overview and statistics
|
||||||
@@ -286,6 +289,7 @@ mosquitto_sub -h localhost -t "infoscreen/+/heartbeat" -v
|
|||||||
- **Media**: File upload and conversion
|
- **Media**: File upload and conversion
|
||||||
- **Settings**: System configuration
|
- **Settings**: System configuration
|
||||||
- **Holidays**: Academic calendar management
|
- **Holidays**: Academic calendar management
|
||||||
|
- **Program info**: Version, build info, tech stack and paginated changelog (reads `dashboard/public/program-info.json`)
|
||||||
|
|
||||||
## 🔒 Security & Authentication
|
## 🔒 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.
|
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 ≤ 8–10 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.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": [
|
||||||
"stylelint-config-standard",
|
"stylelint-config-standard"
|
||||||
"stylelint-config-tailwindcss"
|
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"at-rule-no-unknown": null
|
"at-rule-no-unknown": null
|
||||||
|
|||||||
@@ -45,9 +45,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@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": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
@@ -66,8 +63,6 @@
|
|||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"stylelint": "^16.21.0",
|
"stylelint": "^16.21.0",
|
||||||
"stylelint-config-standard": "^38.0.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"stylelint-config-tailwindcss": "^1.0.0",
|
|
||||||
"tailwindcss": "^3.4.17",
|
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"typescript-eslint": "^8.30.1",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"appName": "Infoscreen-Management",
|
"appName": "Infoscreen-Management",
|
||||||
"version": "2025.1.0-alpha.7",
|
"version": "2025.1.0-alpha.8",
|
||||||
"copyright": "© 2025 Third-Age-Applications",
|
"copyright": "© 2025 Third-Age-Applications",
|
||||||
"supportContact": "support@third-age-applications.com",
|
"supportContact": "support@third-age-applications.com",
|
||||||
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
||||||
@@ -30,6 +30,17 @@
|
|||||||
"commitId": "8d1df7199cb7"
|
"commitId": "8d1df7199cb7"
|
||||||
},
|
},
|
||||||
"changelog": [
|
"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",
|
"version": "2025.1.0-alpha.7",
|
||||||
"date": "2025-09-21",
|
"date": "2025-09-21",
|
||||||
|
|||||||
@@ -1,19 +1,4 @@
|
|||||||
@import "../node_modules/@syncfusion/ej2-base/styles/material.css";
|
/* Removed legacy Syncfusion material theme imports; using material3 imports in main.tsx */
|
||||||
@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";
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Inter, 'Segoe UI', Roboto, Arial, sans-serif;
|
font-family: Inter, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
/* @tailwind base;
|
/* Tailwind removed: base/components/utilities directives no longer used. */
|
||||||
@tailwind components; */
|
|
||||||
|
|
||||||
/* @tailwind utilities; */
|
|
||||||
|
|
||||||
/* :root {
|
/* :root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
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 { Client } from './apiClients';
|
||||||
import type { KanbanComponent as KanbanComponentType } from '@syncfusion/ej2-react-kanban';
|
import type { KanbanComponent as KanbanComponentType } from '@syncfusion/ej2-react-kanban';
|
||||||
import { DialogComponent } from '@syncfusion/ej2-react-popups';
|
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 { useToast } from './components/ToastProvider';
|
||||||
import { L10n } from '@syncfusion/ej2-base';
|
import { L10n } from '@syncfusion/ej2-base';
|
||||||
|
|
||||||
@@ -41,10 +46,10 @@ const de = {
|
|||||||
rename: 'Umbenennen',
|
rename: 'Umbenennen',
|
||||||
confirmDelete: 'Löschbestätigung',
|
confirmDelete: 'Löschbestätigung',
|
||||||
reallyDelete: (name: string) => `Möchten Sie die Gruppe <b>${name}</b> wirklich löschen?`,
|
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',
|
groupCreated: 'Gruppe angelegt',
|
||||||
groupDeleted: 'Gruppe gelöscht. Clients in "Nicht zugeordnet" verschoben',
|
groupDeleted: 'Gruppe gelöscht. Alle Clients wurden nach "Nicht zugeordnet" verschoben.',
|
||||||
groupRenamed: 'Gruppenname geändert',
|
groupRenamed: 'Gruppe umbenannt',
|
||||||
selectGroup: 'Gruppe wählen',
|
selectGroup: 'Gruppe wählen',
|
||||||
newName: 'Neuer Name',
|
newName: 'Neuer Name',
|
||||||
warning: 'Achtung:',
|
warning: 'Achtung:',
|
||||||
@@ -312,7 +317,12 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch {
|
} 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);
|
setDraggedCard(null);
|
||||||
};
|
};
|
||||||
@@ -326,25 +336,23 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div id="dialog-target">
|
<div id="dialog-target">
|
||||||
<h2 className="text-xl font-bold mb-4">{de.title}</h2>
|
<h2 className="text-xl font-bold mb-4">{de.title}</h2>
|
||||||
<div className="flex gap-2 mb-4">
|
<div
|
||||||
<button
|
style={{
|
||||||
className="px-4 py-2 bg-blue-500 text-white rounded"
|
display: 'flex',
|
||||||
onClick={() => setShowDialog(true)}
|
flexWrap: 'wrap',
|
||||||
>
|
gap: '12px',
|
||||||
|
marginBottom: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonComponent cssClass="e-primary" onClick={() => setShowDialog(true)}>
|
||||||
{de.newGroup}
|
{de.newGroup}
|
||||||
</button>
|
</ButtonComponent>
|
||||||
<button
|
<ButtonComponent cssClass="e-warning" onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}>
|
||||||
className="px-4 py-2 bg-yellow-500 text-white rounded"
|
|
||||||
onClick={() => setRenameDialog({ open: true, oldName: '', newName: '' })}
|
|
||||||
>
|
|
||||||
{de.renameGroup}
|
{de.renameGroup}
|
||||||
</button>
|
</ButtonComponent>
|
||||||
<button
|
<ButtonComponent cssClass="e-danger" onClick={() => setDeleteDialog({ open: true, groupName: '' })}>
|
||||||
className="px-4 py-2 bg-red-500 text-white rounded"
|
|
||||||
onClick={() => setDeleteDialog({ open: true, groupName: '' })}
|
|
||||||
>
|
|
||||||
{de.deleteGroup}
|
{de.deleteGroup}
|
||||||
</button>
|
</ButtonComponent>
|
||||||
</div>
|
</div>
|
||||||
<KanbanComponent
|
<KanbanComponent
|
||||||
locale="de"
|
locale="de"
|
||||||
@@ -362,155 +370,155 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
columns={kanbanColumns}
|
columns={kanbanColumns}
|
||||||
/>
|
/>
|
||||||
{showDialog && (
|
{showDialog && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
<DialogComponent
|
||||||
<div className="bg-white p-6 rounded shadow">
|
visible={showDialog}
|
||||||
<h3 className="mb-2 font-bold">{de.newGroup}</h3>
|
header={de.newGroup}
|
||||||
<input
|
close={() => setShowDialog(false)}
|
||||||
className="border p-2 mb-2 w-full"
|
target="#dialog-target"
|
||||||
value={newGroupName}
|
width="420px"
|
||||||
onChange={e => setNewGroupName(e.target.value)}
|
footerTemplate={() => (
|
||||||
placeholder="Raumname"
|
<div className="flex gap-2 justify-end">
|
||||||
/>
|
<ButtonComponent cssClass="e-primary" onClick={handleAddGroup} disabled={!newGroupName.trim()}>
|
||||||
<div className="flex gap-2">
|
|
||||||
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={handleAddGroup}>
|
|
||||||
{de.add}
|
{de.add}
|
||||||
</button>
|
</ButtonComponent>
|
||||||
<button
|
<ButtonComponent onClick={() => setShowDialog(false)}>{de.cancel}</ButtonComponent>
|
||||||
className="bg-gray-300 px-4 py-2 rounded"
|
|
||||||
onClick={() => setShowDialog(false)}
|
|
||||||
>
|
|
||||||
{de.cancel}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="mt-2">
|
||||||
|
<TextBoxComponent
|
||||||
|
value={newGroupName}
|
||||||
|
placeholder="Raumname"
|
||||||
|
floatLabelType="Auto"
|
||||||
|
change={(args: TextBoxChangedArgs) => setNewGroupName(String(args.value ?? ''))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DialogComponent>
|
||||||
)}
|
)}
|
||||||
{renameDialog.open && (
|
{renameDialog.open && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
<DialogComponent
|
||||||
<div className="bg-white p-6 rounded shadow">
|
visible={renameDialog.open}
|
||||||
<h3 className="mb-2 font-bold">{de.renameGroup}</h3>
|
header={de.renameGroup}
|
||||||
<select
|
showCloseIcon={true}
|
||||||
className="border p-2 mb-2 w-full"
|
close={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
|
||||||
value={renameDialog.oldName}
|
target="#dialog-target"
|
||||||
onChange={e =>
|
width="480px"
|
||||||
setRenameDialog({
|
footerTemplate={() => (
|
||||||
...renameDialog,
|
<div className="flex gap-2 justify-end">
|
||||||
oldName: e.target.value,
|
<ButtonComponent
|
||||||
newName: e.target.value,
|
cssClass="e-primary"
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
onClick={handleRenameGroup}
|
onClick={handleRenameGroup}
|
||||||
disabled={!renameDialog.oldName || !renameDialog.newName}
|
disabled={!renameDialog.oldName || !renameDialog.newName}
|
||||||
>
|
>
|
||||||
{de.rename}
|
{de.rename}
|
||||||
</button>
|
</ButtonComponent>
|
||||||
<button
|
<ButtonComponent onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}>
|
||||||
className="bg-gray-300 px-4 py-2 rounded"
|
|
||||||
onClick={() => setRenameDialog({ open: false, oldName: '', newName: '' })}
|
|
||||||
>
|
|
||||||
{de.cancel}
|
{de.cancel}
|
||||||
</button>
|
</ButtonComponent>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</DialogComponent>
|
||||||
)}
|
)}
|
||||||
{deleteDialog.open && (
|
{deleteDialog.open && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
<DialogComponent
|
||||||
<div className="bg-white p-6 rounded shadow">
|
visible={deleteDialog.open}
|
||||||
<h3 className="mb-2 font-bold">{de.deleteGroup}</h3>
|
header={de.deleteGroup}
|
||||||
<select
|
showCloseIcon={true}
|
||||||
className="border p-2 mb-2 w-full"
|
close={() => setDeleteDialog({ open: false, groupName: '' })}
|
||||||
value={deleteDialog.groupName}
|
target="#dialog-target"
|
||||||
onChange={e => setDeleteDialog({ ...deleteDialog, groupName: e.target.value })}
|
width="520px"
|
||||||
>
|
footerTemplate={() => (
|
||||||
<option value="">{de.selectGroup}</option>
|
<div className="flex gap-2 justify-end">
|
||||||
{groups
|
<ButtonComponent
|
||||||
.filter(g => g.headerText !== 'Nicht zugeordnet')
|
cssClass="e-danger"
|
||||||
.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"
|
|
||||||
onClick={() => setShowDeleteConfirm(true)}
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
disabled={!deleteDialog.groupName}
|
disabled={!deleteDialog.groupName}
|
||||||
>
|
>
|
||||||
{de.deleteGroup}
|
{de.deleteGroup}
|
||||||
</button>
|
</ButtonComponent>
|
||||||
<button
|
<ButtonComponent onClick={() => setDeleteDialog({ open: false, groupName: '' })}>
|
||||||
className="bg-gray-300 px-4 py-2 rounded"
|
{de.cancel}
|
||||||
onClick={() => setDeleteDialog({ open: false, groupName: '' })}
|
</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}
|
{de.cancel}
|
||||||
</button>
|
</ButtonComponent>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,20 @@ import { registerLicense } from '@syncfusion/ej2-base';
|
|||||||
import '@syncfusion/ej2-base/styles/material3.css';
|
import '@syncfusion/ej2-base/styles/material3.css';
|
||||||
import '@syncfusion/ej2-navigations/styles/material3.css';
|
import '@syncfusion/ej2-navigations/styles/material3.css';
|
||||||
import '@syncfusion/ej2-buttons/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
|
// Setze hier deinen Lizenzschlüssel ein
|
||||||
registerLicense(
|
registerLicense(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { PagerComponent } from '@syncfusion/ej2-react-grids';
|
||||||
|
|
||||||
interface ProgramInfo {
|
interface ProgramInfo {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -6,9 +7,7 @@ interface ProgramInfo {
|
|||||||
copyright: string;
|
copyright: string;
|
||||||
supportContact: string;
|
supportContact: string;
|
||||||
description: string;
|
description: string;
|
||||||
techStack: {
|
techStack: Record<string, string>;
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
openSourceComponents: {
|
openSourceComponents: {
|
||||||
frontend: { name: string; license: string }[];
|
frontend: { name: string; license: string }[];
|
||||||
backend: { name: string; license: string }[];
|
backend: { name: string; license: string }[];
|
||||||
@@ -27,26 +26,32 @@ interface ProgramInfo {
|
|||||||
const Programminfo: React.FC = () => {
|
const Programminfo: React.FC = () => {
|
||||||
const [info, setInfo] = useState<ProgramInfo | null>(null);
|
const [info, setInfo] = useState<ProgramInfo | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
|
const pageSize = 5;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
fetch('/program-info.json')
|
fetch('/program-info.json')
|
||||||
.then(response => {
|
.then(res => {
|
||||||
if (!response.ok) {
|
if (!res.ok) throw new Error('Netzwerk-Antwort war nicht ok');
|
||||||
throw new Error('Netzwerk-Antwort war nicht ok');
|
return res.json();
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
})
|
||||||
.then(data => setInfo(data))
|
.then((data: ProgramInfo) => {
|
||||||
.catch(error => {
|
if (isMounted) setInfo(data);
|
||||||
console.error('Fehler beim Laden der Programminformationen:', error);
|
})
|
||||||
setError('Informationen konnten nicht geladen werden.');
|
.catch(err => {
|
||||||
|
console.error('Fehler beim Laden der Programminformationen:', err);
|
||||||
|
if (isMounted) setError('Informationen konnten nicht geladen werden.');
|
||||||
});
|
});
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<p>{error}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -55,112 +60,178 @@ const Programminfo: React.FC = () => {
|
|||||||
if (!info) {
|
if (!info) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<p>Lade Informationen...</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const monoFont = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-2">{info.appName}</h2>
|
<h2 style={{ fontSize: '1.75rem', fontWeight: 700, marginBottom: '0.5rem' }}>{info.appName}</h2>
|
||||||
<p className="text-gray-600">{info.description}</p>
|
<p style={{ color: '#4b5563' }}>{info.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2rem' }}>
|
||||||
{/* Allgemeine Infos & Build */}
|
{/* Allgemeine Infos & Build */}
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
|
||||||
<h3 className="text-xl font-semibold mb-4 border-b pb-2">Allgemein</h3>
|
<div className="e-card">
|
||||||
<div className="space-y-3">
|
<div className="e-card-header">
|
||||||
<p>
|
<div className="e-card-header-caption">
|
||||||
<strong>Version:</strong> {info.version}
|
<div className="e-card-title">Allgemein</div>
|
||||||
</p>
|
</div>
|
||||||
<p>
|
</div>
|
||||||
<strong>Copyright:</strong> {info.copyright}
|
<div className="e-card-content">
|
||||||
</p>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
<p>
|
<p>
|
||||||
<strong>Support:</strong>{' '}
|
<strong>Version:</strong> {info.version}
|
||||||
<a href={`mailto:${info.supportContact}`} className="text-blue-600 hover:underline">
|
</p>
|
||||||
{info.supportContact}
|
<p>
|
||||||
</a>
|
<strong>Copyright:</strong> {info.copyright}
|
||||||
</p>
|
</p>
|
||||||
<hr className="my-4" />
|
<p>
|
||||||
<h4 className="font-semibold">Build-Informationen</h4>
|
<strong>Support:</strong>{' '}
|
||||||
<p>
|
<a href={`mailto:${info.supportContact}`} style={{ color: '#2563eb', textDecoration: 'none' }}>
|
||||||
<strong>Build-Datum:</strong>{' '}
|
{info.supportContact}
|
||||||
{new Date(info.buildInfo.buildDate).toLocaleString('de-DE')}
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<hr style={{ margin: '1rem 0' }} />
|
||||||
<strong>Commit-ID:</strong>{' '}
|
<h4 style={{ fontWeight: 600 }}>Build-Informationen</h4>
|
||||||
<span className="font-mono text-sm bg-gray-100 p-1 rounded">
|
<p>
|
||||||
{info.buildInfo.commitId}
|
<strong>Build-Datum:</strong> {new Date(info.buildInfo.buildDate).toLocaleString('de-DE')}
|
||||||
</span>
|
</p>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Technischer Stack */}
|
{/* Technischer Stack */}
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
|
||||||
<h3 className="text-xl font-semibold mb-4 border-b pb-2">Technologie-Stack</h3>
|
<div className="e-card">
|
||||||
<ul className="list-disc list-inside space-y-2">
|
<div className="e-card-header">
|
||||||
{Object.entries(info.techStack).map(([key, value]) => (
|
<div className="e-card-header-caption">
|
||||||
<li key={key}>
|
<div className="e-card-title">Technologie-Stack</div>
|
||||||
<span className="font-semibold capitalize">{key}:</span> {value}
|
</div>
|
||||||
</li>
|
</div>
|
||||||
))}
|
<div className="e-card-content">
|
||||||
</ul>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Changelog */}
|
{/* Changelog */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold mb-4">Änderungsprotokoll (Changelog)</h3>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '0.5rem' }}>
|
||||||
<div className="space-y-6">
|
<h3 style={{ fontSize: '1.25rem', fontWeight: 600, margin: 0 }}>Änderungsprotokoll (Changelog)</h3>
|
||||||
{info.changelog.map(log => (
|
<div style={{ marginLeft: 'auto' }}>
|
||||||
<div key={log.version} className="bg-white p-6 rounded-lg shadow">
|
<span style={{ color: '#6b7280', fontSize: '0.875rem' }}>
|
||||||
<h4 className="font-bold text-lg mb-2">
|
Insgesamt {info.changelog.length} Einträge
|
||||||
Version {log.version}{' '}
|
</span>
|
||||||
<span className="text-sm font-normal text-gray-500">
|
</div>
|
||||||
- {new Date(log.date).toLocaleDateString('de-DE')}
|
</div>
|
||||||
</span>
|
<div style={{ marginBottom: '0.75rem' }}>
|
||||||
</h4>
|
<PagerComponent
|
||||||
<ul className="list-disc list-inside space-y-1 text-gray-700">
|
totalRecordsCount={info.changelog.length}
|
||||||
{log.changes.map((change, index) => (
|
pageSize={pageSize}
|
||||||
<li key={index}>{change}</li>
|
pageCount={5}
|
||||||
))}
|
currentPage={currentPage}
|
||||||
</ul>
|
click={(args: { currentPage: number }) => setCurrentPage(args.currentPage)}
|
||||||
</div>
|
/>
|
||||||
))}
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Open Source Komponenten */}
|
{/* Open Source Komponenten */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold mb-4">Verwendete Open-Source-Komponenten</h3>
|
<h3 style={{ fontSize: '1.25rem', fontWeight: 600, marginBottom: '1rem' }}>Verwendete Open-Source-Komponenten</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2rem' }}>
|
||||||
{info.openSourceComponents.frontend && (
|
{info.openSourceComponents.frontend && (
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
|
||||||
<h4 className="font-bold mb-3">Frontend</h4>
|
<div className="e-card">
|
||||||
<ul className="list-disc list-inside space-y-1">
|
<div className="e-card-header">
|
||||||
{info.openSourceComponents.frontend.map(item => (
|
<div className="e-card-header-caption">
|
||||||
<li key={item.name}>
|
<div className="e-card-title">Frontend</div>
|
||||||
{item.name} ({item.license}-Lizenz)
|
</div>
|
||||||
</li>
|
</div>
|
||||||
))}
|
<div className="e-card-content">
|
||||||
</ul>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{info.openSourceComponents.backend && (
|
{info.openSourceComponents.backend && (
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
<div style={{ flex: '1 1 360px', minWidth: '320px' }}>
|
||||||
<h4 className="font-bold mb-3">Backend</h4>
|
<div className="e-card">
|
||||||
<ul className="list-disc list-inside space-y-1">
|
<div className="e-card-header">
|
||||||
{info.openSourceComponents.backend.map(item => (
|
<div className="e-card-header-caption">
|
||||||
<li key={item.name}>
|
<div className="e-card-title">Backend</div>
|
||||||
{item.name} ({item.license}-Lizenz)
|
</div>
|
||||||
</li>
|
</div>
|
||||||
))}
|
<div className="e-card-content">
|
||||||
</ul>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user