diff --git a/FeatureRequests/FEATURE_REQUEST-upload-form-validation.md b/FeatureRequests/FEATURE_REQUEST-upload-form-validation.md
new file mode 100644
index 0000000..ee9a2e6
--- /dev/null
+++ b/FeatureRequests/FEATURE_REQUEST-upload-form-validation.md
@@ -0,0 +1,148 @@
+
+
+# FEATURE_REQUEST: Upload-Formular – Validierungsfeedback bei fehlenden Pflichtfeldern
+
+**Status**: ⏳ Geplant
+**Erstellt**: 1. April 2026
+
+---
+
+## Problem
+
+Das Upload-Formular (`MultiUploadPage`) blockiert den Upload-Vorgang still, wenn Pflichtfelder
+fehlen. Die Funktion `handleUpload` bricht ohne Benutzerrückmeldung ab:
+
+```js
+// Aktuelles Verhalten in handleUpload():
+if (selectedImages.length === 0) return;
+if (!metadata.year || !metadata.title.trim()) return;
+if (!consents.workshopConsent) return;
+```
+
+Der Nutzer klickt auf den Upload-Button – nichts passiert. Kein Hinweis, welches Feld fehlt,
+keine visuelle Hervorhebung, kein Scroll zum Problem.
+
+### Betroffene Pflichtfelder
+
+| Feld | Bedingung |
+|---|---|
+| Bilder auswählen | `selectedImages.length === 0` |
+| Jahr | `!metadata.year` |
+| Titel der Slideshow | `!metadata.title.trim()` |
+| Workshop-Einwilligung (Checkbox) | `!consents.workshopConsent` |
+
+---
+
+## Gewünschtes Verhalten
+
+1. **Klick auf Upload-Button** → Validierung aller Pflichtfelder
+2. **Bei einem oder mehreren fehlenden Feldern:**
+ - Das erste fehlende Feld wird visuell hervorgehoben (roter Rahmen / Fehlermeldung unterhalb)
+ - Die Seite scrollt automatisch zum ersten fehlenden Feld
+ - Der Upload wird nicht ausgeführt
+3. **Fehlermeldungen verschwinden**, sobald der Nutzer das jeweilige Feld korrekt ausfüllt
+
+---
+
+## Use Cases
+
+### UC-1: Kein Titel eingegeben
+- Nutzer wählt Bilder aus, füllt Jahr aus, setzt Einwilligung, aber lässt Titel leer
+- Klick auf Upload-Button
+- → Seite scrollt zum Titel-Feld, Feld wird rot umrandet, Meldung: *„Bitte gib einen Titel für die Slideshow ein."*
+
+### UC-2: Kein Jahr gewählt
+- → Seite scrollt zum Jahr-Feld, Fehlermeldung: *„Bitte wähle ein Jahr aus."*
+
+### UC-3: Einwilligung fehlt
+- → Seite scrollt zur Checkbox, Fehlermeldung: *„Bitte bestätige die Einwilligung."*
+
+### UC-4: Mehrere Felder fehlen gleichzeitig
+- → Scroll zum **ersten** fehlenden Feld (Reihenfolge: Bilder → Jahr → Titel → Einwilligung)
+- Alle fehlenden Felder werden gleichzeitig markiert
+
+---
+
+## Technische Umsetzung
+
+### Implementierungsschritte
+
+1. **Branch anlegen**: `feature/upload-form-validation`
+2. **Plan-Datei anlegen**: `FeatureRequests/FEATURE_PLAN-upload-form-validation.md`
+3. Fragen zur Umsetzung stellen (siehe unten)
+4. Code-Implementierung
+
+### Technischer Ansatz
+
+**State für Validierungsfehler** in `MultiUploadPage`:
+
+```js
+const [validationErrors, setValidationErrors] = useState({
+ images: false,
+ year: false,
+ title: false,
+ workshopConsent: false,
+});
+```
+
+**Refs für Auto-Scroll** zu jedem Pflichtfeld:
+
+```js
+const imagesSectionRef = useRef(null);
+const yearFieldRef = useRef(null);
+const titleFieldRef = useRef(null);
+const consentRef = useRef(null);
+```
+
+**Validierungslogik** vor dem Upload:
+
+```js
+const validate = () => {
+ const errors = {
+ images: selectedImages.length === 0,
+ year: !metadata.year,
+ title: !metadata.title.trim(),
+ workshopConsent: !consents.workshopConsent,
+ };
+ setValidationErrors(errors);
+
+ // Zum ersten Fehler scrollen
+ if (errors.images) imagesSectionRef.current?.scrollIntoView({ behavior: 'smooth' });
+ else if (errors.year) yearFieldRef.current?.scrollIntoView({ behavior: 'smooth' });
+ else if (errors.title) titleFieldRef.current?.scrollIntoView({ behavior: 'smooth' });
+ else if (errors.workshopConsent) consentRef.current?.scrollIntoView({ behavior: 'smooth' });
+
+ return !Object.values(errors).some(Boolean);
+};
+```
+
+**Fehler-Reset** bei Änderungen an den Feldern (jeweils im jeweiligen onChange-Handler):
+
+```js
+setValidationErrors(prev => ({ ...prev, title: false }));
+```
+
+**Visuelle Hervorhebung**: Felder-Komponenten erhalten ein `error`-Prop (MUI-Standard):
+
+```jsx
+
+```
+
+---
+
+## Klärungsfragen vor der Umsetzung
+
+1. Werden die Eingabe-Komponenten für Jahr und Titel als MUI-`TextField`/`Select` gerendert
+ oder als eigene Custom-Komponenten? (Bestimmt, ob `error`-Prop direkt nutzbar ist)
+2. Soll die Einwilligungs-Checkbox ebenfalls eine inline Fehlermeldung erhalten oder reicht
+ ein Scroll + farbliche Markierung des Checkbox-Labels?
+3. Soll der Upload-Button während der Validierung deaktiviert (`disabled`) bleiben, oder erst
+ nach dem ersten Klickversuch aktiviert werden? (aktuelle Implementierung: Button ist immer aktiv)
diff --git a/frontend/src/Components/ComponentUtils/ConsentManager.js b/frontend/src/Components/ComponentUtils/ConsentManager.js
index 58c4b48..8cb4245 100644
--- a/frontend/src/Components/ComponentUtils/ConsentManager.js
+++ b/frontend/src/Components/ComponentUtils/ConsentManager.js
@@ -16,7 +16,9 @@ function ConsentManager({
token,
groupId,
onRefresh,
- mode = 'edit'
+ mode = 'edit',
+ validationErrors,
+ onValidationClear
}) {
// Initialize with proper defaults
const defaultConsents = {
@@ -210,6 +212,8 @@ function ConsentManager({
groupId={groupId}
token={token}
onSave={null}
+ workshopConsentError={validationErrors?.workshopConsent}
+ onWorkshopConsentErrorClear={() => onValidationClear?.('workshopConsent')}
>
{/* Alerts and Buttons only in edit mode */}
{!isUploadMode && (
diff --git a/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js b/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js
index f74c22c..6a7c653 100644
--- a/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js
+++ b/frontend/src/Components/ComponentUtils/GroupMetadataEditor.js
@@ -19,7 +19,9 @@ function GroupMetadataEditor({
token,
groupId,
onRefresh,
- mode = 'edit'
+ mode = 'edit',
+ validationErrors,
+ onValidationClear
}) {
const [metadata, setMetadata] = useState(initialMetadata || {
year: new Date().getFullYear(),
@@ -150,6 +152,8 @@ function GroupMetadataEditor({
{!isUploadMode && hasChanges() && (
diff --git a/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js b/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js
index 7972860..7095d38 100644
--- a/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js
+++ b/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js
@@ -42,7 +42,9 @@ function ConsentCheckboxes({
disabled = false,
mode = 'upload',
groupId = null,
- children
+ children,
+ workshopConsentError = false,
+ onWorkshopConsentErrorClear
}) {
const [platforms, setPlatforms] = useState([]);
const [loading, setLoading] = useState(true);
@@ -71,6 +73,7 @@ function ConsentCheckboxes({
...consents,
workshopConsent: event.target.checked
});
+ if (onWorkshopConsentErrorClear) onWorkshopConsentErrorClear();
};
const handleSocialMediaChange = (platformId) => (event) => {
@@ -168,6 +171,11 @@ function ConsentCheckboxes({
}
/>
+ {workshopConsentError && (
+
+ Bitte bestätige die Einwilligung zur Anzeige in der Werkstatt.
+
+ )}
diff --git a/frontend/src/Components/ComponentUtils/MultiUpload/DescriptionInput.js b/frontend/src/Components/ComponentUtils/MultiUpload/DescriptionInput.js
index 87d8a79..029779e 100644
--- a/frontend/src/Components/ComponentUtils/MultiUpload/DescriptionInput.js
+++ b/frontend/src/Components/ComponentUtils/MultiUpload/DescriptionInput.js
@@ -3,7 +3,9 @@ import { TextField, Typography, Grid, Box } from '@mui/material';
function DescriptionInput({
metadata = {},
- onMetadataChange
+ onMetadataChange,
+ validationErrors = {},
+ onValidationClear
}) {
const handleFieldChange = (field, value) => {
@@ -12,6 +14,7 @@ function DescriptionInput({
[field]: value
};
onMetadataChange(updatedMetadata);
+ if (onValidationClear) onValidationClear(field);
};
const currentYear = new Date().getFullYear();
@@ -86,6 +89,8 @@ function DescriptionInput({
min: 1900,
max: currentYear + 10
}}
+ error={!!validationErrors.year}
+ helperText={validationErrors.year ? 'Bitte wähle ein Jahr aus.' : ''}
/>
@@ -100,6 +105,8 @@ function DescriptionInput({
onChange={(e) => handleFieldChange('title', e.target.value)}
placeholder="z.B. Wohnzimmer Renovierung"
inputProps={{ maxLength: 100 }}
+ error={!!validationErrors.title}
+ helperText={validationErrors.title ? 'Bitte gib einen Titel für die Slideshow ein.' : ''}
/>
diff --git a/frontend/src/Components/ComponentUtils/MultiUpload/MultiImageDropzone.js b/frontend/src/Components/ComponentUtils/MultiUpload/MultiImageDropzone.js
index 5cc1a6b..c5825f4 100644
--- a/frontend/src/Components/ComponentUtils/MultiUpload/MultiImageDropzone.js
+++ b/frontend/src/Components/ComponentUtils/MultiUpload/MultiImageDropzone.js
@@ -1,7 +1,7 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
-function MultiImageDropzone({ onImagesSelected, selectedImages = [] }) {
+function MultiImageDropzone({ onImagesSelected, selectedImages = [], hasError = false }) {
const handleFiles = (files) => {
// Filter nur Bilddateien
@@ -57,21 +57,21 @@ function MultiImageDropzone({ onImagesSelected, selectedImages = [] }) {
};
const dropzoneSx = {
- border: '2px dashed #cccccc',
+ border: `2px dashed ${hasError ? '#d32f2f' : '#cccccc'}`,
borderRadius: '8px',
padding: '40px 20px',
textAlign: 'center',
cursor: 'pointer',
transition: 'all 0.3s ease',
- backgroundColor: '#fafafa',
+ backgroundColor: hasError ? '#fff5f5' : '#fafafa',
minHeight: '200px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
'&:hover': {
- borderColor: '#999999',
- backgroundColor: '#f0f0f0'
+ borderColor: hasError ? '#b71c1c' : '#999999',
+ backgroundColor: hasError ? '#ffebee' : '#f0f0f0'
}
};
diff --git a/frontend/src/Components/Pages/MultiUploadPage.js b/frontend/src/Components/Pages/MultiUploadPage.js
index 3207149..8163803 100644
--- a/frontend/src/Components/Pages/MultiUploadPage.js
+++ b/frontend/src/Components/Pages/MultiUploadPage.js
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
// Components
import NavbarUpload from '../ComponentUtils/Headers/NavbarUpload';
@@ -34,6 +34,17 @@ function MultiUploadPage() {
const [uploadResult, setUploadResult] = useState(null);
const [isEditMode, setIsEditMode] = useState(false);
const [imageDescriptions, setImageDescriptions] = useState({});
+ const [validationErrors, setValidationErrors] = useState({
+ images: false,
+ year: false,
+ title: false,
+ workshopConsent: false,
+ });
+
+ // Refs für Auto-Scroll zu Pflichtfeldern
+ const dropzoneRef = useRef(null);
+ const metadataRef = useRef(null);
+ const consentRef = useRef(null);
// Cleanup object URLs when component unmounts
useEffect(() => {
@@ -108,12 +119,34 @@ function MultiUploadPage() {
}));
};
+ const clearValidationError = (field) => {
+ setValidationErrors(prev => ({ ...prev, [field]: false }));
+ };
+
+ const validate = () => {
+ const errors = {
+ images: selectedImages.length === 0,
+ year: !metadata.year,
+ title: !metadata.title.trim(),
+ workshopConsent: !consents.workshopConsent,
+ };
+ setValidationErrors(errors);
+
+ if (errors.images) {
+ dropzoneRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ } else if (errors.year) {
+ metadataRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ } else if (errors.title) {
+ metadataRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ } else if (errors.workshopConsent) {
+ consentRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+
+ return !Object.values(errors).some(Boolean);
+ };
+
const handleUpload = async () => {
- if (selectedImages.length === 0) return;
-
- if (!metadata.year || !metadata.title.trim()) return;
-
- if (!consents.workshopConsent) return;
+ if (!validate()) return;
setUploading(true);
setUploadProgress(0);
@@ -151,13 +184,6 @@ function MultiUploadPage() {
}
};
- const canUpload = () => {
- return selectedImages.length > 0 &&
- metadata.year &&
- metadata.title.trim() &&
- consents.workshopConsent;
- };
-
return (
{
}
@@ -177,10 +203,21 @@ function MultiUploadPage() {
{!uploading ? (
<>
{/* Image Dropzone - stays inline as it's upload-specific */}
-
+
+
{
+ handleImagesSelected(imgs);
+ clearValidationError('images');
+ }}
+ selectedImages={selectedImages}
+ hasError={validationErrors.images}
+ />
+ {validationErrors.images && (
+
+ Bitte wähle mindestens ein Bild aus.
+
+ )}
+
{/* Image Gallery with descriptions */}
{selectedImages.length > 0 && (
@@ -201,24 +238,31 @@ function MultiUploadPage() {
{selectedImages.length > 0 && (
<>
{/* Modular Components like ManagementPortalPage */}
-
+
+
+
-
+
+
+
{/* Action Buttons */}