Zum Hauptinhalt springen

FileUpload

Die FileUpload-Komponente ermöglicht es Benutzern, Dateien hochzuladen, entweder durch Auswahl über einen Datei-Dialog oder per Drag & Drop.

Import

import { FileUpload } from '@smolitux/core';

Verwendung

Einfacher FileUpload

<FileUpload />

FileUpload mit Label und Hilfetext

<FileUpload 
label="Dokumente hochladen"
helperText="Unterstützte Formate: PDF, DOC, DOCX (max. 5MB)"
/>

FileUpload mit benutzerdefiniertem Dropzone-Text

<FileUpload 
label="Bilder hochladen"
dropzoneText="Ziehen Sie Ihre Bilder hierher oder klicken Sie, um Dateien auszuwählen"
/>

FileUpload mit Dateityp-Beschränkung

<FileUpload 
label="Bilder hochladen"
accept="image/*"
helperText="Nur Bilddateien sind erlaubt"
/>

FileUpload mit Größenbeschränkung

<FileUpload 
label="Dokumente hochladen"
maxSize={5 * 1024 * 1024} // 5MB
helperText="Maximale Dateigröße: 5MB"
/>

FileUpload mit Mehrfachauswahl

<FileUpload 
label="Mehrere Dateien hochladen"
multiple
/>

FileUpload mit Vorschau

<FileUpload 
label="Bilder mit Vorschau"
accept="image/*"
showPreview
/>

FileUpload mit Fortschrittsanzeige

<FileUpload 
label="Dateien mit Fortschrittsanzeige"
showProgress
/>

FileUpload mit automatischem Upload

<FileUpload 
label="Automatischer Upload"
autoUpload
uploadUrl="/api/upload"
/>

FileUpload mit benutzerdefiniertem Upload-Handler

function CustomUploadExample() {
const handleUpload = async (files) => {
// Simuliere einen Upload-Prozess
for (const file of files) {
// Aktualisiere den Fortschritt
for (let progress = 0; progress <= 100; progress += 10) {
await new Promise(resolve => setTimeout(resolve, 200));
file.progress = progress;
// Hier würde man normalerweise den State aktualisieren
}

file.status = 'success';
}

console.log('Upload abgeschlossen:', files);
};

return (
<FileUpload
label="Benutzerdefinierter Upload"
onUpload={handleUpload}
showProgress
/>
);
}

Kontrollierter FileUpload

function ControlledFileUploadExample() {
const [files, setFiles] = useState([]);

const handleChange = (newFiles) => {
setFiles(newFiles);
};

return (
<div>
<FileUpload
label="Kontrollierter FileUpload"
value={files}
onChange={handleChange}
showPreview
/>

<div className="mt-4">
<h3 className="text-lg font-medium">Ausgewählte Dateien:</h3>
<ul className="list-disc pl-5 mt-2">
{files.map(file => (
<li key={file.id}>
{file.name} ({(file.size / 1024).toFixed(2)} KB)
</li>
))}
</ul>
</div>
</div>
);
}

FileUpload mit Validierung

function ValidatedFileUploadExample() {
const [files, setFiles] = useState([]);
const [error, setError] = useState('');

const validateFiles = (newFiles) => {
// Prüfe, ob alle Dateien Bilder sind
const invalidFiles = newFiles.filter(file => !file.type.startsWith('image/'));

if (invalidFiles.length > 0) {
setError(`Folgende Dateien sind keine Bilder: ${invalidFiles.map(f => f.name).join(', ')}`);
return false;
}

// Prüfe, ob alle Dateien kleiner als 2MB sind
const largeFiles = newFiles.filter(file => file.size > 2 * 1024 * 1024);

if (largeFiles.length > 0) {
setError(`Folgende Dateien sind größer als 2MB: ${largeFiles.map(f => f.name).join(', ')}`);
return false;
}

setError('');
return true;
};

const handleChange = (newFiles) => {
if (validateFiles(newFiles)) {
setFiles(newFiles);
}
};

return (
<FileUpload
label="Bilder hochladen (max. 2MB)"
accept="image/*"
value={files}
onChange={handleChange}
error={error}
showPreview
/>
);
}

Props

PropTypStandardBeschreibung
labelstring-Label für die Dateiauswahl
dropzoneTextstring'Dateien hierher ziehen oder klicken, um auszuwählen'Text im Dropbereich
helperTextReactNode-Hilfetext
errorReactNode-Fehlermeldung
valueFileInfo[]-Aktuelle Dateien
onChange(files: FileInfo[]) => void-Callback bei Datei-Änderungen
onUploadStart(files: FileInfo[]) => void-Callback bei Upload-Start
onUploadProgress(file: FileInfo, progress: number) => void-Callback bei Upload-Fortschritt
onUploadComplete(file: FileInfo) => void-Callback bei Upload-Abschluss
onUploadError(file: FileInfo, error: any) => void-Callback bei Upload-Fehler
autoUploadbooleanfalseAutomatischer Upload
maxSizenumber-Maximale Dateigröße in Bytes
acceptstring-Akzeptierte Dateitypen (z.B. "image/*,.pdf")
multiplebooleanfalseMehrfachauswahl erlauben
disabledbooleanfalseKomponente deaktivieren
showPreviewbooleanfalseVorschau anzeigen
showProgressbooleanfalseFortschrittsanzeige anzeigen
uploadUrlstring-URL für automatischen Upload
uploadMethod'POST' | 'PUT''POST'HTTP-Methode für Upload
uploadHeadersRecord<string, string>-HTTP-Header für Upload
uploadFieldNamestring'file'Feldname für Upload
classNamestring-Zusätzliche CSS-Klassen

FileInfo Interface

EigenschaftTypBeschreibung
idstringEindeutige ID des Files
namestringOriginal-Dateiname
typestringMIME-Typ
sizenumberDateigröße in Bytes
fileFileDatei-Objekt
progressnumberFortschritt (0-100)
errorstringFehler
status'idle' | 'uploading' | 'success' | 'error'Status
previewUrlstringVorschau-URL

Barrierefreiheit

Die FileUpload-Komponente ist für Barrierefreiheit optimiert:

  • Verwendet native <input type="file"> Elemente für korrekte Semantik
  • Labels sind korrekt mit den Eingabefeldern verknüpft
  • Unterstützt Tastaturnavigation
  • Bietet visuelle und textuelle Rückmeldung zum Upload-Status
  • Fehlermeldungen werden klar kommuniziert

Beispiele

Profilbild-Upload

function ProfileImageUpload() {
const [image, setImage] = useState(null);
const [isUploading, setIsUploading] = useState(false);

const handleImageChange = (files) => {
if (files.length > 0) {
setImage(files[0]);
setIsUploading(true);

// Simuliere einen Upload
setTimeout(() => {
setIsUploading(false);
files[0].status = 'success';
files[0].progress = 100;
}, 2000);
} else {
setImage(null);
}
};

const handleRemoveImage = () => {
setImage(null);
};

return (
<div className="max-w-md mx-auto">
<h2 className="text-xl font-bold mb-4">Profilbild ändern</h2>

<div className="flex items-center mb-6">
<div className="relative mr-4">
{image && image.previewUrl ? (
<img
src={image.previewUrl}
alt="Profilbild"
className="w-24 h-24 rounded-full object-cover"
/>
) : (
<div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center text-gray-500">
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
)}

{isUploading && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 rounded-full">
<svg className="animate-spin h-8 w-8 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
)}
</div>

<div>
<FileUpload
accept="image/*"
onChange={handleImageChange}
showPreview
dropzoneText="Bild auswählen"
helperText="JPG, PNG oder GIF (max. 2MB)"
maxSize={2 * 1024 * 1024}
className="mb-2"
/>

{image && (
<button
className="text-sm text-red-600 hover:text-red-800"
onClick={handleRemoveImage}
>
Bild entfernen
</button>
)}
</div>
</div>

<div className="flex justify-end">
<button className="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700">
Speichern
</button>
</div>
</div>
);
}

Dokumenten-Upload mit Fortschrittsanzeige

function DocumentUpload() {
const [documents, setDocuments] = useState([]);

const handleDocumentsChange = (files) => {
setDocuments(files);
};

const handleUpload = async () => {
// Simuliere einen Upload-Prozess für jede Datei
const updatedDocuments = [...documents];

for (let i = 0; i < updatedDocuments.length; i++) {
const doc = updatedDocuments[i];

if (doc.status !== 'idle') continue;

doc.status = 'uploading';
setDocuments([...updatedDocuments]);

// Simuliere Fortschritt
for (let progress = 0; progress <= 100; progress += 10) {
await new Promise(resolve => setTimeout(resolve, 200));
doc.progress = progress;
setDocuments([...updatedDocuments]);
}

// Simuliere Erfolg oder Fehler (zufällig)
if (Math.random() > 0.2) {
doc.status = 'success';
} else {
doc.status = 'error';
doc.error = 'Upload fehlgeschlagen. Bitte versuchen Sie es erneut.';
}

setDocuments([...updatedDocuments]);
}
};

const handleRemove = (id) => {
setDocuments(documents.filter(doc => doc.id !== id));
};

const handleRetry = async (id) => {
const doc = documents.find(d => d.id === id);
if (!doc) return;

doc.status = 'uploading';
doc.error = undefined;
doc.progress = 0;
setDocuments([...documents]);

// Simuliere Fortschritt
for (let progress = 0; progress <= 100; progress += 10) {
await new Promise(resolve => setTimeout(resolve, 200));
doc.progress = progress;
setDocuments([...documents]);
}

doc.status = 'success';
setDocuments([...documents]);
};

return (
<div className="max-w-2xl mx-auto">
<h2 className="text-xl font-bold mb-4">Dokumente hochladen</h2>

<FileUpload
label="Dokumente auswählen"
accept=".pdf,.doc,.docx,.txt"
multiple
value={documents}
onChange={handleDocumentsChange}
helperText="Unterstützte Formate: PDF, DOC, DOCX, TXT (max. 10MB pro Datei)"
maxSize={10 * 1024 * 1024}
className="mb-6"
/>

{documents.length > 0 && (
<div className="mb-6">
<div className="flex justify-between items-center mb-2">
<h3 className="font-medium">Ausgewählte Dokumente</h3>
<button
className="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700"
onClick={handleUpload}
disabled={documents.every(doc => doc.status !== 'idle')}
>
Alle hochladen
</button>
</div>

<div className="border rounded-md divide-y">
{documents.map(doc => (
<div key={doc.id} className="p-3">
<div className="flex justify-between items-center mb-2">
<div className="flex items-center">
<svg className="w-5 h-5 text-gray-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span className="font-medium">{doc.name}</span>
</div>

<div className="flex items-center">
<span className="text-sm text-gray-500 mr-2">
{(doc.size / 1024).toFixed(2)} KB
</span>

{doc.status === 'idle' && (
<button
className="text-gray-500 hover:text-gray-700"
onClick={() => handleRemove(doc.id)}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}

{doc.status === 'error' && (
<button
className="text-yellow-500 hover:text-yellow-700"
onClick={() => handleRetry(doc.id)}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
)}

{doc.status === 'success' && (
<svg className="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
</div>

{(doc.status === 'uploading' || doc.status === 'success') && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className={`h-2.5 rounded-full ${doc.status === 'success' ? 'bg-green-500' : 'bg-blue-500'}`}
style={{ width: `${doc.progress}%` }}
></div>
</div>
)}

{doc.status === 'error' && (
<p className="text-sm text-red-500 mt-1">{doc.error}</p>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}

Bild-Galerie-Upload

function ImageGalleryUpload() {
const [images, setImages] = useState([]);

const handleImagesChange = (files) => {
// Filtere nur Bilder
const imageFiles = files.filter(file => file.type.startsWith('image/'));
setImages(imageFiles);
};

const handleRemoveImage = (id) => {
setImages(images.filter(img => img.id !== id));
};

return (
<div className="max-w-3xl mx-auto">
<h2 className="text-xl font-bold mb-4">Bilder-Galerie</h2>

<FileUpload
label="Bilder hochladen"
accept="image/*"
multiple
value={images}
onChange={handleImagesChange}
showPreview
helperText="Ziehen Sie Ihre Bilder hierher oder klicken Sie, um Dateien auszuwählen"
className="mb-6"
/>

{images.length > 0 && (
<div>
<h3 className="font-medium mb-2">Vorschau ({images.length} Bilder)</h3>

<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{images.map(img => (
<div key={img.id} className="relative group">
<img
src={img.previewUrl}
alt={img.name}
className="w-full h-40 object-cover rounded-md"
/>

<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-40 transition-opacity flex items-center justify-center opacity-0 group-hover:opacity-100">
<button
className="p-1 bg-white rounded-full text-red-500 hover:text-red-700"
onClick={() => handleRemoveImage(img.id)}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>

<div className="mt-1 text-sm truncate">{img.name}</div>
</div>
))}
</div>

<div className="mt-4 flex justify-end">
<button className="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700">
Alle hochladen
</button>
</div>
</div>
)}
</div>
);
}