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
This commit is contained in:
parent
920a81e075
commit
25dda32c4e
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
149
frontend/ERROR_HANDLING.md
Normal file
149
frontend/ERROR_HANDLING.md
Normal file
|
|
@ -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
|
||||
```
|
||||
|
|
@ -18,8 +18,7 @@
|
|||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/logo-180.png" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://kit.fontawesome.com/fc1a4a5a71.js" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="@sweetalert2/theme-material-ui/material-ui.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Raleway:wght@100&display=swap" rel="stylesheet">
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<AdminSessionProvider>
|
||||
<Router>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Routes>
|
||||
{/* Public Routes - immer verfügbar */}
|
||||
<Route path="/" element={<MultiUploadPage />} />
|
||||
<Route path="/manage/:token" element={<ManagementPortalPage />} />
|
||||
<ErrorBoundary>
|
||||
<AdminSessionProvider>
|
||||
<Router>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Routes>
|
||||
{/* Public Routes - immer verfügbar */}
|
||||
<Route path="/" element={<MultiUploadPage />} />
|
||||
<Route path="/manage/:token" element={<ManagementPortalPage />} />
|
||||
|
||||
{/* Error Pages */}
|
||||
<Route path="/error/403" element={<ForbiddenPage />} />
|
||||
<Route path="/error/500" element={<InternalServerErrorPage />} />
|
||||
<Route path="/error/502" element={<BadGatewayPage />} />
|
||||
<Route path="/error/503" element={<ServiceUnavailablePage />} />
|
||||
|
||||
{/* Internal Only Routes - nur auf internal host geladen */}
|
||||
{hostConfig.isInternal && (
|
||||
|
|
@ -112,6 +124,7 @@ function App() {
|
|||
</Suspense>
|
||||
</Router>
|
||||
</AdminSessionProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
<strong>Wichtig:</strong> Bitte senden Sie jetzt eine E-Mail an{' '}
|
||||
<strong>Wichtig:</strong> Bitte sende eine E-Mail an{' '}
|
||||
<a href="mailto:info@hobbyhimmel.de" style={{ color: '#2196F3', fontWeight: 'bold' }}>
|
||||
info@hobbyhimmel.de
|
||||
</a>{' '}
|
||||
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.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<g id={group} transform={transform} key={digitIndex}>
|
||||
{segmentOrder.map((segment, idx) => (
|
||||
<polygon
|
||||
key={polygons[idx]}
|
||||
id={polygons[idx]}
|
||||
points={polyPoints[idx]}
|
||||
transform={polyTransforms[idx]}
|
||||
style={{
|
||||
fill: getSegmentColor(digit, segment),
|
||||
fillOpacity: 1,
|
||||
stroke: 'none',
|
||||
strokeWidth: 2
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="error-animation-container">
|
||||
<div className="error-rotor">
|
||||
<svg
|
||||
className="error-logo"
|
||||
version="1.1"
|
||||
viewBox="0 0 289.40499 170.09499"
|
||||
>
|
||||
{/* Wolke (g561) - bleibt immer gleich */}
|
||||
<g id="g561" style={{ display: 'inline' }}>
|
||||
<path
|
||||
id="path1353"
|
||||
style={{ display: 'inline', fill: '#48484a' }}
|
||||
d="M 138.80469 0 C 97.587768 0 63.224812 29.321264 55.423828 68.242188 C 53.972832 68.119188 52.50934 68.042969 51.027344 68.042969 C 22.8464 68.042969 0 90.887413 0 119.06836 C 0 147.2483 22.8474 170.0957 51.027344 170.0957 C 65.865314 170.0957 210.51721 170.09375 225.61719 170.09375 C 260.84611 170.09375 289.4043 142.40467 289.4043 107.17773 C 289.4053 71.952807 260.84808 43.392578 225.61914 43.392578 C 221.50914 43.392578 217.49456 43.796064 213.60156 44.539062 C 199.2046 18.011166 171.10863 0 138.80469 0 z M 171.96289 40.238281 A 39.540237 71.54811 46.312638 0 1 192.97852 47.357422 A 39.540237 71.54811 46.312638 0 1 170.08984 124.95117 A 39.540237 71.54811 46.312638 0 1 90.582031 147.28711 A 39.540237 71.54811 46.312638 0 1 113.4707 69.695312 A 39.540237 71.54811 46.312638 0 1 171.96289 40.238281 z"
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* Sieben-Segment-Anzeige (g136) - dynamisch generiert */}
|
||||
<g id="g136">
|
||||
<g id="siebensegment" transform="matrix(0.46393276,-0.46393277,0.46393277,0.46393276,33.958225,228.89983)" style={{ display: 'inline' }}>
|
||||
{renderDigit(0)}
|
||||
{renderDigit(1)}
|
||||
{renderDigit(2)}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ErrorAnimation.propTypes = {
|
||||
errorCode: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
])
|
||||
};
|
||||
|
||||
export default ErrorAnimation;
|
||||
38
frontend/src/Components/ComponentUtils/ErrorBoundary.js
Normal file
38
frontend/src/Components/ComponentUtils/ErrorBoundary.js
Normal file
|
|
@ -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 <InternalServerErrorPage />;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
30
frontend/src/Components/Pages/403Page.js
Normal file
30
frontend/src/Components/Pages/403Page.js
Normal file
|
|
@ -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 (
|
||||
<div className="allContainerNoBackground">
|
||||
{hostConfig.isPublic ? <NavbarUpload /> : <Navbar />}
|
||||
|
||||
<div className="containerError">
|
||||
<div style={{ textAlign: 'center', marginTop: '2rem' }}>
|
||||
<h1>403 - Zugriff verweigert</h1>
|
||||
<p>Sie haben keine Berechtigung, auf diese Ressource zuzugreifen.</p>
|
||||
<ErrorAnimation errorCode="403" />
|
||||
<a href="/" style={{ color: '#007bff', textDecoration: 'underline' }}>
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForbiddenPage
|
||||
File diff suppressed because one or more lines are too long
30
frontend/src/Components/Pages/500Page.js
Normal file
30
frontend/src/Components/Pages/500Page.js
Normal file
|
|
@ -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 (
|
||||
<div className="allContainerNoBackground">
|
||||
{hostConfig.isPublic ? <NavbarUpload /> : <Navbar />}
|
||||
|
||||
<div className="containerError">
|
||||
<div style={{ textAlign: 'center', marginTop: '2rem' }}>
|
||||
<h1>500 - Interner Serverfehler</h1>
|
||||
<p>Es ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es später erneut.</p>
|
||||
<ErrorAnimation errorCode="500" />
|
||||
<a href="/" style={{ color: '#007bff', textDecoration: 'underline' }}>
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InternalServerErrorPage
|
||||
30
frontend/src/Components/Pages/502Page.js
Normal file
30
frontend/src/Components/Pages/502Page.js
Normal file
|
|
@ -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 (
|
||||
<div className="allContainerNoBackground">
|
||||
{hostConfig.isPublic ? <NavbarUpload /> : <Navbar />}
|
||||
|
||||
<div className="containerError">
|
||||
<div style={{ textAlign: 'center', marginTop: '2rem' }}>
|
||||
<h1>502 - Bad Gateway</h1>
|
||||
<p>Der Server hat eine ungültige Antwort erhalten. Bitte versuchen Sie es später erneut.</p>
|
||||
<ErrorAnimation errorCode="502" />
|
||||
<a href="/" style={{ color: '#007bff', textDecoration: 'underline' }}>
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BadGatewayPage
|
||||
30
frontend/src/Components/Pages/503Page.js
Normal file
30
frontend/src/Components/Pages/503Page.js
Normal file
|
|
@ -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 (
|
||||
<div className="allContainerNoBackground">
|
||||
{hostConfig.isPublic ? <NavbarUpload /> : <Navbar />}
|
||||
|
||||
<div className="containerError">
|
||||
<div style={{ textAlign: 'center', marginTop: '2rem' }}>
|
||||
<h1>503 - Service nicht verfügbar</h1>
|
||||
<p>Der Service ist vorübergehend nicht verfügbar. Bitte versuchen Sie es später erneut.</p>
|
||||
<ErrorAnimation errorCode="503" />
|
||||
<a href="/" style={{ color: '#007bff', textDecoration: 'underline' }}>
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceUnavailablePage
|
||||
|
|
@ -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;
|
||||
}
|
||||
10
frontend/src/Components/Pages/Css/ErrorPage.css
Normal file
10
frontend/src/Components/Pages/Css/ErrorPage.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
62
frontend/src/Utils/apiClient.js
Normal file
62
frontend/src/Utils/apiClient.js
Normal file
|
|
@ -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;
|
||||
116
frontend/src/Utils/apiFetch.js
Normal file
116
frontend/src/Utils/apiFetch.js
Normal file
|
|
@ -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<Response>} - 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;
|
||||
|
|
@ -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'
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
162
test-loading.html
Normal file
162
test-loading.html
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Loading Animation Test</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Roboto', Arial, sans-serif;
|
||||
background-color: whitesmoke;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
margin-bottom: 30px;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Loading Animation Styles */
|
||||
.loading-logo-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
position: relative;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.rotor {
|
||||
display: inline-block;
|
||||
transform-origin: center;
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
animation: rotateY 4s linear infinite;
|
||||
}
|
||||
|
||||
.loading-logo {
|
||||
display: block;
|
||||
width: 400px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.loading-logo #g136 {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
will-change: transform;
|
||||
animation: rotateHammerAxis 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotateY {
|
||||
from {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotateY(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotateHammerAxis {
|
||||
from {
|
||||
transform: rotate3d(1, -1, 0, 0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate3d(1, -1, 0, 360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🎨 Loading Animation Test & Fehlerseiten-Design</h1>
|
||||
|
||||
<!-- Original Loading Animation -->
|
||||
<div class="demo-section">
|
||||
<h2>Original Loading Animation</h2>
|
||||
<p class="demo-description">Die Standard-Loading-Animation mit grünem Hammer</p>
|
||||
<div class="loading-logo-container">
|
||||
<div class="rotor">
|
||||
<svg
|
||||
class="loading-logo"
|
||||
version="1.1"
|
||||
viewBox="0 0 289.40499 170.09499"
|
||||
id="svg264"
|
||||
>
|
||||
<g id="g561" style="display:inline">
|
||||
<path id="path1353" style="display:inline;fill:#48484a" d="M 138.80469 0 C 97.587768 0 63.224812 29.321264 55.423828 68.242188 C 53.972832 68.119188 52.50934 68.042969 51.027344 68.042969 C 22.8464 68.042969 0 90.887413 0 119.06836 C 0 147.2483 22.8474 170.0957 51.027344 170.0957 C 65.865314 170.0957 210.51721 170.09375 225.61719 170.09375 C 260.84611 170.09375 289.4043 142.40467 289.4043 107.17773 C 289.4053 71.952807 260.84808 43.392578 225.61914 43.392578 C 221.50914 43.392578 217.49456 43.796064 213.60156 44.539062 C 199.2046 18.011166 171.10863 0 138.80469 0 z M 171.96289 40.238281 A 39.540237 71.54811 46.312638 0 1 192.97852 47.357422 A 39.540237 71.54811 46.312638 0 1 170.08984 124.95117 A 39.540237 71.54811 46.312638 0 1 90.582031 147.28711 A 39.540237 71.54811 46.312638 0 1 113.4707 69.695312 A 39.540237 71.54811 46.312638 0 1 171.96289 40.238281 z "/>
|
||||
</g>
|
||||
<g id="g136">
|
||||
<g id="siebensegment" transform="matrix(0.46393276,-0.46393277,0.46393277,0.46393276,33.958225,228.89983)" style="display:inline">
|
||||
<g id="g1758" transform="translate(113.66502,-113.03641)">
|
||||
<polygon points="20,20 10,10 20,0 60,0 70,10 60,20 " id="polygon1573" transform="matrix(0.67523047,0,0,0.67523047,117.69293,49.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="60,20 70,10 80,20 80,40 70,50 60,40 " id="polygon1575" transform="matrix(0.67523047,0,0,0.67523047,119.69293,51.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="80,60 80,80 70,90 60,80 60,60 70,50 " id="polygon1577" transform="matrix(0.67523047,0,0,0.67523047,119.69293,55.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="20,80 60,80 70,90 60,100 20,100 10,90 " id="polygon1579" transform="matrix(0.67523047,0,0,0.67523047,117.69293,57.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="10,80 0,90 -10,80 -10,60 0,50 10,60 " id="polygon1581" transform="matrix(0.67523047,0,0,0.67523047,122.44524,55.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="10,20 10,40 0,50 -10,40 -10,20 0,10 " id="polygon1583" transform="matrix(0.67523047,0,0,0.67523047,122.44524,51.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="20,60 10,50 20,40 60,40 70,50 60,60 " id="polygon1585" transform="matrix(0.67523047,0,0,0.67523047,117.69293,53.286325)" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2"/>
|
||||
</g>
|
||||
<g id="g1782" transform="translate(179.35956,-113.03641)">
|
||||
<polygon points="70,10 60,20 20,20 10,10 20,0 60,0 " id="polygon1768" transform="matrix(0.67523047,0,0,0.67523047,117.69293,49.286325)" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2"/>
|
||||
<polygon points="70,50 60,40 60,20 70,10 80,20 80,40 " id="polygon1770" transform="matrix(0.67523047,0,0,0.67523047,119.69293,51.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="60,60 70,50 80,60 80,80 70,90 60,80 " id="polygon1772" transform="matrix(0.67523047,0,0,0.67523047,119.69293,55.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="20,100 10,90 20,80 60,80 70,90 60,100 " id="polygon1774" transform="matrix(0.67523047,0,0,0.67523047,117.69293,57.286325)" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2"/>
|
||||
<polygon points="0,50 10,60 10,80 0,90 -10,80 -10,60 " id="polygon1776" transform="matrix(0.67523047,0,0,0.67523047,122.44524,55.286325)" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2"/>
|
||||
<polygon points="-10,20 0,10 10,20 10,40 0,50 -10,40 " id="polygon1778" transform="matrix(0.67523047,0,0,0.67523047,122.44524,51.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="70,50 60,60 20,60 10,50 20,40 60,40 " id="polygon1780" transform="matrix(0.67523047,0,0,0.67523047,117.69293,53.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
</g>
|
||||
<g id="g1800" transform="translate(47.970487,-113.03641)">
|
||||
<polygon points="60,20 20,20 10,10 20,0 60,0 70,10 " id="polygon1786" transform="matrix(0.67523047,0,0,0.67523047,117.69293,49.286325)" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2"/>
|
||||
<polygon points="60,40 60,20 70,10 80,20 80,40 70,50 " id="polygon1788" transform="matrix(0.67523047,0,0,0.67523047,119.69293,51.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="70,50 80,60 80,80 70,90 60,80 60,60 " id="polygon1790" transform="matrix(0.67523047,0,0,0.67523047,119.69293,55.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="10,90 20,80 60,80 70,90 60,100 20,100 " id="polygon1792" transform="matrix(0.67523047,0,0,0.67523047,117.69293,57.286325)" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2"/>
|
||||
<polygon points="10,60 10,80 0,90 -10,80 -10,60 0,50 " id="polygon1794" transform="matrix(0.67523047,0,0,0.67523047,122.44524,55.286325)" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2"/>
|
||||
<polygon points="0,10 10,20 10,40 0,50 -10,40 -10,20 " id="polygon1796" transform="matrix(0.67523047,0,0,0.67523047,122.44524,51.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
<polygon points="60,60 20,60 10,50 20,40 60,40 70,50 " id="polygon1798" transform="matrix(0.67523047,0,0,0.67523047,117.69293,53.286325)" style="fill:#76b043;stroke:none;stroke-width:2"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user