From 9dcc974c1d5ba048f2e77db69609ca11a412c908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20He=C3=9Fe?= Date: Mon, 20 Jul 2026 22:16:27 +0200 Subject: [PATCH] Added image picker and upload function --- src/components/ImagePicker.tsx | 401 +++++++++++++++++++++++++++++++++ 1 file changed, 401 insertions(+) create mode 100644 src/components/ImagePicker.tsx diff --git a/src/components/ImagePicker.tsx b/src/components/ImagePicker.tsx new file mode 100644 index 0000000..ed994fd --- /dev/null +++ b/src/components/ImagePicker.tsx @@ -0,0 +1,401 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Upload, Image as ImageIcon, Link as LinkIcon, X, Check, Loader2, FolderOpen, AlertCircle } from "lucide-react"; + +interface ImagePickerProps { + value: string; + onChange: (url: string) => void; + token: string; + label: string; +} + +interface ServerFile { + name: string; + url: string; +} + +export default function ImagePicker({ value, onChange, token, label }: ImagePickerProps) { + const [isOpen, setIsOpen] = useState(false); + const [activeTab, setActiveTab] = useState<"upload" | "server" | "url">("upload"); + const [manualUrl, setManualUrl] = useState(value || ""); + const [serverFiles, setServerFiles] = useState([]); + const [loadingFiles, setLoadingFiles] = useState(false); + const [uploading, setUploading] = useState(false); + const [error, setError] = useState(""); + const [dragActive, setDragActive] = useState(false); + const fileInputRef = useRef(null); + + // Fetch server uploads when the "server" tab is selected + useEffect(() => { + if (isOpen && activeTab === "server") { + fetchServerFiles(); + } + }, [isOpen, activeTab]); + + // Sync state if external value changes + useEffect(() => { + setManualUrl(value || ""); + }, [value]); + + const fetchServerFiles = async () => { + setLoadingFiles(true); + setError(""); + try { + const response = await fetch("/api/admin/list-uploads", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + if (!response.ok) { + throw new Error("Fehler beim Laden der Serverdateien"); + } + const data = await response.json(); + if (data.success) { + setServerFiles(data.files || []); + } else { + setError(data.error || "Laden der Dateien fehlgeschlagen."); + } + } catch (err: any) { + setError("Verbindung zum Server fehlgeschlagen."); + console.error(err); + } finally { + setLoadingFiles(false); + } + }; + + const handleFileUpload = async (file: File) => { + if (!file) return; + if (!file.type.startsWith("image/")) { + setError("Bitte wählen Sie nur Bilddateien (PNG, JPG, WebP, etc.) aus."); + return; + } + + setUploading(true); + setError(""); + + try { + // Read file as base64 + const reader = new FileReader(); + reader.onloadend = async () => { + const base64Data = reader.result as string; + try { + const response = await fetch("/api/admin/upload-file", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + fileName: file.name, + base64Data: base64Data, + }), + }); + + if (!response.ok) { + throw new Error("Serverfehler beim Upload."); + } + + const data = await response.json(); + if (data.success) { + onChange(data.url); + setIsOpen(false); + } else { + setError(data.error || "Upload fehlgeschlagen."); + } + } catch (err: any) { + setError("Upload-Fehler: " + err.message); + } finally { + setUploading(false); + } + }; + + reader.onerror = () => { + setError("Fehler beim Lesen der lokalen Datei."); + setUploading(false); + }; + + reader.readAsDataURL(file); + } catch (err: any) { + setError("Ein unerwarteter Fehler ist aufgetreten."); + setUploading(false); + } + }; + + const handleDrag = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") { + setDragActive(true); + } else if (e.type === "dragleave") { + setDragActive(false); + } + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + handleFileUpload(e.dataTransfer.files[0]); + } + }; + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + handleFileUpload(e.target.files[0]); + } + }; + + const applyManualUrl = () => { + onChange(manualUrl); + setIsOpen(false); + }; + + const handleSelectServerFile = (url: string) => { + onChange(url); + setIsOpen(false); + }; + + return ( +
+
+ {/* Preview thumbnail */} +
+ {value ? ( + Vorschau { + // If local image or external URL fails, display placeholder icon + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + ) : ( + + )} +
+ + {/* Text Input displaying URL + Button to trigger modal */} +
+ + +
+
+ + {/* Modal overlay */} + {isOpen && ( +
+
+ + {/* Modal Header */} +
+
+

+ {label || "Bild auswählen"} +

+

Lokal hochladen, vom Server wählen oder Web-URL eintragen.

+
+ +
+ + {/* Modal Tabs */} +
+ + + +
+ + {/* Modal Content */} +
+ + {/* ERROR DISPLAY */} + {error && ( +
+ + {error} +
+ )} + + {/* TAB 1: UPLOAD LOCAL FILE */} + {activeTab === "upload" && ( +
+
fileInputRef.current?.click()} + className={`border-2 border-dashed rounded-xl p-8 flex flex-col items-center justify-center text-center cursor-pointer transition-all ${ + dragActive + ? "border-indigo-500 bg-indigo-950/10" + : "border-slate-800 hover:border-slate-700 bg-slate-950/20" + }`} + > + + + {uploading ? ( +
+ +

Übertrage Datei zum Server...

+
+ ) : ( +
+
+ +
+

Bilddatei hierher ziehen oder anklicken

+

Unterstützt JPG, PNG, GIF, WebP (max. 10MB)

+
+ )} +
+
+ )} + + {/* TAB 2: CHOOSE FROM SERVER FILES */} + {activeTab === "server" && ( +
+ {loadingFiles ? ( +
+ +

Durchsuche Server-Speicher...

+
+ ) : serverFiles.length === 0 ? ( +
+ +

Noch keine Dateien auf dem Server hochgeladen.

+

Nutzen Sie den "Hochladen"-Tab, um ein neues Bild hinzuzufügen.

+
+ ) : ( +
+ {serverFiles.map((file) => { + const isSelected = value === file.url; + return ( + + ); + })} +
+ )} +
+ )} + + {/* TAB 3: PASTE INTERNET URL */} + {activeTab === "url" && ( +
+
+ + setManualUrl(e.target.value)} + placeholder="https://example.com/bild.jpg" + className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-xs font-mono" + /> +

Sie können jede direkte Bild-URL eines Drittanbieters eintragen.

+
+ +
+ +
+
+ )} + +
+
+
+ )} +
+ ); +}