import React, { useMemo, useRef, useState } from 'react' function clamp(value, min, max) { return Math.min(max, Math.max(min, value)) } export default function ProfileCoverEditor({ isOpen, onClose, coverUrl, coverPosition, onCoverUpdated, onCoverRemoved, }) { const previewRef = useRef(null) const [saving, setSaving] = useState(false) const [uploading, setUploading] = useState(false) const [removing, setRemoving] = useState(false) const [position, setPosition] = useState(coverPosition ?? 50) const csrfToken = useMemo( () => document.querySelector('meta[name="csrf-token"]')?.content ?? '', [] ) if (!isOpen) { return null } const updatePositionFromPointer = (clientY) => { const el = previewRef.current if (!el) return const rect = el.getBoundingClientRect() if (rect.height <= 0) return const normalized = ((clientY - rect.top) / rect.height) * 100 setPosition(Math.round(clamp(normalized, 0, 100))) } const handlePointerDown = (event) => { updatePositionFromPointer(event.clientY) const onMove = (moveEvent) => updatePositionFromPointer(moveEvent.clientY) const onUp = () => { window.removeEventListener('pointermove', onMove) window.removeEventListener('pointerup', onUp) } window.addEventListener('pointermove', onMove) window.addEventListener('pointerup', onUp) } const handleUpload = async (event) => { const file = event.target.files?.[0] if (!file) return setUploading(true) try { const body = new FormData() body.append('cover', file) const response = await fetch('/api/profile/cover/upload', { method: 'POST', headers: { 'X-CSRF-TOKEN': csrfToken, Accept: 'application/json', }, body, }) const payload = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(payload?.message || payload?.error || 'Cover upload failed.') } const nextPosition = Number.isFinite(payload.cover_position) ? payload.cover_position : 50 setPosition(nextPosition) onCoverUpdated(payload.cover_url, nextPosition) } catch (error) { window.alert(error?.message || 'Cover upload failed.') } finally { setUploading(false) event.target.value = '' } } const handleSavePosition = async () => { if (!coverUrl) return setSaving(true) try { const response = await fetch('/api/profile/cover/position', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken, Accept: 'application/json', }, body: JSON.stringify({ position }), }) const payload = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(payload?.message || payload?.error || 'Could not save position.') } onCoverUpdated(coverUrl, payload.cover_position ?? position) onClose() } catch (error) { window.alert(error?.message || 'Could not save position.') } finally { setSaving(false) } } const handleRemove = async () => { if (!coverUrl) return setRemoving(true) try { const response = await fetch('/api/profile/cover', { method: 'DELETE', headers: { 'X-CSRF-TOKEN': csrfToken, Accept: 'application/json', }, }) const payload = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(payload?.message || payload?.error || 'Could not remove cover.') } setPosition(payload.cover_position ?? 50) onCoverRemoved() onClose() } catch (error) { window.alert(error?.message || 'Could not remove cover.') } finally { setRemoving(false) } } return (
Allowed: JPG, PNG, WEBP. Max 5MB. Recommended: 1920x480.
Drag vertically to reposition the cover.