import { useState, useEffect } from 'react' import { useAuth } from '../contexts/AuthContext' import { usePermissions } from '../hooks/usePermissions' const Profile = () => { const { authFetch, user } = useAuth() const { isAdmin } = usePermissions() const [loading, setLoading] = useState(false) const [showSuccessAnimation, setShowSuccessAnimation] = useState(false) const [error, setError] = useState('') const [success, setSuccess] = useState('') const [uploadingAvatar, setUploadingAvatar] = useState(false) const [avatarUrl, setAvatarUrl] = useState(null) const [showCropModal, setShowCropModal] = useState(false) const [selectedFile, setSelectedFile] = useState(null) const [cropImage, setCropImage] = useState(null) const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 }) const [cropPosition, setCropPosition] = useState({ x: 0, y: 0, size: 200 }) const [isDragging, setIsDragging] = useState(false) const [isResizing, setIsResizing] = useState(false) const [dragStart, setDragStart] = useState({ x: 0, y: 0 }) const [formData, setFormData] = useState({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' }) useEffect(() => { if (user) { setFormData({ username: user.username || '', email: user.email || '', oldPassword: '', password: '', confirmPassword: '' }) // Lade Profilbild loadAvatar() } }, [user]) const loadAvatar = async () => { if (user?.id) { // Versuche Profilbild zu laden, mit Timestamp für Cache-Busting const url = `/api/users/${user.id}/avatar?t=${Date.now()}` try { const response = await authFetch(url) if (response.ok) { setAvatarUrl(url) } else { setAvatarUrl(null) } } catch { setAvatarUrl(null) } } else { setAvatarUrl(null) } } const handleFileSelect = (e) => { const file = e.target.files?.[0] if (!file) return // Validiere Dateityp const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'] if (!allowedTypes.includes(file.type)) { setError('Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt') return } // Validiere Dateigröße (max 10MB) if (file.size > 10 * 1024 * 1024) { setError('Datei ist zu groß. Maximale Größe: 10MB') return } setSelectedFile(file) // Lade Bild für Crop-Modal const reader = new FileReader() reader.onload = (e) => { setCropImage(e.target.result) setShowCropModal(true) // Setze initiale Crop-Position (zentriert) const img = new Image() img.onload = () => { const minSize = Math.min(img.width, img.height) const cropSize = Math.min(minSize * 0.8, 400) setImageDimensions({ width: img.width, height: img.height }) setCropPosition({ x: (img.width - cropSize) / 2, y: (img.height - cropSize) / 2, size: cropSize }) } img.src = e.target.result } reader.readAsDataURL(file) } const handleCropDragStart = (e) => { e.preventDefault() setIsDragging(true) const img = document.getElementById('crop-image') if (!img) return const rect = img.getBoundingClientRect() const scaleX = imageDimensions.width / rect.width const scaleY = imageDimensions.height / rect.height setDragStart({ x: e.clientX - (cropPosition.x / scaleX + rect.left), y: e.clientY - (cropPosition.y / scaleY + rect.top) }) } const handleCropDrag = (e) => { if (!isDragging) return const img = document.getElementById('crop-image') if (!img) return const rect = img.getBoundingClientRect() const scaleX = imageDimensions.width / rect.width const scaleY = imageDimensions.height / rect.height const newX = (e.clientX - rect.left - dragStart.x) * scaleX const newY = (e.clientY - rect.top - dragStart.y) * scaleY // Begrenze auf Bildgrenzen const maxX = imageDimensions.width - cropPosition.size const maxY = imageDimensions.height - cropPosition.size setCropPosition(prev => ({ ...prev, x: Math.max(0, Math.min(maxX, newX)), y: Math.max(0, Math.min(maxY, newY)) })) } const handleCropDragEnd = () => { setIsDragging(false) } const handleCropResize = (e) => { e.preventDefault() e.stopPropagation() setIsResizing(true) const img = document.getElementById('crop-image') if (!img) return const rect = img.getBoundingClientRect() const scale = Math.min(imageDimensions.width / rect.width, imageDimensions.height / rect.height) const startY = e.clientY const startSize = cropPosition.size const startX = cropPosition.x const startYPos = cropPosition.y const handleMouseMove = (moveEvent) => { const deltaY = (moveEvent.clientY - startY) * scale const newSize = Math.max(50, Math.min( Math.min(imageDimensions.width, imageDimensions.height), startSize - deltaY )) // Zentriere Crop-Bereich bei Größenänderung const maxX = imageDimensions.width - newSize const maxY = imageDimensions.height - newSize setCropPosition({ x: Math.max(0, Math.min(maxX, startX + (startSize - newSize) / 2)), y: Math.max(0, Math.min(maxY, startYPos + (startSize - newSize) / 2)), size: newSize }) } const handleMouseUp = () => { setIsResizing(false) document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) } document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) } const cropImageToCircle = async () => { if (!cropImage) return null return new Promise((resolve) => { const img = new Image() img.onload = () => { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') const size = cropPosition.size canvas.width = size canvas.height = size // Erstelle kreisförmigen Clip-Pfad ctx.beginPath() ctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI) ctx.clip() // Zeichne zugeschnittenes Bild ctx.drawImage( img, cropPosition.x, cropPosition.y, cropPosition.size, cropPosition.size, 0, 0, size, size ) // Konvertiere zu Blob canvas.toBlob((blob) => { resolve(blob) }, 'image/png', 0.95) } img.src = cropImage }) } const handleCropConfirm = async () => { setUploadingAvatar(true) setError('') setSuccess('') try { const croppedBlob = await cropImageToCircle() if (!croppedBlob) { setError('Fehler beim Zuschneiden des Bildes') setUploadingAvatar(false) return } // Erstelle File aus Blob const file = new File([croppedBlob], selectedFile.name, { type: 'image/png' }) const formData = new FormData() formData.append('avatar', file) const response = await authFetch(`/api/users/${user.id}/avatar`, { method: 'POST', body: formData, }) if (response.ok) { setSuccess('Profilbild erfolgreich hochgeladen') setShowCropModal(false) setSelectedFile(null) setCropImage(null) // Lade Profilbild neu loadAvatar() } else { const errorData = await response.json() setError(errorData.error || 'Fehler beim Hochladen des Profilbilds') } } catch (err) { setError('Fehler beim Hochladen des Profilbilds') console.error('Error uploading avatar:', err) } finally { setUploadingAvatar(false) } } const handleSubmit = async (e) => { e.preventDefault() setError('') setSuccess('') setShowSuccessAnimation(false) setLoading(true) // Validierung: Passwort-Bestätigung muss übereinstimmen if (formData.password && formData.password !== formData.confirmPassword) { setError('Die Passwörter stimmen nicht überein') setLoading(false) return } // Validierung: Wenn Passwort geändert wird, muss altes Passwort vorhanden sein if (formData.password && !formData.oldPassword) { setError('Bitte geben Sie das alte Passwort ein, um das Passwort zu ändern') setLoading(false) return } try { const body = { // Nur der spezielle Admin-User mit UID 'admin': Username und Email nicht ändern // Andere Admin-User können ihre Daten ändern ...(user?.id !== 'admin' && formData.username && { username: formData.username }), ...(user?.id !== 'admin' && formData.email && { email: formData.email }), ...(formData.password && { password: formData.password, oldPassword: formData.oldPassword }) } const response = await authFetch(`/api/users/${user.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }) if (response.ok) { setShowSuccessAnimation(true) // Warte kurz, damit Animation sichtbar ist setTimeout(() => { setShowSuccessAnimation(false) // Aktualisiere User-Daten im AuthContext window.location.reload() }, 2000) } else { const errorData = await response.json() setError(errorData.error || 'Fehler beim Aktualisieren des Profils') setShowSuccessAnimation(false) } } catch (err) { setError('Fehler beim Aktualisieren des Profils') setShowSuccessAnimation(false) console.error('Error updating profile:', err) } finally { setLoading(false) } } const handleChange = (e) => { setFormData({ ...formData, [e.target.name]: e.target.value }) // Clear success/error messages when user starts typing if (success) setSuccess('') if (error) setError('') } if (!user) { return (

Lade Profil...

) } return (

Mein Profil

Verwalten Sie Ihre persönlichen Daten und Einstellungen.

{/* Profilbild */}
{avatarUrl ? ( Profilbild { // Wenn Bild nicht geladen werden kann, setze avatarUrl auf null setAvatarUrl(null) }} /> ) : (
👤
)}

{user.username}

{user.email}

{avatarUrl ? 'Klicken Sie auf das Kamera-Icon, um Ihr Profilbild zu ändern' : 'Klicken Sie auf das Kamera-Icon, um ein Profilbild hochzuladen'}

{/* Profil bearbeiten Formular */}
{user?.id === 'admin' && (

Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden

)}
{user?.id === 'admin' && (

Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden

)}

Passwort ändern

Lassen Sie die Felder leer, wenn Sie Ihr Passwort nicht ändern möchten.

{/* Passwortrichtlinie - nur anzeigen wenn Passwort eingegeben wird */} {formData.password && (

Passwortrichtlinie:

  • = 8 ? 'text-green-400' : ''}`}> {formData.password.length >= 8 ? '✓' : '○'} Mindestens 8 Zeichen
  • {/[A-Z]/.test(formData.password) ? '✓' : '○'} Mindestens ein Großbuchstabe
  • {/[a-z]/.test(formData.password) ? '✓' : '○'} Mindestens ein Kleinbuchstabe
  • {/[0-9]/.test(formData.password) ? '✓' : '○'} Mindestens eine Zahl
  • {/[^A-Za-z0-9]/.test(formData.password) ? '✓' : '○'} Mindestens ein Sonderzeichen
)}
{formData.confirmPassword && formData.password !== formData.confirmPassword && (

Die Passwörter stimmen nicht überein

)} {formData.confirmPassword && formData.password === formData.confirmPassword && formData.password && (

✓ Passwörter stimmen überein

)}
{error && (
{error}
)}
{/* Success Animation Popup */} {showSuccessAnimation && (
{/* Ping Animation */}
{/* Checkmark Circle */}

Profil erfolgreich aktualisiert

)} {/* Crop Modal */} {showCropModal && cropImage && (

Profilbild zuschneiden

Verschieben Sie den Kreis, um den gewünschten Bereich auszuwählen. Ziehen Sie an den Ecken, um die Größe zu ändern.

{ if (isDragging) handleCropDrag(e) if (isResizing) return }} onMouseUp={handleCropDragEnd} onMouseLeave={handleCropDragEnd} > Zu schneidendes Bild {/* Crop-Bereich (Kreis) */} {imageDimensions.width > 0 && (
{/* Resize-Handles an den Ecken */}
)}
)}
) } export default Profile