import { useState, useEffect, useMemo } from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { queryClient } from "@/client/rpc-client"; export function AdminGallery() { // Component state const [photoTitle, setPhotoTitle] = useState(""); const [photoPreview, setPhotoPreview] = useState(""); const [photoBase64, setPhotoBase64] = useState(""); const [errorMsg, setErrorMsg] = useState(""); const [successMsg, setSuccessMsg] = useState(""); const [draggedPhotoId, setDraggedPhotoId] = useState(null); // Data fetching with live query const { data: photos, refetch: refetchPhotos } = useQuery( queryClient.gallery.live.adminListPhotos.experimental_liveOptions({ input: { sessionId: localStorage.getItem("sessionId") || "" } }) ); // Memoized sorted photos to avoid re-sorting on every render and prevent cache mutation const sortedPhotos = useMemo(() => [...(photos || [])].sort((a, b) => a.order - b.order), [photos]); // Mutations const { mutate: uploadPhoto, isPending: isUploading } = useMutation( queryClient.gallery.uploadPhoto.mutationOptions() ); const { mutate: deletePhoto } = useMutation( queryClient.gallery.deletePhoto.mutationOptions() ); const { mutate: updatePhotoOrder } = useMutation( queryClient.gallery.updatePhotoOrder.mutationOptions() ); const { mutate: setCoverPhoto } = useMutation( queryClient.gallery.setCoverPhoto.mutationOptions() ); // Auto-clear messages after 5 seconds useEffect(() => { if (errorMsg) { const timer = setTimeout(() => setErrorMsg(""), 5000); return () => clearTimeout(timer); } }, [errorMsg]); useEffect(() => { if (successMsg) { const timer = setTimeout(() => setSuccessMsg(""), 5000); return () => clearTimeout(timer); } }, [successMsg]); // Image compression function (adapted from booking-form.tsx) const compressImage = (file: File, maxWidth: number = 800, quality: number = 0.8): Promise => { return new Promise((resolve, reject) => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { // Calculate new dimensions let { width, height } = img; if (width > maxWidth) { height = (height * maxWidth) / width; width = maxWidth; } canvas.width = width; canvas.height = height; // Draw and compress ctx?.drawImage(img, 0, 0, width, height); const compressedDataUrl = canvas.toDataURL('image/jpeg', quality); resolve(compressedDataUrl); }; img.onerror = reject; img.src = URL.createObjectURL(file); }); }; // File upload handler const handlePhotoUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; // Validate file type if (!file.type.startsWith('image/')) { setErrorMsg("Bitte wähle nur Bilddateien aus."); return; } // Check file size (max 1MB) if (file.size > 1 * 1024 * 1024) { setErrorMsg("Das Foto ist zu groß. Bitte wähle ein Bild unter 1MB."); return; } try { // Helper: measure DataURL size via Blob const getDataUrlSizeBytes = async (dataUrl: string): Promise => { const res = await fetch(dataUrl); const blob = await res.blob(); return blob.size; }; // Try compressing with decreasing quality until <= 1MB or give up const qualitySteps = [0.8, 0.6, 0.4]; let finalDataUrl = ""; for (const q of qualitySteps) { const candidate = await compressImage(file, 800, q); const sizeBytes = await getDataUrlSizeBytes(candidate); if (sizeBytes <= 1 * 1024 * 1024) { finalDataUrl = candidate; break; } } if (!finalDataUrl) { setErrorMsg("Das komprimierte Bild ist weiterhin größer als 1MB. Bitte wähle ein kleineres Bild."); return; } setPhotoBase64(finalDataUrl); setPhotoPreview(finalDataUrl); setErrorMsg(""); } catch (error) { console.error('Photo compression failed:', error); setErrorMsg('Fehler beim Verarbeiten des Bildes. Bitte versuche es mit einem anderen Bild.'); return; } }; const removePhoto = () => { setPhotoBase64(""); setPhotoPreview(""); // Reset file input const fileInput = document.getElementById('gallery-photo-upload') as HTMLInputElement; if (fileInput) fileInput.value = ''; }; // Drag and drop handlers const handleDragStart = (e: React.DragEvent, photoId: string) => { setDraggedPhotoId(photoId); e.dataTransfer.effectAllowed = 'move'; }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }; const handleDrop = (e: React.DragEvent, targetPhotoId: string) => { e.preventDefault(); if (!draggedPhotoId || draggedPhotoId === targetPhotoId) { setDraggedPhotoId(null); return; } const draggedPhoto = photos?.find(p => p.id === draggedPhotoId); const targetPhoto = photos?.find(p => p.id === targetPhotoId); if (!draggedPhoto || !targetPhoto) { setDraggedPhotoId(null); return; } const sessionId = localStorage.getItem("sessionId"); if (!sessionId) { setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden."); setDraggedPhotoId(null); return; } // Build a fully reordered list based on the current sorted order const sorted = [...(photos || [])].sort((a, b) => a.order - b.order); const fromIndex = sorted.findIndex(p => p.id === draggedPhotoId); const toIndex = sorted.findIndex(p => p.id === targetPhotoId); if (fromIndex === -1 || toIndex === -1) { setDraggedPhotoId(null); return; } const reordered = [...sorted]; const [moved] = reordered.splice(fromIndex, 1); reordered.splice(toIndex, 0, moved); // Reindex orders 0..n-1 const photoOrders = reordered.map((p, idx) => ({ id: p.id, order: idx })); updatePhotoOrder( { sessionId, photoOrders }, { onSuccess: () => { setSuccessMsg("Foto-Reihenfolge aktualisiert."); // Sofortige Aktualisierung der Thumbnails in korrekter Reihenfolge refetchPhotos(); }, onError: (err: any) => { setErrorMsg(err?.message || "Fehler beim Aktualisieren der Reihenfolge."); } } ); setDraggedPhotoId(null); }; const handleDragEnd = () => { setDraggedPhotoId(null); }; return (
{/* Error and Success Messages */} {errorMsg && (
{errorMsg}
)} {successMsg && (
{successMsg}
)} {/* Upload Form Section */}

Neues Foto hochladen

Max. 1MB, alle Bildformate erlaubt

{photoPreview && (
Foto Vorschau
)}
setPhotoTitle(e.target.value)} placeholder="z.B. French Manicure, Gel Nails..." className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500" />
{/* Photo Grid Section */}

Foto-Galerie verwalten

Ziehe Fotos per Drag & Drop, um die Reihenfolge zu ändern. Klicke auf das X, um ein Foto zu löschen.

{photos?.length === 0 && (
Noch keine Fotos hochgeladen
Lade dein erstes Foto hoch, um deine Galerie zu starten.
)} {sortedPhotos && sortedPhotos.length > 0 && (
{sortedPhotos.map((photo) => (
handleDragStart(e, photo.id)} onDragOver={handleDragOver} onDrop={(e) => handleDrop(e, photo.id)} onDragEnd={handleDragEnd} className={`relative bg-gray-50 rounded-lg overflow-hidden border-2 transition-all duration-200 hover:shadow-md cursor-move ${ draggedPhotoId === photo.id ? 'opacity-50 border-pink-300' : 'border-transparent' }`} > {photo.title
{photo.title && (
{photo.title}
)}
Reihenfolge: {photo.order}
{new Date(photo.createdAt).toLocaleDateString('de-DE')}
))}
)}
); }