- Created new modular components: * ConsentManager: Manages workshop + social media consents with individual save * GroupMetadataEditor: Manages group metadata (title, description, name, year) with save * ImageDescriptionManager: Manages image descriptions with batch save * DeleteGroupButton: Standalone group deletion component - Refactored ManagementPortalPage to use modular components: * Each component in Paper box with heading inside (not outside) * HTML buttons with CSS classes (btn btn-success, btn btn-secondary) * Inline feedback with Material-UI Alert instead of SweetAlert2 popups * Icons: 💾 save, ↩ discard, 🗑️ delete * Individual save/discard functionality per component - Enhanced ConsentCheckboxes component: * Added children prop for flexible composition * Conditional heading for manage mode inside Paper box - Fixed DescriptionInput: * Removed duplicate heading (now only in parent component) - React state management improvements: * Deep copy pattern for nested objects/arrays * Sorted array comparison for order-insensitive change detection * Set-based comparison for detecting removed items * Initialization guard to prevent useEffect overwrites - Bug fixes: * Fixed image reordering using existing /api/groups/:groupId/reorder route * Fixed edit mode toggle with unsaved changes warning * Fixed consent state updates with proper object references * Fixed uploadImageBatch signature to use object destructuring * Removed unnecessary /api/manage/:token/reorder route from backend Next: Apply same modular pattern to MultiUploadPage and ModerationGroupImagesPage
143 lines
3.9 KiB
JavaScript
143 lines
3.9 KiB
JavaScript
import React from 'react';
|
|
import { TextField, Typography, Grid, Box } from '@mui/material';
|
|
|
|
function DescriptionInput({
|
|
metadata = {},
|
|
onMetadataChange
|
|
}) {
|
|
|
|
const handleFieldChange = (field, value) => {
|
|
const updatedMetadata = {
|
|
...metadata,
|
|
[field]: value
|
|
};
|
|
onMetadataChange(updatedMetadata);
|
|
};
|
|
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
const fieldLabelSx = {
|
|
fontFamily: 'roboto',
|
|
fontSize: '14px',
|
|
color: '#555555',
|
|
marginBottom: '8px',
|
|
display: 'block'
|
|
};
|
|
|
|
const sectionTitleSx = {
|
|
fontFamily: 'roboto',
|
|
fontSize: '18px',
|
|
color: '#333333',
|
|
marginBottom: '15px',
|
|
display: 'block',
|
|
fontWeight: 500
|
|
};
|
|
|
|
const textFieldSx = {
|
|
width: '100%',
|
|
marginBottom: '15px',
|
|
'& .MuiOutlinedInput-root': {
|
|
borderRadius: '8px'
|
|
}
|
|
};
|
|
|
|
const requiredFieldSx = {
|
|
'& .MuiOutlinedInput-root': {
|
|
borderRadius: '8px',
|
|
'& fieldset': {
|
|
borderColor: '#E57373'
|
|
}
|
|
}
|
|
};
|
|
|
|
const optionalFieldSx = {
|
|
'& .MuiOutlinedInput-root': {
|
|
borderRadius: '8px',
|
|
'& fieldset': {
|
|
borderColor: '#E0E0E0'
|
|
}
|
|
}
|
|
};
|
|
|
|
const characterCountSx = {
|
|
fontSize: '12px',
|
|
color: '#999999',
|
|
textAlign: 'right',
|
|
marginTop: '-10px',
|
|
marginBottom: '10px'
|
|
};
|
|
|
|
const requiredIndicatorSx = { color: '#E57373', fontSize: '16px' };
|
|
const optionalIndicatorSx = { color: '#9E9E9E', fontSize: '12px', fontStyle: 'italic' };
|
|
|
|
return (
|
|
<Box sx={{ marginTop: '20px', marginBottom: '20px' }}>
|
|
<Grid container spacing={2}>
|
|
<Grid item xs={12} sm={6}>
|
|
<Typography sx={fieldLabelSx}>
|
|
Jahr <Box component="span" sx={requiredIndicatorSx}>*</Box>
|
|
</Typography>
|
|
<TextField
|
|
sx={{ ...textFieldSx, ...requiredFieldSx }}
|
|
variant="outlined"
|
|
type="number"
|
|
value={metadata.year || currentYear}
|
|
onChange={(e) => handleFieldChange('year', parseInt(e.target.value))}
|
|
placeholder={currentYear.toString()}
|
|
inputProps={{
|
|
min: 1900,
|
|
max: currentYear + 10
|
|
}}
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid item xs={12} sm={6}>
|
|
<Typography sx={fieldLabelSx}>
|
|
Titel <Box component="span" sx={requiredIndicatorSx}>*</Box>
|
|
</Typography>
|
|
<TextField
|
|
sx={{ ...textFieldSx, ...requiredFieldSx }}
|
|
variant="outlined"
|
|
value={metadata.title || ''}
|
|
onChange={(e) => handleFieldChange('title', e.target.value)}
|
|
placeholder="z.B. Wohnzimmer Renovierung"
|
|
inputProps={{ maxLength: 100 }}
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid item xs={12}>
|
|
<Typography sx={fieldLabelSx}>
|
|
Beschreibung <Box component="span" sx={optionalIndicatorSx}>(optional)</Box>
|
|
</Typography>
|
|
<TextField
|
|
sx={{ ...textFieldSx, ...optionalFieldSx }}
|
|
multiline
|
|
rows={3}
|
|
variant="outlined"
|
|
value={metadata.description || ''}
|
|
onChange={(e) => handleFieldChange('description', e.target.value)}
|
|
placeholder="Detaillierte Beschreibung des Projekts..."
|
|
inputProps={{ maxLength: 500 }}
|
|
/>
|
|
<Box sx={characterCountSx}>{(metadata.description || '').length} / 500 Zeichen</Box>
|
|
</Grid>
|
|
|
|
<Grid item xs={12}>
|
|
<Typography sx={fieldLabelSx}>
|
|
Name/Ersteller <Box component="span" sx={optionalIndicatorSx}>(optional)</Box>
|
|
</Typography>
|
|
<TextField
|
|
sx={{ ...textFieldSx, ...optionalFieldSx }}
|
|
variant="outlined"
|
|
value={metadata.name || ''}
|
|
onChange={(e) => handleFieldChange('name', e.target.value)}
|
|
placeholder="Dein Name oder Projektersteller"
|
|
inputProps={{ maxLength: 50 }}
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export default DescriptionInput; |