From 25dda32c4e55cf639159ceb8e3e28c5a83082e9e Mon Sep 17 00:00:00 2001 From: "matthias.lotz" Date: Wed, 26 Nov 2025 22:42:55 +0100 Subject: [PATCH] feat: Error handling system and animated error pages - Add ErrorBoundary component for React error handling - Create animated error pages (403, 404, 500, 502, 503) - Implement ErrorAnimation component with seven-segment display - Add apiClient (axios) and apiFetch (fetch) wrappers with automatic error page redirects - Migrate critical API calls to use new error handling - Update font from Roboto to Open Sans across all components - Remove unused CLIENT_URL from docker-compose files - Rename 404Page.css to ErrorPage.css for consistency - Add comprehensive ERROR_HANDLING.md documentation --- docker/dev/docker-compose.yml | 1 - docker/prod/docker-compose.yml | 1 - frontend/ERROR_HANDLING.md | 149 ++++++++++++++++ frontend/public/index.html | 3 +- frontend/src/App.css | 10 +- frontend/src/App.js | 27 ++- .../ComponentUtils/ConsentManager.js | 7 +- .../Components/ComponentUtils/Css/Footer.css | 4 +- .../ComponentUtils/Css/ImageGallery.css | 4 +- .../Components/ComponentUtils/Css/Navbar.css | 4 +- .../ComponentUtils/DeleteGroupButton.js | 3 +- .../ErrorAnimation/ErrorAnimation.css | 78 +++++++++ .../ErrorAnimation/ErrorAnimation.js | 146 ++++++++++++++++ .../ComponentUtils/ErrorBoundary.js | 38 ++++ .../ComponentUtils/GroupMetadataEditor.js | 3 +- .../ComponentUtils/ImageDescriptionManager.js | 5 +- frontend/src/Components/Pages/403Page.js | 30 ++++ frontend/src/Components/Pages/404Page.js | 32 ++-- frontend/src/Components/Pages/500Page.js | 30 ++++ frontend/src/Components/Pages/502Page.js | 30 ++++ frontend/src/Components/Pages/503Page.js | 30 ++++ frontend/src/Components/Pages/Css/404Page.css | 68 -------- .../src/Components/Pages/Css/ErrorPage.css | 10 ++ .../Components/Pages/ManagementPortalPage.js | 7 +- .../Components/Pages/PublicGroupImagesPage.js | 3 +- frontend/src/Utils/apiClient.js | 62 +++++++ frontend/src/Utils/apiFetch.js | 116 +++++++++++++ frontend/src/Utils/batchUpload.js | 10 +- frontend/src/Utils/sendRequest.js | 4 +- test-loading.html | 162 ++++++++++++++++++ 30 files changed, 952 insertions(+), 125 deletions(-) create mode 100644 frontend/ERROR_HANDLING.md create mode 100644 frontend/src/Components/ComponentUtils/ErrorAnimation/ErrorAnimation.css create mode 100644 frontend/src/Components/ComponentUtils/ErrorAnimation/ErrorAnimation.js create mode 100644 frontend/src/Components/ComponentUtils/ErrorBoundary.js create mode 100644 frontend/src/Components/Pages/403Page.js create mode 100644 frontend/src/Components/Pages/500Page.js create mode 100644 frontend/src/Components/Pages/502Page.js create mode 100644 frontend/src/Components/Pages/503Page.js delete mode 100644 frontend/src/Components/Pages/Css/404Page.css create mode 100644 frontend/src/Components/Pages/Css/ErrorPage.css create mode 100644 frontend/src/Utils/apiClient.js create mode 100644 frontend/src/Utils/apiFetch.js create mode 100644 test-loading.html diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index 249c1ef..cf8d18c 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -19,7 +19,6 @@ services: environment: - CHOKIDAR_USEPOLLING=true - API_URL=http://localhost:5001 - - CLIENT_URL=http://localhost:3000 - PUBLIC_HOST=public.test.local - INTERNAL_HOST=internal.test.local depends_on: diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 69202f0..d510ec1 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -15,7 +15,6 @@ services: - backend environment: - API_URL=http://backend:5000 - - CLIENT_URL=http://localhost - PUBLIC_HOST=deinprojekt.hobbyhimmel.de - INTERNAL_HOST=deinprojekt.lan.hobbyhimmel.de diff --git a/frontend/ERROR_HANDLING.md b/frontend/ERROR_HANDLING.md new file mode 100644 index 0000000..8f1ba2e --- /dev/null +++ b/frontend/ERROR_HANDLING.md @@ -0,0 +1,149 @@ +# Error Handling System + +Das Frontend verfügt jetzt über ein vollständiges Error Handling System für HTTP-Fehler und React-Fehler. + +## ✅ Migration abgeschlossen + +Alle kritischen API-Aufrufe wurden auf das neue Error-Handling-System migriert: +- ✅ `sendRequest.js` → `apiClient` (axios-basiert) +- ✅ `batchUpload.js` → `apiFetch` +- ✅ `PublicGroupImagesPage.js` → `apiFetch` +- ✅ `ManagementPortalPage.js` → `apiFetch` +- ✅ `DeleteGroupButton.js` → `apiFetch` +- ✅ `ConsentManager.js` → `apiFetch` +- ✅ `ImageDescriptionManager.js` → `apiFetch` +- ✅ `GroupMetadataEditor.js` → `apiFetch` + +**Hinweis:** `adminApi.js` und `socialMediaApi.js` verwenden ihr eigenes `adminFetch`-System mit CSRF-Token-Handling und wurden bewusst nicht migriert. + +## Komponenten + +### 1. ErrorBoundary (`/Components/ComponentUtils/ErrorBoundary.js`) +- Fängt React-Fehler (z.B. Rendering-Fehler) ab +- Zeigt automatisch die 500-Error-Page bei unerwarteten Fehlern +- Loggt Fehlerdetails in der Konsole für Debugging + +### 2. API Client (`/Utils/apiClient.js`) +- Axios-Instance mit Response-Interceptor +- Für FormData-Uploads (z.B. Bilder) +- Automatische Weiterleitung zu Error-Pages basierend auf HTTP-Statuscode + +### 3. API Fetch Wrapper (`/Utils/apiFetch.js`) +- Native Fetch-Wrapper mit Error-Handling +- Für Standard-JSON-API-Aufrufe +- Automatische Weiterleitung zu Error-Pages: + - **403 Forbidden** → `/error/403` + - **500 Internal Server Error** → `/error/500` + - **502 Bad Gateway** → `/error/502` + - **503 Service Unavailable** → `/error/503` + +### 4. Error Pages Routes (`App.js`) +- Neue Routes für alle Error-Pages: + - `/error/403` - Forbidden + - `/error/500` - Internal Server Error + - `/error/502` - Bad Gateway + - `/error/503` - Service Unavailable + - `*` - 404 Not Found (catch-all) + +## Verwendung + +### Für File-Uploads (FormData) +Verwende `apiClient` für multipart/form-data Uploads: + +```javascript +import apiClient from '../Utils/apiClient'; + +const formData = new FormData(); +formData.append('file', file); + +apiClient.post('/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' } +}) + .then(response => { + // Success handling + }) + .catch(error => { + // Automatische Weiterleitung zu Error-Page bei 403, 500, 502, 503 + }); +``` + +### Für JSON-API-Aufrufe +Verwende `apiFetch` oder Helper-Funktionen: + +```javascript +import { apiFetch, apiGet, apiPost } from '../Utils/apiFetch'; + +// GET Request +const data = await apiGet('/api/groups'); + +// POST Request +const result = await apiPost('/api/groups', { name: 'Test' }); + +// Custom Request +const response = await apiFetch('/api/groups/123', { + method: 'DELETE' +}); +``` + +## Backend Error Codes + +Das Backend liefert bereits folgende Statuscodes: + +- **403**: CSRF-Fehler, fehlende Admin-Session, public host auf internal routes +- **500**: Datenbank-Fehler, Upload-Fehler, Migration-Fehler +- **502**: Nicht implementiert (wird von Reverse Proxy geliefert) +- **503**: Nicht implementiert (für Wartungsmodus vorgesehen) + +## Testing + +Um die Error-Pages zu testen: + +1. **403**: Versuche ohne Login auf Admin-Routen zuzugreifen +2. **404**: Navigiere zu einer nicht existierenden Route (z.B. `/nicht-vorhanden`) +3. **500**: Simuliere Backend-Fehler +4. **502/503**: Manuell über `/error/502` oder `/error/503` aufrufen + +## Architektur + +``` +┌─────────────────────────────────────────────┐ +│ App.js │ +│ ┌───────────────────────────────────────┐ │ +│ │ ErrorBoundary │ │ +│ │ (fängt React-Fehler) │ │ +│ │ ┌─────────────────────────────────┐ │ │ +│ │ │ Router │ │ │ +│ │ │ ┌───────────────────────────┐ │ │ │ +│ │ │ │ Routes │ │ │ │ +│ │ │ │ - / │ │ │ │ +│ │ │ │ - /error/403 │ │ │ │ +│ │ │ │ - /error/500 │ │ │ │ +│ │ │ │ - /error/502 │ │ │ │ +│ │ │ │ - /error/503 │ │ │ │ +│ │ │ │ - * (404) │ │ │ │ +│ │ │ └───────────────────────────┘ │ │ │ +│ │ └─────────────────────────────────┘ │ │ +│ └───────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────┐ +│ API Layer │ +├─────────────────────────────────────────────┤ +│ apiClient.js (axios) │ +│ - FormData/File-Uploads │ +│ - Response Interceptor │ +│ │ +│ apiFetch.js (fetch) │ +│ - JSON-API-Aufrufe │ +│ - Error-Response-Handling │ +│ │ +│ adminApi.js (fetch + CSRF) │ +│ - Admin-Authentifizierung │ +│ - CSRF-Token-Management │ +│ - Nicht migriert (eigenes System) │ +└─────────────────────────────────────────────┘ + +Error-Flow: +HTTP 403/500/502/503 → Interceptor/Handler → window.location.href → Error-Page +React Error → ErrorBoundary → 500-Page +``` diff --git a/frontend/public/index.html b/frontend/public/index.html index d272b33..655651e 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -18,8 +18,7 @@ - - + diff --git a/frontend/src/App.css b/frontend/src/App.css index 16d93fb..62f742b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -3,13 +3,15 @@ /* Page-specific styles for GroupsOverviewPage */ .groups-overview-container { padding-top: 20px; padding-bottom: 40px; min-height: 80vh; } .header-card { border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); margin-bottom: 30px; text-align: center; padding: 20px; } -.header-title { font-family: roboto; font-weight: 500; font-size: 28px; color: #333333; margin-bottom: 10px; } -.header-subtitle { font-family: roboto; font-size: 16px; color: #666666; margin-bottom: 20px; } +.header-title { font-family: 'Open Sans', sans-serif; font-weight: 500; font-size: 28px; color: #333333; margin-bottom: 10px; } +.header-subtitle { font-family: 'Open Sans', sans-serif; font-size: 16px; color: #666666; margin-bottom: 20px; } @media (max-width:800px) { .nav__links, .cta { display:none; } } /* Page-specific styles for ModerationPage */ -.moderation-page { font-family: roboto; max-width: 1200px; margin: 0 auto; padding: 20px; } -.moderation-content h1 { font-family: roboto; text-align:left; color:#333; margin-bottom:30px; } +.moderation-page { font-family: 'Open Sans', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; } +h1 { font-family: 'Open Sans', sans-serif; text-align:left; color:#333; margin-bottom:30px; } +p { font-family: 'Open Sans', sans-serif; color:#555; line-height:1.6; } +.moderation-content h1 { font-family: 'Open Sans', sans-serif; text-align:left; color:#333; margin-bottom:30px; } .moderation-loading, .moderation-error { text-align:center; padding:50px; font-size:18px; } .moderation-error { color:#dc3545; } diff --git a/frontend/src/App.js b/frontend/src/App.js index 6d5ce2b..627676c 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -3,11 +3,16 @@ import './App.css'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { AdminSessionProvider } from './contexts/AdminSessionContext.jsx'; import { getHostConfig } from './Utils/hostDetection.js'; +import ErrorBoundary from './Components/ComponentUtils/ErrorBoundary.js'; // Always loaded (public + internal) import MultiUploadPage from './Components/Pages/MultiUploadPage'; import ManagementPortalPage from './Components/Pages/ManagementPortalPage'; import NotFoundPage from './Components/Pages/404Page.js'; +import ForbiddenPage from './Components/Pages/403Page.js'; +import InternalServerErrorPage from './Components/Pages/500Page.js'; +import BadGatewayPage from './Components/Pages/502Page.js'; +import ServiceUnavailablePage from './Components/Pages/503Page.js'; // Lazy loaded (internal only) - Code Splitting für Performance const SlideshowPage = lazy(() => import('./Components/Pages/SlideshowPage')); @@ -52,13 +57,20 @@ function App() { const hostConfig = getHostConfig(); return ( - - - }> - - {/* Public Routes - immer verfügbar */} - } /> - } /> + + + + }> + + {/* Public Routes - immer verfügbar */} + } /> + } /> + + {/* Error Pages */} + } /> + } /> + } /> + } /> {/* Internal Only Routes - nur auf internal host geladen */} {hostConfig.isInternal && ( @@ -112,6 +124,7 @@ function App() { + ); } diff --git a/frontend/src/Components/ComponentUtils/ConsentManager.js b/frontend/src/Components/ComponentUtils/ConsentManager.js index ab9a248..58c4b48 100644 --- a/frontend/src/Components/ComponentUtils/ConsentManager.js +++ b/frontend/src/Components/ComponentUtils/ConsentManager.js @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { Box, Alert, Typography } from '@mui/material'; import ConsentCheckboxes from './MultiUpload/ConsentCheckboxes'; +import { apiFetch } from '../../Utils/apiFetch'; /** * Manages consents with save functionality @@ -148,7 +149,7 @@ function ConsentManager({ // Save each change for (const change of changes) { - const res = await fetch(`/api/manage/${token}/consents`, { + const res = await apiFetch(`/api/manage/${token}/consents`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(change) @@ -235,11 +236,11 @@ function ConsentManager({ {/* Email Hint after successful save */} {showEmailHint && successMessage && ( - Wichtig: Bitte senden Sie jetzt eine E-Mail an{' '} + Wichtig: Bitte sende eine E-Mail an{' '} info@hobbyhimmel.de {' '} - mit Ihrer Gruppen-ID, um die Löschung Ihrer Bilder auf den Social Media Plattformen anzufordern. + mit Deiner Gruppen-ID, um die Löschung Deiner Bilder auf den Social Media Plattformen anzufordern. )} diff --git a/frontend/src/Components/ComponentUtils/Css/Footer.css b/frontend/src/Components/ComponentUtils/Css/Footer.css index 1989cdc..0dc429a 100644 --- a/frontend/src/Components/ComponentUtils/Css/Footer.css +++ b/frontend/src/Components/ComponentUtils/Css/Footer.css @@ -3,7 +3,7 @@ text-align: right; font-size: 11px; color: #808080; - font-family: "Roboto", sans-serif; + font-family: "Open Sans", sans-serif; font-weight: lighter; margin: 0; padding-right: 20px; @@ -23,7 +23,7 @@ footer { footer a { font-size: 11px; color: #777; - font-family: "Roboto", sans-serif; + font-family: "Open Sans", sans-serif; font-weight: lighter; text-decoration: none; transition: color 0.2s ease; diff --git a/frontend/src/Components/ComponentUtils/Css/ImageGallery.css b/frontend/src/Components/ComponentUtils/Css/ImageGallery.css index 96b0cdc..9b22860 100644 --- a/frontend/src/Components/ComponentUtils/Css/ImageGallery.css +++ b/frontend/src/Components/ComponentUtils/Css/ImageGallery.css @@ -185,7 +185,7 @@ .image-gallery-title { margin-bottom: 15px; - font-family: 'Roboto', sans-serif; + font-family: 'Open Sans', sans-serif; color: #333; font-size: 1.5rem; font-weight: 500; @@ -294,7 +294,7 @@ padding: 8px; border: 1px solid #ccc; border-radius: 4px; - font-family: 'Roboto', sans-serif; + font-family: 'Open Sans', sans-serif; font-size: 14px; resize: vertical; min-height: 50px; diff --git a/frontend/src/Components/ComponentUtils/Css/Navbar.css b/frontend/src/Components/ComponentUtils/Css/Navbar.css index f7f7f88..c881388 100644 --- a/frontend/src/Components/ComponentUtils/Css/Navbar.css +++ b/frontend/src/Components/ComponentUtils/Css/Navbar.css @@ -9,7 +9,7 @@ header { .logo { margin-right: auto; color: #ECF0F1; - font-family: 'Montserrat', sans-serif; + font-family: 'Open Sans', sans-serif; font-size: 20px; display: flex; flex-direction: row; @@ -33,7 +33,7 @@ header { .nav__links a, .cta, .overlay__content a { - font-family: "Montserrat", sans-serif; + font-family: "Open Sans", sans-serif; font-weight: 500; color: #edf0f1; text-decoration: none; diff --git a/frontend/src/Components/ComponentUtils/DeleteGroupButton.js b/frontend/src/Components/ComponentUtils/DeleteGroupButton.js index 64096cf..eb30539 100644 --- a/frontend/src/Components/ComponentUtils/DeleteGroupButton.js +++ b/frontend/src/Components/ComponentUtils/DeleteGroupButton.js @@ -3,6 +3,7 @@ import { Button } from '@mui/material'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; import Swal from 'sweetalert2'; import { useNavigate } from 'react-router-dom'; +import { apiFetch } from '../../Utils/apiFetch'; /** * Delete group button with confirmation dialog @@ -41,7 +42,7 @@ function DeleteGroupButton({ token, groupName = 'diese Gruppe' }) { try { setDeleting(true); - const res = await fetch(`/api/manage/${token}`, { + const res = await apiFetch(`/api/manage/${token}`, { method: 'DELETE' }); diff --git a/frontend/src/Components/ComponentUtils/ErrorAnimation/ErrorAnimation.css b/frontend/src/Components/ComponentUtils/ErrorAnimation/ErrorAnimation.css new file mode 100644 index 0000000..80dcfb0 --- /dev/null +++ b/frontend/src/Components/ComponentUtils/ErrorAnimation/ErrorAnimation.css @@ -0,0 +1,78 @@ +/* ErrorAnimation Component Styles */ + +.error-animation-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 400px; + position: relative; + perspective: 1000px; + margin: 40px 0; +} + +.error-rotor { + display: inline-block; + transform-origin: center; + transform-style: preserve-3d; + will-change: transform; + animation: errorRotateY 4s linear infinite; +} + +.error-logo { + display: block; + width: 400px; + height: auto; +} + +.error-logo #g136 { + transform-box: fill-box; + transform-origin: center; + will-change: transform; + animation: errorRotateSegments 3s linear infinite; +} + +@keyframes errorRotateY { + 0% { + transform: rotateY(0deg); + } + 50% { + transform: rotateY(90deg); + } + 100% { + transform: rotateY(0deg); + } +} + +@keyframes errorRotateSegments { + 0% { + transform: rotate3d(1, -1, 0, 0deg); + } + 50% { + transform: rotate3d(1, -1, 0, 90deg); + } + 100% { + transform: rotate3d(1, -1, 0, 0deg); + } +} + +/* Responsive Sizing */ +@media (max-width: 600px) { + .error-logo { + width: 300px; + } + + .error-animation-container { + height: 300px; + } +} + +@media (max-width: 400px) { + .error-logo { + width: 250px; + } + + .error-animation-container { + height: 250px; + } +} diff --git a/frontend/src/Components/ComponentUtils/ErrorAnimation/ErrorAnimation.js b/frontend/src/Components/ComponentUtils/ErrorAnimation/ErrorAnimation.js new file mode 100644 index 0000000..2bb5098 --- /dev/null +++ b/frontend/src/Components/ComponentUtils/ErrorAnimation/ErrorAnimation.js @@ -0,0 +1,146 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './ErrorAnimation.css'; + +/** + * ErrorAnimation Component + * Zeigt eine animierte Wolke mit einem Fehlercode in Sieben-Segment-Anzeige + * + * @param {string} errorCode - Der anzuzeigende Fehlercode (z.B. "404", "403", "500") + */ +const ErrorAnimation = ({ errorCode = "404" }) => { + // Sieben-Segment-Mapping: welche Segmente für welche Ziffer leuchten + const segmentPatterns = { + '0': ['a', 'b', 'c', 'd', 'e', 'f'], + '1': ['b', 'c'], + '2': ['a', 'b', 'd', 'e', 'g'], + '3': ['a', 'b', 'c', 'd', 'g'], + '4': ['b', 'c', 'f', 'g'], + '5': ['a', 'c', 'd', 'f', 'g'], + '6': ['a', 'c', 'd', 'e', 'f', 'g'], + '7': ['a', 'b', 'c'], + '8': ['a', 'b', 'c', 'd', 'e', 'f', 'g'], + '9': ['a', 'b', 'c', 'd', 'f', 'g'] + }; + + // Segment-Zuordnung zu Polygon-IDs (Position im Array = Segment) + const segmentOrder = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; + + // Fehlercode auf max 3 Ziffern begrenzen und mit Leerzeichen auffüllen + const displayCode = errorCode.toString().padStart(3, ' ').slice(0, 3); + const digits = displayCode.split(''); + + /** + * Bestimmt die Füllfarbe für ein Segment + * @param {string} digit - Die Ziffer (0-9 oder Leerzeichen) + * @param {string} segment - Das Segment (a-g) + * @returns {string} - Hex-Farbcode + */ + const getSegmentColor = (digit, segment) => { + if (digit === ' ') return '#ffffff'; // Leerzeichen = alle aus + const pattern = segmentPatterns[digit]; + return pattern && pattern.includes(segment) ? '#76b043' : '#ffffff'; + }; + + /** + * Generiert Polygon-Elemente für eine Ziffer + * @param {number} digitIndex - Position der Ziffer (0-2) + * @returns {JSX.Element[]} - Array von Polygon-Elementen + */ + const renderDigit = (digitIndex) => { + const digit = digits[digitIndex]; + // Mapping: digitIndex 0 (links) = g1800, digitIndex 1 (mitte) = g1758, digitIndex 2 (rechts) = g1782 + const baseIds = { + 0: { group: 'g1800', polygons: ['polygon1786', 'polygon1788', 'polygon1790', 'polygon1792', 'polygon1794', 'polygon1796', 'polygon1798'] }, + 1: { group: 'g1758', polygons: ['polygon1573', 'polygon1575', 'polygon1577', 'polygon1579', 'polygon1581', 'polygon1583', 'polygon1585'] }, + 2: { group: 'g1782', polygons: ['polygon1768', 'polygon1770', 'polygon1772', 'polygon1774', 'polygon1776', 'polygon1778', 'polygon1780'] } + }; + + const transforms = { + 0: 'translate(47.970487,-113.03641)', + 1: 'translate(113.66502,-113.03641)', + 2: 'translate(179.35956,-113.03641)' + }; + + const { group, polygons } = baseIds[digitIndex]; + const transform = transforms[digitIndex]; + + const polyPoints = [ + '20,20 10,10 20,0 60,0 70,10 60,20', // a (oben) + '60,20 70,10 80,20 80,40 70,50 60,40', // b (rechts oben) + '80,60 80,80 70,90 60,80 60,60 70,50', // c (rechts unten) + '20,80 60,80 70,90 60,100 20,100 10,90', // d (unten) + '10,80 0,90 -10,80 -10,60 0,50 10,60', // e (links unten) + '10,20 10,40 0,50 -10,40 -10,20 0,10', // f (links oben) + '20,60 10,50 20,40 60,40 70,50 60,60' // g (mitte) + ]; + + const polyTransforms = [ + 'matrix(0.67523047,0,0,0.67523047,117.69293,49.286325)', + 'matrix(0.67523047,0,0,0.67523047,119.69293,51.286325)', + 'matrix(0.67523047,0,0,0.67523047,119.69293,55.286325)', + 'matrix(0.67523047,0,0,0.67523047,117.69293,57.286325)', + 'matrix(0.67523047,0,0,0.67523047,122.44524,55.286325)', + 'matrix(0.67523047,0,0,0.67523047,122.44524,51.286325)', + 'matrix(0.67523047,0,0,0.67523047,117.69293,53.286325)' + ]; + + return ( + + {segmentOrder.map((segment, idx) => ( + + ))} + + ); + }; + + return ( +
+
+ + {/* Wolke (g561) - bleibt immer gleich */} + + + + + {/* Sieben-Segment-Anzeige (g136) - dynamisch generiert */} + + + {renderDigit(0)} + {renderDigit(1)} + {renderDigit(2)} + + + +
+
+ ); +}; + +ErrorAnimation.propTypes = { + errorCode: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]) +}; + +export default ErrorAnimation; diff --git a/frontend/src/Components/ComponentUtils/ErrorBoundary.js b/frontend/src/Components/ComponentUtils/ErrorBoundary.js new file mode 100644 index 0000000..68857fd --- /dev/null +++ b/frontend/src/Components/ComponentUtils/ErrorBoundary.js @@ -0,0 +1,38 @@ +import React from 'react'; +import InternalServerErrorPage from '../Pages/500Page'; + +/** + * Error Boundary Component + * Fängt React-Fehler ab und zeigt die 500-Error-Page an + */ +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + // Log error details for debugging + console.error('ErrorBoundary caught an error:', error, errorInfo); + this.setState({ + error, + errorInfo + }); + } + + render() { + if (this.state.hasError) { + // Render 500 Error Page + return ; + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js b/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js index c67ca00..4e603b0 100644 --- a/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js +++ b/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js @@ -4,6 +4,7 @@ import Swal from 'sweetalert2'; import DescriptionInput from './MultiUpload/DescriptionInput'; import { adminRequest } from '../../services/adminApi'; import { handleAdminError } from '../../services/adminErrorHandler'; +import { apiFetch } from '../../Utils/apiFetch'; /** * Manages group metadata with save functionality @@ -76,7 +77,7 @@ function GroupMetadataEditor({ if (isModerateMode) { await adminRequest(endpoint, method, metadata); } else { - const res = await fetch(endpoint, { + const res = await apiFetch(endpoint, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(metadata) diff --git a/frontend/src/Components/ComponentUtils/ImageDescriptionManager.js b/frontend/src/Components/ComponentUtils/ImageDescriptionManager.js index 4b3b9ad..25cfba8 100644 --- a/frontend/src/Components/ComponentUtils/ImageDescriptionManager.js +++ b/frontend/src/Components/ComponentUtils/ImageDescriptionManager.js @@ -4,6 +4,7 @@ import Swal from 'sweetalert2'; import ImageGallery from './ImageGallery'; import { adminRequest } from '../../services/adminApi'; import { handleAdminError } from '../../services/adminErrorHandler'; +import { apiFetch } from '../../Utils/apiFetch'; /** * Manages image descriptions with save functionality @@ -49,7 +50,7 @@ function ImageDescriptionManager({ if (mode === 'moderate') { await adminRequest(endpoint, 'DELETE'); } else { - const res = await fetch(endpoint, { + const res = await apiFetch(endpoint, { method: 'DELETE' }); @@ -138,7 +139,7 @@ function ImageDescriptionManager({ if (mode === 'moderate') { await adminRequest(endpoint, method, { descriptions }); } else { - const res = await fetch(endpoint, { + const res = await apiFetch(endpoint, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ descriptions }) diff --git a/frontend/src/Components/Pages/403Page.js b/frontend/src/Components/Pages/403Page.js new file mode 100644 index 0000000..ef68e0c --- /dev/null +++ b/frontend/src/Components/Pages/403Page.js @@ -0,0 +1,30 @@ +import React from 'react' +import Navbar from '../ComponentUtils/Headers/Navbar' +import NavbarUpload from '../ComponentUtils/Headers/NavbarUpload' +import ErrorAnimation from '../ComponentUtils/ErrorAnimation/ErrorAnimation' +import { getHostConfig } from '../../Utils/hostDetection' + +import './Css/ErrorPage.css' + +function ForbiddenPage() { + const hostConfig = getHostConfig(); + + return ( +
+ {hostConfig.isPublic ? : } + +
+
+

403 - Zugriff verweigert

+

Sie haben keine Berechtigung, auf diese Ressource zuzugreifen.

+ + + Zurück zur Startseite + +
+
+
+ ) +} + +export default ForbiddenPage diff --git a/frontend/src/Components/Pages/404Page.js b/frontend/src/Components/Pages/404Page.js index 86db3d2..a8897e9 100644 --- a/frontend/src/Components/Pages/404Page.js +++ b/frontend/src/Components/Pages/404Page.js @@ -1,9 +1,11 @@ import React from 'react' import Navbar from '../ComponentUtils/Headers/Navbar' import NavbarUpload from '../ComponentUtils/Headers/NavbarUpload' +import ErrorAnimation from '../ComponentUtils/ErrorAnimation/ErrorAnimation' import { getHostConfig } from '../../Utils/hostDetection' -import './Css/404Page.css' +import './Css/ErrorPage.css' +import '../../App.css' function FZF() { const hostConfig = getHostConfig(); @@ -12,33 +14,25 @@ function FZF() {
{hostConfig.isPublic ? : } -
+
{hostConfig.isPublic ? (

404 - Diese Funktion ist nicht verfügbar

Diese Funktion ist nur über das interne Netzwerk erreichbar.

+ Zurück zum Upload
) : ( - <> - - - - - - - - - - - - - - - - +
+

404 - Seite nicht gefunden

+

Die angeforderte Seite existiert nicht.

+ + + Zurück zur Startseite + +
)}
diff --git a/frontend/src/Components/Pages/500Page.js b/frontend/src/Components/Pages/500Page.js new file mode 100644 index 0000000..07c4e04 --- /dev/null +++ b/frontend/src/Components/Pages/500Page.js @@ -0,0 +1,30 @@ +import React from 'react' +import Navbar from '../ComponentUtils/Headers/Navbar' +import NavbarUpload from '../ComponentUtils/Headers/NavbarUpload' +import ErrorAnimation from '../ComponentUtils/ErrorAnimation/ErrorAnimation' +import { getHostConfig } from '../../Utils/hostDetection' + +import './Css/ErrorPage.css' + +function InternalServerErrorPage() { + const hostConfig = getHostConfig(); + + return ( +
+ {hostConfig.isPublic ? : } + +
+
+

500 - Interner Serverfehler

+

Es ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es später erneut.

+ + + Zurück zur Startseite + +
+
+
+ ) +} + +export default InternalServerErrorPage diff --git a/frontend/src/Components/Pages/502Page.js b/frontend/src/Components/Pages/502Page.js new file mode 100644 index 0000000..1917635 --- /dev/null +++ b/frontend/src/Components/Pages/502Page.js @@ -0,0 +1,30 @@ +import React from 'react' +import Navbar from '../ComponentUtils/Headers/Navbar' +import NavbarUpload from '../ComponentUtils/Headers/NavbarUpload' +import ErrorAnimation from '../ComponentUtils/ErrorAnimation/ErrorAnimation' +import { getHostConfig } from '../../Utils/hostDetection' + +import './Css/ErrorPage.css' + +function BadGatewayPage() { + const hostConfig = getHostConfig(); + + return ( +
+ {hostConfig.isPublic ? : } + +
+
+

502 - Bad Gateway

+

Der Server hat eine ungültige Antwort erhalten. Bitte versuchen Sie es später erneut.

+ + + Zurück zur Startseite + +
+
+
+ ) +} + +export default BadGatewayPage diff --git a/frontend/src/Components/Pages/503Page.js b/frontend/src/Components/Pages/503Page.js new file mode 100644 index 0000000..88edbde --- /dev/null +++ b/frontend/src/Components/Pages/503Page.js @@ -0,0 +1,30 @@ +import React from 'react' +import Navbar from '../ComponentUtils/Headers/Navbar' +import NavbarUpload from '../ComponentUtils/Headers/NavbarUpload' +import ErrorAnimation from '../ComponentUtils/ErrorAnimation/ErrorAnimation' +import { getHostConfig } from '../../Utils/hostDetection' + +import './Css/ErrorPage.css' + +function ServiceUnavailablePage() { + const hostConfig = getHostConfig(); + + return ( +
+ {hostConfig.isPublic ? : } + +
+
+

503 - Service nicht verfügbar

+

Der Service ist vorübergehend nicht verfügbar. Bitte versuchen Sie es später erneut.

+ + + Zurück zur Startseite + +
+
+
+ ) +} + +export default ServiceUnavailablePage diff --git a/frontend/src/Components/Pages/Css/404Page.css b/frontend/src/Components/Pages/Css/404Page.css deleted file mode 100644 index ebd479b..0000000 --- a/frontend/src/Components/Pages/Css/404Page.css +++ /dev/null @@ -1,68 +0,0 @@ -.container404{ - margin-top: 25vh; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - -webkit-flex-wrap: nowrap; - -ms-flex-wrap: nowrap; - flex-wrap: nowrap; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-content: center; - -ms-flex-line-pack: center; - align-content: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; -} - -.page404 { - width: 400px; - height: auto; -} - -#tree{ - stroke: #59513C; -} - -#wood-stump{ - stroke: #59513C; - -webkit-animation: wood-stump 3s infinite ease-in-out; - -moz-animation: wood-stump 3s infinite ease-in-out; - -o-animation: wood-stump 3s infinite ease-in-out; - animation: wood-stump 3s infinite ease-in-out; -} - -@-webkit-keyframes wood-stump{ 0% { -webkit-transform: translate(100px) } 50% { -webkit-transform: translate(50px); } 100% { -webkit-transform: translate(100px); } } -@-moz-keyframes wood-stump{ 0% { -moz-transform: translate(100px); } 50% { -moz-transform: translate(50px); } 100% { -moz-transform: translate(100px); } } -@-o-keyframes wood-stump{ 0% { -o-transform: translate(100px); } 50% { -o-transform: translate(50px); } 100% { -o-transform: translate(100px); } } -@keyframes wood-stump{ 0% {-webkit-transform: translate(100px);-moz-transform: translate(100px);-ms-transform: translate(100px);transform: translate(100px); } 50% {-webkit-transform: translate(0px);-moz-transform: translate(0px);-ms-transform: translate(0px);transform: translate(0px); } 100% {-webkit-transform: translate(100px); -moz-transform: translate(100px);-ms-transform: translate(100px);transform: translate(100px); } } - - -#leaf{ - stroke: #59513C; - -webkit-animation: leaf 7s infinite ease-in-out; - -moz-animation: leaf 7s infinite ease-in-out; - -o-animation: leaf 7s infinite ease-in-out; - animation: leaf 7s infinite ease-in-out; -} - -@-webkit-keyframes leaf{ 0% { -webkit-transform: translate(0, 70px) } 50% { -webkit-transform: translate(0, 50px); } 100% { -webkit-transform: translate(0, 70px); } } -@-moz-keyframes leaf{ 0% { -moz-transform: translate(0, 70px); } 50% { -moz-transform: translate(0, 50px); } 100% { -moz-transform: translate(0, 70px); } } -@-o-keyframes leaf{ 0% { -o-transform: translate(0, 70px); } 50% { -o-transform: translate(0, 50px); } 100% { -o-transform: translate(0, 70px); } } -@keyframes leaf{ 0% {-webkit-transform: translate(0, 70px);-moz-transform: translate(0, 70px);-ms-transform: translate(0, 70px);transform: translate(0, 70px); } 50% {-webkit-transform: translate(0px);-moz-transform: translate(0px);-ms-transform: translate(0px);transform: translate(0px); } 100% {-webkit-transform: translate(0, 70px); -moz-transform: translate(0, 70px);-ms-transform: translate(0, 70px);transform: translate(0, 70px); } } - -#border{ - stroke: #59513C; -} - -#Page{ - fill: #59513C; -} -#notFound{ - fill: #A7444B; -} diff --git a/frontend/src/Components/Pages/Css/ErrorPage.css b/frontend/src/Components/Pages/Css/ErrorPage.css new file mode 100644 index 0000000..8bf1447 --- /dev/null +++ b/frontend/src/Components/Pages/Css/ErrorPage.css @@ -0,0 +1,10 @@ +/* Error Pages Container */ +.containerError { + margin-top: 25vh; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: center; + align-content: center; + align-items: center; +} diff --git a/frontend/src/Components/Pages/ManagementPortalPage.js b/frontend/src/Components/Pages/ManagementPortalPage.js index 03e7546..acca180 100644 --- a/frontend/src/Components/Pages/ManagementPortalPage.js +++ b/frontend/src/Components/Pages/ManagementPortalPage.js @@ -12,6 +12,7 @@ import ImageDescriptionManager from '../ComponentUtils/ImageDescriptionManager'; import GroupMetadataEditor from '../ComponentUtils/GroupMetadataEditor'; import ConsentManager from '../ComponentUtils/ConsentManager'; import DeleteGroupButton from '../ComponentUtils/DeleteGroupButton'; +import { apiFetch } from '../../Utils/apiFetch'; /** * ManagementPortalPage - Self-service management for uploaded groups @@ -36,7 +37,7 @@ function ManagementPortalPage() { setLoading(true); setError(null); - const res = await fetch(`/api/manage/${token}`); + const res = await apiFetch(`/api/manage/${token}`); if (res.status === 404) { setError('Ungültiger oder abgelaufener Verwaltungslink'); @@ -105,7 +106,7 @@ function ManagementPortalPage() { formData.append('images', file); }); - const res = await fetch(`/api/manage/${token}/images`, { + const res = await apiFetch(`/api/manage/${token}/images`, { method: 'POST', body: formData }); @@ -146,7 +147,7 @@ function ManagementPortalPage() { const imageIds = newOrder.map(img => img.id); // Use token-based management API - const response = await fetch(`/api/manage/${token}/reorder`, { + const response = await apiFetch(`/api/manage/${token}/reorder`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageIds: imageIds }) diff --git a/frontend/src/Components/Pages/PublicGroupImagesPage.js b/frontend/src/Components/Pages/PublicGroupImagesPage.js index 5c9bec9..7d01be8 100644 --- a/frontend/src/Components/Pages/PublicGroupImagesPage.js +++ b/frontend/src/Components/Pages/PublicGroupImagesPage.js @@ -5,6 +5,7 @@ import Navbar from '../ComponentUtils/Headers/Navbar'; import Footer from '../ComponentUtils/Footer'; import ImageGalleryCard from '../ComponentUtils/ImageGalleryCard'; import ImageGallery from '../ComponentUtils/ImageGallery'; +import { apiFetch } from '../../Utils/apiFetch'; const PublicGroupImagesPage = () => { @@ -22,7 +23,7 @@ const PublicGroupImagesPage = () => { try { setLoading(true); // Public endpoint (no moderation controls) - const res = await fetch(`/api/groups/${groupId}`); + const res = await apiFetch(`/api/groups/${groupId}`); if (!res.ok) throw new Error('Nicht gefunden'); const data = await res.json(); setGroup(data); diff --git a/frontend/src/Utils/apiClient.js b/frontend/src/Utils/apiClient.js new file mode 100644 index 0000000..1787fd7 --- /dev/null +++ b/frontend/src/Utils/apiClient.js @@ -0,0 +1,62 @@ +import axios from 'axios'; + +/** + * Axios instance with error handling interceptors + * Handles HTTP status codes and redirects to appropriate error pages + */ + +// Create axios instance +const apiClient = axios.create({ + baseURL: window._env_?.API_URL || '', + headers: { + 'Content-Type': 'application/json', + }, + withCredentials: true, // For session cookies +}); + +/** + * Response interceptor for error handling + */ +apiClient.interceptors.response.use( + (response) => { + // Pass through successful responses + return response; + }, + (error) => { + if (error.response) { + const { status } = error.response; + + // Handle specific HTTP status codes + switch (status) { + case 403: + // Forbidden - redirect to 403 page + window.location.href = '/error/403'; + break; + + case 500: + // Internal Server Error - redirect to 500 page + window.location.href = '/error/500'; + break; + + case 502: + // Bad Gateway - redirect to 502 page + window.location.href = '/error/502'; + break; + + case 503: + // Service Unavailable - redirect to 503 page + window.location.href = '/error/503'; + break; + + default: + // For other errors, just reject the promise + break; + } + } + + // Always reject the promise so calling code can handle it + return Promise.reject(error); + } +); + +export default apiClient; diff --git a/frontend/src/Utils/apiFetch.js b/frontend/src/Utils/apiFetch.js new file mode 100644 index 0000000..6bcca22 --- /dev/null +++ b/frontend/src/Utils/apiFetch.js @@ -0,0 +1,116 @@ +/** + * Enhanced Fetch Wrapper with Error Handling + * Automatically redirects to error pages based on HTTP status codes + * + * Note: adminApi.js uses its own adminFetch wrapper for CSRF token handling + * and should not be migrated to this wrapper. + */ + +const handleErrorResponse = (status) => { + switch (status) { + case 403: + window.location.href = '/error/403'; + break; + case 500: + window.location.href = '/error/500'; + break; + case 502: + window.location.href = '/error/502'; + break; + case 503: + window.location.href = '/error/503'; + break; + default: + // Don't redirect for other errors (400, 401, etc.) + break; + } +}; + +/** + * Enhanced fetch with automatic error page redirects + * @param {string} url - The URL to fetch + * @param {object} options - Fetch options + * @returns {Promise} - The response object + */ +export const apiFetch = async (url, options = {}) => { + try { + const response = await fetch(url, { + ...options, + credentials: options.credentials || 'include' + }); + + // If response is not ok, handle error + if (!response.ok) { + handleErrorResponse(response.status); + } + + return response; + } catch (error) { + // Network errors or other fetch failures + console.error('Fetch error:', error); + throw error; + } +}; + +/** + * Helper for GET requests + */ +export const apiGet = async (url) => { + const response = await apiFetch(url, { method: 'GET' }); + return response.json(); +}; + +/** + * Helper for POST requests + */ +export const apiPost = async (url, body = null, options = {}) => { + const fetchOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + ...options + }; + + if (body) { + fetchOptions.body = JSON.stringify(body); + } + + const response = await apiFetch(url, fetchOptions); + return response.json(); +}; + +/** + * Helper for PUT requests + */ +export const apiPut = async (url, body = null, options = {}) => { + const fetchOptions = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + ...options + }; + + if (body) { + fetchOptions.body = JSON.stringify(body); + } + + const response = await apiFetch(url, fetchOptions); + return response.json(); +}; + +/** + * Helper for DELETE requests + */ +export const apiDelete = async (url, options = {}) => { + const response = await apiFetch(url, { + method: 'DELETE', + ...options + }); + return response.json(); +}; + +export default apiFetch; diff --git a/frontend/src/Utils/batchUpload.js b/frontend/src/Utils/batchUpload.js index 8bcf8bb..0057b71 100644 --- a/frontend/src/Utils/batchUpload.js +++ b/frontend/src/Utils/batchUpload.js @@ -1,3 +1,5 @@ +import { apiFetch } from './apiFetch'; + // Batch-Upload Funktion für mehrere Bilder export const uploadImageBatch = async ({ images, metadata, imageDescriptions = {}, consents = null, onProgress }) => { if (!images || images.length === 0) { @@ -29,7 +31,7 @@ export const uploadImageBatch = async ({ images, metadata, imageDescriptions = { } try { - const response = await fetch('/api/upload/batch', { + const response = await apiFetch('/api/upload/batch', { method: 'POST', body: formData }); @@ -50,7 +52,7 @@ export const uploadImageBatch = async ({ images, metadata, imageDescriptions = { // Einzelne Gruppe abrufen export const fetchGroup = async (groupId) => { try { - const response = await fetch(`/api/groups/${groupId}`); + const response = await apiFetch(`/api/groups/${groupId}`); if (!response.ok) { const errorData = await response.json(); @@ -67,7 +69,7 @@ export const fetchGroup = async (groupId) => { // Alle Gruppen abrufen export const fetchAllGroups = async () => { try { - const response = await fetch('/api/groups'); + const response = await apiFetch('/api/groups'); if (!response.ok) { const errorData = await response.json(); @@ -84,7 +86,7 @@ export const fetchAllGroups = async () => { // Gruppe löschen export const deleteGroup = async (groupId) => { try { - const response = await fetch(`/api/groups/${groupId}`, { + const response = await apiFetch(`/api/groups/${groupId}`, { method: 'DELETE' }); diff --git a/frontend/src/Utils/sendRequest.js b/frontend/src/Utils/sendRequest.js index babc33a..b7e3106 100644 --- a/frontend/src/Utils/sendRequest.js +++ b/frontend/src/Utils/sendRequest.js @@ -1,4 +1,4 @@ -import axios from 'axios' +import apiClient from './apiClient' //import swal from 'sweetalert'; import Swal from 'sweetalert2/dist/sweetalert2.js' @@ -22,7 +22,7 @@ export async function sendRequest(file, handleLoading, handleResponse) { handleLoading() try { - const res = await axios.post(window._env_.API_URL + '/upload', formData, { + const res = await apiClient.post('/upload', formData, { headers: { "Content-Type": "multipart/form-data" } diff --git a/test-loading.html b/test-loading.html new file mode 100644 index 0000000..93f1caa --- /dev/null +++ b/test-loading.html @@ -0,0 +1,162 @@ + + + + + + Loading Animation Test + + + +

🎨 Loading Animation Test & Fehlerseiten-Design

+ + +
+

Original Loading Animation

+

Die Standard-Loading-Animation mit grünem Hammer

+
+
+ +
+
+
+ + + +