import React, { useState, useRef, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { RotateCw, RotateCcw, Crop as CropIcon, Download, X, Check, Save, Image as ImageIcon, Sliders } from 'lucide-react'; import { updatePicture } from '@/modules/posts/client-pictures'; import { useAuth } from '@/hooks/useAuth'; import { useToast } from '@/components/ui/use-toast'; import { uploadImage } from '@/lib/uploadUtils'; import { Slider } from '@/components/ui/slider'; interface ImageEditorProps { imageUrl: string; pictureId: string; onSave?: (newUrl: string) => void; onClose?: () => void; } export const ImageEditor = ({ imageUrl, pictureId, onSave, onClose }: ImageEditorProps) => { const [previewUrl, setPreviewUrl] = useState(imageUrl); const [loading, setLoading] = useState(false); const { toast } = useToast(); const { user } = useAuth(); const imageRef = useRef(null); // Edit State const [mode, setMode] = useState<'view' | 'crop' | 'adjust'>('view'); const [brightness, setBrightness] = useState(1); const [contrast, setContrast] = useState(1); // Reset adjustments when image changes or mode resets (optional, depends on UX) // For now, these are "pending" adjustments locally until applied. useEffect(() => { setPreviewUrl(imageUrl); // Reset local edits on new image setBrightness(1); setContrast(1); setMode('view'); }, [imageUrl]); const executeOperation = async (ops: any[]) => { if (!previewUrl) return; setLoading(true); try { const response = await fetch(previewUrl); const blob = await response.blob(); const file = new File([blob], "source.jpg", { type: blob.type }); const formData = new FormData(); formData.append('file', file); formData.append('operations', JSON.stringify(ops)); const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL; const res = await fetch(`${serverUrl}/api/images/transform`, { method: 'POST', body: formData }); if (!res.ok) throw new Error(await res.text()); const resultBlob = await res.blob(); const resultUrl = URL.createObjectURL(resultBlob); setPreviewUrl(resultUrl); toast({ title: "Applied" }); } catch (err: any) { toast({ title: "Operation failed", description: err.message, variant: "destructive" }); } finally { setLoading(false); } }; const handleRotate = (angle: number) => { executeOperation([{ type: 'rotate', angle }]); }; const handleSave = async () => { if (!previewUrl || !pictureId || !user) return; setLoading(true); try { const response = await fetch(previewUrl); const blob = await response.blob(); const file = new File([blob], "edited.jpg", { type: "image/jpeg" }); const { publicUrl } = await uploadImage(file, user.id); await updatePicture(pictureId, { image_url: publicUrl, updated_at: new Date().toISOString() }); toast({ title: "Image Saved", description: "Source image updated successfully." }); onSave?.(publicUrl); } catch (err: any) { console.error(err); toast({ title: "Save failed", description: err.message, variant: "destructive" }); } finally { setLoading(false); } }; // --- Visual Cropper State --- const [cropRect, setCropRect] = useState<{ x: number, y: number, w: number, h: number } | null>(null); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState<{ x: number, y: number } | null>(null); const [dragAction, setDragAction] = useState<'move' | 'nw' | 'ne' | 'sw' | 'se' | 'create' | null>(null); const [aspectRatio, setAspectRatio] = useState(null); const PRESETS = [ { label: 'Free', value: null }, { label: 'Original', value: 'original' }, { label: 'Square', value: 1 }, { label: '9:16', value: 9 / 16 }, { label: '16:9', value: 16 / 9 }, { label: '4:5', value: 4 / 5 }, { label: '5:4', value: 5 / 4 }, { label: '3:4', value: 3 / 4 }, { label: '4:3', value: 4 / 3 }, ]; useEffect(() => { if (mode === 'crop' && imageRef.current) { const { width, height } = imageRef.current.getBoundingClientRect(); const w = width * 0.8; const h = height * 0.8; setCropRect({ x: (width - w) / 2, y: (height - h) / 2, w, h }); } }, [mode]); const getPointerPos = (e: React.PointerEvent | PointerEvent) => { if (!imageRef.current) return { x: 0, y: 0 }; const rect = imageRef.current.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; }; const handlePointerDown = (e: React.PointerEvent) => { if (mode !== 'crop' || !cropRect) return; e.preventDefault(); e.stopPropagation(); // Capture pointer to ensure we get move/up events even if we leave the div (e.target as Element).setPointerCapture(e.pointerId); const { x, y } = getPointerPos(e); const handleSize = 40; // Increased hit area for easier touch const r = cropRect; if (Math.abs(x - r.x) < handleSize && Math.abs(y - r.y) < handleSize) setDragAction('nw'); else if (Math.abs(x - (r.x + r.w)) < handleSize && Math.abs(y - r.y) < handleSize) setDragAction('ne'); else if (Math.abs(x - r.x) < handleSize && Math.abs(y - (r.y + r.h)) < handleSize) setDragAction('sw'); else if (Math.abs(x - (r.x + r.w)) < handleSize && Math.abs(y - (r.y + r.h)) < handleSize) setDragAction('se'); else if (x > r.x && x < r.x + r.w && y > r.y && y < r.y + r.h) setDragAction('move'); else setDragAction('create'); setIsDragging(true); setDragStart({ x, y }); }; const handlePointerMove = (e: React.PointerEvent) => { if (!isDragging || !dragStart || !cropRect || !imageRef.current) return; const { x, y } = getPointerPos(e); const dx = x - dragStart.x; const dy = y - dragStart.y; const imgW = imageRef.current.offsetWidth; const imgH = imageRef.current.offsetHeight; setCropRect(prev => { if (!prev) return null; let next = { ...prev }; if (dragAction === 'move') { next.x = Math.max(0, Math.min(imgW - next.w, prev.x + dx)); next.y = Math.max(0, Math.min(imgH - next.h, prev.y + dy)); } else { let newW = next.w; let newH = next.h; let newX = next.x; let newY = next.y; if (dragAction === 'se') { newW = Math.max(20, prev.w + dx); newH = Math.max(20, prev.h + dy); } else if (dragAction === 'sw') { newW = prev.w - dx; newX = Math.min(prev.x + prev.w - 20, prev.x + dx); newH = Math.max(20, prev.h + dy); } else if (dragAction === 'ne') { newW = Math.max(20, prev.w + dx); newH = prev.h - dy; newY = Math.min(prev.y + prev.h - 20, prev.y + dy); } else if (dragAction === 'nw') { newW = prev.w - dx; newX = Math.min(prev.x + prev.w - 20, prev.x + dx); newH = prev.h - dy; newY = Math.min(prev.y + prev.h - 20, prev.y + dy); } if (aspectRatio !== null) { if (dragAction === 'se' || dragAction === 'sw') { newH = newW / aspectRatio; } else { newW = newH * aspectRatio; if (dragAction === 'nw') newX = prev.x + prev.w - newW; } newH = newW / aspectRatio; if (dragAction === 'nw') { newY = prev.y + prev.h - newH; newX = prev.x + prev.w - newW; } else if (dragAction === 'ne') { newY = prev.y + prev.h - newH; } } next.w = Math.max(20, newW); next.h = Math.max(20, newH); next.x = newX; next.y = newY; } return next; }); setDragStart({ x, y }); }; const handlePointerUp = (e: React.PointerEvent) => { setIsDragging(false); setDragAction(null); (e.target as Element).releasePointerCapture(e.pointerId); }; const applyCrop = () => { if (!cropRect || !imageRef.current) return; const displayRect = imageRef.current.getBoundingClientRect(); const scaleX = imageRef.current.naturalWidth / displayRect.width; const scaleY = imageRef.current.naturalHeight / displayRect.height; let realX = Math.round(cropRect.x * scaleX); let realY = Math.round(cropRect.y * scaleY); let realW = Math.round(cropRect.w * scaleX); let realH = Math.round(cropRect.h * scaleY); realX = Math.max(0, realX); realY = Math.max(0, realY); realW = Math.max(1, Math.min(realW, imageRef.current.naturalWidth - realX)); realH = Math.max(1, Math.min(realH, imageRef.current.naturalHeight - realY)); const realCrop = { x: realX, y: realY, width: realW, height: realH }; executeOperation([{ type: 'crop', ...realCrop }]); setMode('view'); }; const applyAdjust = () => { // Send adjust op executeOperation([{ type: 'adjust', brightness: brightness !== 1 ? brightness : undefined, contrast: contrast !== 1 ? contrast : undefined }]); setMode('view'); // Reset sliders as they are applied to the "base" image now setBrightness(1); setContrast(1); }; const cancelAdjust = () => { setBrightness(1); setContrast(1); setMode('view'); }; const downloadImage = () => { if (previewUrl) { const a = document.createElement('a'); a.href = previewUrl; a.download = 'edited-image.jpg'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } }; return (
{/* Close Button if provided */} {onClose && (
)}
{previewUrl ? (
Work e.preventDefault()} /> {mode === 'crop' && cropRect && (
)}
) : (

No image loaded

)}
{/* Controls Container - enable pointer events for children */}
{mode === 'crop' && (
{PRESETS.map(p => ( ))}
)} {mode === 'adjust' && (
Brightness {Math.round(brightness * 100)}%
setBrightness(v)} />
Contrast {Math.round(contrast * 100)}%
setContrast(v)} />
)}
{mode === 'view' ? ( <>
) : mode === 'crop' ? ( <> Crop Mode ) : ( // Adjust Mode Toolbar <> Adjust Mode )}
{ loading && (
) }
); };