- 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
281 lines
8.8 KiB
JavaScript
281 lines
8.8 KiB
JavaScript
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
|
|
* Wraps ConsentCheckboxes and provides save for workshop + social media consents
|
|
*
|
|
* @param mode - 'edit' (default) shows save/discard, 'upload' hides them
|
|
*/
|
|
function ConsentManager({
|
|
initialConsents,
|
|
consents: externalConsents,
|
|
onConsentsChange,
|
|
token,
|
|
groupId,
|
|
onRefresh,
|
|
mode = 'edit'
|
|
}) {
|
|
// Initialize with proper defaults
|
|
const defaultConsents = {
|
|
workshopConsent: false,
|
|
socialMediaConsents: []
|
|
};
|
|
|
|
const [consents, setConsents] = useState(defaultConsents);
|
|
const [originalConsents, setOriginalConsents] = useState(defaultConsents);
|
|
const [saving, setSaving] = useState(false);
|
|
const [initialized, setInitialized] = useState(false);
|
|
const [successMessage, setSuccessMessage] = useState('');
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
const [showEmailHint, setShowEmailHint] = useState(false);
|
|
|
|
// In upload mode: use external state
|
|
const isUploadMode = mode === 'upload';
|
|
const currentConsents = isUploadMode ? externalConsents : consents;
|
|
const setCurrentConsents = isUploadMode ? onConsentsChange : setConsents;
|
|
|
|
// Update ONLY ONCE when initialConsents first arrives (edit mode only)
|
|
React.useEffect(() => {
|
|
if (initialConsents && !initialized && !isUploadMode) {
|
|
// Deep copy to avoid shared references
|
|
const consentsCopy = {
|
|
workshopConsent: initialConsents.workshopConsent,
|
|
socialMediaConsents: [...(initialConsents.socialMediaConsents || [])]
|
|
};
|
|
setConsents(consentsCopy);
|
|
|
|
// Separate deep copy for original
|
|
const originalCopy = {
|
|
workshopConsent: initialConsents.workshopConsent,
|
|
socialMediaConsents: [...(initialConsents.socialMediaConsents || [])]
|
|
};
|
|
setOriginalConsents(originalCopy);
|
|
|
|
setInitialized(true);
|
|
}
|
|
}, [initialConsents, initialized, isUploadMode]);
|
|
|
|
const hasChanges = () => {
|
|
if (isUploadMode) return false; // No changes tracking in upload mode
|
|
// Check workshop consent
|
|
if (consents.workshopConsent !== originalConsents.workshopConsent) {
|
|
return true;
|
|
}
|
|
|
|
// Check social media consents - sort before comparing (order doesn't matter)
|
|
const currentIds = (consents.socialMediaConsents || []).map(c => c.platformId).sort((a, b) => a - b);
|
|
const originalIds = (originalConsents.socialMediaConsents || []).map(c => c.platformId).sort((a, b) => a - b);
|
|
|
|
// Different lengths = definitely changed
|
|
if (currentIds.length !== originalIds.length) {
|
|
return true;
|
|
}
|
|
|
|
// Compare sorted arrays element by element
|
|
for (let i = 0; i < currentIds.length; i++) {
|
|
if (currentIds[i] !== originalIds[i]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// Check if social media consent was revoked (for email hint)
|
|
const hasSocialMediaRevocations = () => {
|
|
const currentIds = new Set((consents.socialMediaConsents || []).map(c => c.platformId));
|
|
const originalIds = new Set((originalConsents.socialMediaConsents || []).map(c => c.platformId));
|
|
|
|
// Check if any original platform is missing in current
|
|
for (let platformId of originalIds) {
|
|
if (!currentIds.has(platformId)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!hasChanges()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setSaving(true);
|
|
setSuccessMessage('');
|
|
setErrorMessage('');
|
|
|
|
// Detect changes
|
|
const changes = [];
|
|
|
|
// Workshop consent change
|
|
if (consents.workshopConsent !== originalConsents.workshopConsent) {
|
|
changes.push({
|
|
consentType: 'workshop',
|
|
action: consents.workshopConsent ? 'restore' : 'revoke'
|
|
});
|
|
}
|
|
|
|
// Social media consent changes
|
|
const originalSocialIds = new Set(originalConsents.socialMediaConsents.map(c => c.platformId));
|
|
const currentSocialIds = new Set(consents.socialMediaConsents.map(c => c.platformId));
|
|
|
|
// Revoked social media consents
|
|
const revoked = [];
|
|
originalSocialIds.forEach(platformId => {
|
|
if (!currentSocialIds.has(platformId)) {
|
|
revoked.push(platformId);
|
|
changes.push({
|
|
consentType: 'social_media',
|
|
action: 'revoke',
|
|
platformId
|
|
});
|
|
}
|
|
});
|
|
|
|
// Restored social media consents
|
|
currentSocialIds.forEach(platformId => {
|
|
if (!originalSocialIds.has(platformId)) {
|
|
changes.push({
|
|
consentType: 'social_media',
|
|
action: 'restore',
|
|
platformId
|
|
});
|
|
}
|
|
});
|
|
|
|
// Save each change
|
|
for (const change of changes) {
|
|
const res = await apiFetch(`/api/manage/${token}/consents`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(change)
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}));
|
|
throw new Error(body.error || 'Fehler beim Speichern der Einwilligung');
|
|
}
|
|
}
|
|
|
|
// Show success message
|
|
setSuccessMessage('Einwilligungen wurden erfolgreich gespeichert.');
|
|
|
|
// Show email hint after saving if social media was revoked
|
|
setShowEmailHint(revoked.length > 0);
|
|
|
|
// Update original consents with deep copy
|
|
setOriginalConsents({
|
|
workshopConsent: consents.workshopConsent,
|
|
socialMediaConsents: [...(consents.socialMediaConsents || [])]
|
|
});
|
|
|
|
// Don't refresh - just show success message
|
|
|
|
} catch (error) {
|
|
console.error('Error saving consents:', error);
|
|
setErrorMessage(error.message || 'Einwilligungen konnten nicht gespeichert werden');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDiscard = () => {
|
|
setConsents({
|
|
...originalConsents,
|
|
socialMediaConsents: [...(originalConsents.socialMediaConsents || [])]
|
|
});
|
|
setSuccessMessage('');
|
|
setErrorMessage('');
|
|
setShowEmailHint(false);
|
|
};
|
|
|
|
const handleConsentChange = (newConsents) => {
|
|
// Force new object reference so React detects the change
|
|
setConsents({
|
|
workshopConsent: newConsents.workshopConsent,
|
|
socialMediaConsents: [...(newConsents.socialMediaConsents || [])]
|
|
});
|
|
};
|
|
|
|
return (
|
|
<ConsentCheckboxes
|
|
consents={currentConsents}
|
|
onConsentChange={isUploadMode ? setCurrentConsents : handleConsentChange}
|
|
disabled={saving}
|
|
mode={isUploadMode ? "upload" : "manage"}
|
|
groupId={groupId}
|
|
token={token}
|
|
onSave={null}
|
|
>
|
|
{/* Alerts and Buttons only in edit mode */}
|
|
{!isUploadMode && (
|
|
<>
|
|
{/* Success Message */}
|
|
{successMessage && (
|
|
<Alert severity="success" sx={{ mt: 3 }}>
|
|
{successMessage}
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Email Hint - show IMMEDIATELY when social media revoked (before save) */}
|
|
{hasChanges() && hasSocialMediaRevocations() && !successMessage && (
|
|
<Alert severity="warning" sx={{ mt: 3 }}>
|
|
<strong>Hinweis:</strong> Bei Widerruf einer Social Media Einwilligung müssen Sie nach dem Speichern
|
|
eine E-Mail an{' '}
|
|
<a href="mailto:info@hobbyhimmel.de" style={{ color: '#2196F3', fontWeight: 'bold' }}>
|
|
info@hobbyhimmel.de
|
|
</a>{' '}
|
|
senden, um die Löschung Ihrer Bilder anzufordern.
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Email Hint after successful save */}
|
|
{showEmailHint && successMessage && (
|
|
<Alert severity="info" sx={{ mt: 2 }}>
|
|
<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 Deiner Gruppen-ID, um die Löschung Deiner Bilder auf den Social Media Plattformen anzufordern.
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Error Message */}
|
|
{errorMessage && (
|
|
<Alert severity="error" sx={{ mt: 2 }}>
|
|
{errorMessage}
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
{hasChanges() && (
|
|
<Box sx={{ mt: 3, display: 'flex', gap: 1, justifyContent: 'center' }}>
|
|
<button
|
|
className="btn btn-success"
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
>
|
|
{saving ? '⏳ Speichern...' : '💾 Einwilligungen speichern'}
|
|
</button>
|
|
|
|
<button
|
|
className="btn btn-secondary"
|
|
onClick={handleDiscard}
|
|
disabled={saving}
|
|
>
|
|
↩ Verwerfen
|
|
</button>
|
|
</Box>
|
|
)}
|
|
</>
|
|
)}
|
|
</ConsentCheckboxes>
|
|
);
|
|
}
|
|
|
|
export default ConsentManager;
|