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 */}