diff --git a/app/components/MultiAspectEditor.tsx b/app/components/MultiAspectEditor.tsx new file mode 100644 index 0000000..7c8fbed --- /dev/null +++ b/app/components/MultiAspectEditor.tsx @@ -0,0 +1,153 @@ +import React, { useState, useCallback } from 'react'; +import Cropper from 'react-easy-crop'; +import { ImageIcon, Maximize2, RefreshCw, Copy, Check, X } from 'lucide-react'; + +const ASPECT_RATIOS = [ + { id: 'hero', label: 'Hero Banner (21:9)', ratio: 21 / 9 }, + { id: 'news', label: 'Notícia/Media (16:9)', ratio: 16 / 9 }, + { id: 'square', label: 'Quadrado/Social (1:1)', ratio: 1 / 1 }, +]; + +const MultiAspectEditor = ({ image, onClose, onExport }: { image: string, onClose: () => void, onExport: (results: Record) => void }) => { + const [crops, setCrops] = useState( + ASPECT_RATIOS.reduce((acc, curr) => ({ + ...acc, + [curr.id]: { crop: { x: 0, y: 0 }, zoom: 1, croppedAreaPixels: null } + }), {}) + ); + + const onCropChange = (id: string, crop: any) => { + setCrops(prev => ({ ...prev, [id]: { ...(prev as any)[id], crop } })); + }; + + const onZoomChange = (id: string, zoom: number) => { + setCrops(prev => ({ ...prev, [id]: { ...(prev as any)[id], zoom } })); + }; + + const onCropComplete = useCallback((id: string, _: any, croppedAreaPixels: any) => { + setCrops(prev => ({ ...prev, [id]: { ...(prev as any)[id], croppedAreaPixels } })); + }, []); + + const generateBase64 = async () => { + const results: Record = {}; + for (const ratio of ASPECT_RATIOS) { + const pixelCrop = (crops as any)[ratio.id].croppedAreaPixels; + if (pixelCrop) { + results[ratio.id as keyof typeof results] = await getCroppedImg(image, pixelCrop); + } + } + onExport(results); + }; + + return ( +
+
+ + {/* Header do Modal */} +
+
+

Editor de Enquadramento

+

Ajuste a imagem para os diferentes formatos do portal

+
+ +
+ + {/* Área de Edição - Grid de Croppers */} +
+
+ {ASPECT_RATIOS.map((r) => ( +
+
+ {r.label} + +
+ + {/* Container do Cropper */} +
+ onCropChange(r.id, c)} + onCropComplete={(_, pix) => onCropComplete(r.id, _, pix)} + onZoomChange={(z) => onZoomChange(r.id, z)} + /> +
+ + {/* Slider de Zoom Customizado */} +
+ + onZoomChange(r.id, Number(e.target.value))} + className="w-full h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-blue-600" + /> +
+
+ ))} +
+
+ + {/* Footer com Ações */} +
+ + +
+
+
+ ); +}; + +// Função Utilitária para Canvas -> Base64 +async function getCroppedImg(imageSrc: string, pixelCrop: any) { + const image = await new Promise((resolve, reject) => { + const img = new Image(); + img.addEventListener('load', () => resolve(img)); + img.addEventListener('error', (error) => reject(error)); + img.setAttribute('crossOrigin', 'anonymous'); + img.src = imageSrc; + }); + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | null; + if (!ctx) return ''; + + canvas.width = pixelCrop.width; + canvas.height = pixelCrop.height; + + ctx.drawImage( + image as CanvasImageSource, + pixelCrop.x, + pixelCrop.y, + pixelCrop.width, + pixelCrop.height, + 0, + 0, + pixelCrop.width, + pixelCrop.height + ); + + return canvas.toDataURL('image/jpeg', 0.9); +} + +export default MultiAspectEditor; \ No newline at end of file diff --git a/app/create-news/page.tsx b/app/create-news/page.tsx index 1a1e68e..757f3d6 100644 --- a/app/create-news/page.tsx +++ b/app/create-news/page.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +"use client"; +import React, { useState, useRef } from 'react'; import Image from 'next/image'; import { LayoutDashboard, Newspaper, Users, BarChart3, @@ -6,19 +7,58 @@ import { Type, Calendar, Clock, Tag, User, Save, Eye, Send } from 'lucide-react'; +// Importe o componente que criámos (ajuste o caminho se necessário) +import MultiAspectEditor from '../components/MultiAspectEditor'; + const CreateNewsPage = () => { + // 1. Estados para o Crop + const [tempImage, setTempImage] = useState(null); + const [isEditorOpen, setIsEditorOpen] = useState(false); + const [finalCrops, setFinalCrops] = useState(null); // Guarda os Base64 finais + + const fileInputRef = useRef(null); + + // 2. Lógica de Upload + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const reader = new FileReader(); + reader.readAsDataURL(e.target.files[0]); + reader.onload = () => { + setTempImage(reader.result as string); + setIsEditorOpen(true); + }; + } + }; + + const triggerUpload = () => { + fileInputRef.current?.click(); + }; + return (
- {/* Sidebar Lateral - Glassmorphism */} + {/* --- MODAL DO EDITOR (Renderiza fora do fluxo quando ativo) --- */} + {isEditorOpen && tempImage && ( + setIsEditorOpen(false)} + onExport={(data) => { + setFinalCrops(data); // Aqui tens o objeto com hero, news, square em Base64 + setIsEditorOpen(false); + console.log("Imagens prontas para envio:", data); + }} + /> + )} + + {/* Sidebar Lateral */}
- {/* Coluna Principal (Editor) */} + {/* Coluna Principal */}
@@ -86,12 +127,10 @@ const CreateNewsPage = () => { className="w-full text-xl font-bold border-none focus:ring-0 placeholder:text-slate-300 p-0" />
- - {/* Toolbar Simplificada */}
- - + +
B I @@ -102,42 +141,58 @@ const CreateNewsPage = () => { className="w-full border-none focus:ring-0 resize-none text-slate-700 leading-relaxed p-0" />
- -
- -