mirror of
https://github.com/PeterMaquiran/tvone.git
synced 2026-04-18 15:27:52 +00:00
153 lines
6.0 KiB
TypeScript
153 lines
6.0 KiB
TypeScript
|
|
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<string, string>) => 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<string, string> = {};
|
||
|
|
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 (
|
||
|
|
<div className="fixed inset-0 z-[100] bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||
|
|
<div className="bg-white w-full max-w-6xl h-[90vh] rounded-3xl shadow-2xl flex flex-col overflow-hidden border border-white/20">
|
||
|
|
|
||
|
|
{/* Header do Modal */}
|
||
|
|
<div className="p-6 border-b border-slate-100 flex justify-between items-center bg-white/50 backdrop-blur-md">
|
||
|
|
<div>
|
||
|
|
<h2 className="text-xl font-bold text-slate-900">Editor de Enquadramento</h2>
|
||
|
|
<p className="text-xs text-slate-500">Ajuste a imagem para os diferentes formatos do portal</p>
|
||
|
|
</div>
|
||
|
|
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-full transition-colors">
|
||
|
|
<X size={20} className="text-slate-400" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Área de Edição - Grid de Croppers */}
|
||
|
|
<div className="flex-1 overflow-y-auto p-8 bg-slate-50/50">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||
|
|
{ASPECT_RATIOS.map((r) => (
|
||
|
|
<div key={r.id} className="flex flex-col gap-4">
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<span className="text-[11px] font-bold uppercase tracking-wider text-slate-400">{r.label}</span>
|
||
|
|
<button
|
||
|
|
onClick={() => onZoomChange(r.id, 1)}
|
||
|
|
className="text-blue-600 p-1 hover:bg-blue-50 rounded text-[10px] flex items-center gap-1"
|
||
|
|
>
|
||
|
|
<RefreshCw size={12}/> Reset
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Container do Cropper */}
|
||
|
|
<div className="relative h-64 bg-slate-200 rounded-2xl overflow-hidden border border-slate-200 shadow-inner">
|
||
|
|
<Cropper
|
||
|
|
image={image}
|
||
|
|
crop={(crops as any)[r.id].crop}
|
||
|
|
zoom={(crops as any)[r.id].zoom}
|
||
|
|
aspect={r.ratio}
|
||
|
|
onCropChange={(c) => onCropChange(r.id, c)}
|
||
|
|
onCropComplete={(_, pix) => onCropComplete(r.id, _, pix)}
|
||
|
|
onZoomChange={(z) => onZoomChange(r.id, z)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Slider de Zoom Customizado */}
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<span className="text-slate-400"><Maximize2 size={14}/></span>
|
||
|
|
<input
|
||
|
|
type="range"
|
||
|
|
value={(crops as any)[r.id].zoom}
|
||
|
|
min={1}
|
||
|
|
max={3}
|
||
|
|
step={0.1}
|
||
|
|
aria-labelledby="Zoom"
|
||
|
|
onChange={(e) => 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"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Footer com Ações */}
|
||
|
|
<div className="p-6 border-t border-slate-100 bg-white flex justify-end gap-4">
|
||
|
|
<button onClick={onClose} className="px-6 py-2.5 text-sm font-semibold text-slate-500 hover:text-slate-700">
|
||
|
|
Cancelar
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={generateBase64}
|
||
|
|
className="px-8 py-2.5 bg-blue-600 text-white rounded-xl text-sm font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all flex items-center gap-2"
|
||
|
|
>
|
||
|
|
<Check size={18}/> Finalizar e Exportar
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 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;
|