236 lines
12 KiB
TypeScript
236 lines
12 KiB
TypeScript
import React, { useCallback } from 'react';
|
|
import { X, Stamp, Image as ImageIcon, Type, AtSign, Globe, Instagram } from 'lucide-react';
|
|
import { useEditor } from '../../context/EditorContext';
|
|
import { FileDropZone } from '../ui/FileDropZone';
|
|
import { TimelineElement } from '../../types';
|
|
|
|
interface StickersPanelProps {
|
|
onClose: () => void;
|
|
}
|
|
|
|
/**
|
|
* Panel for brand assets: branded text presets, social handles, stickers.
|
|
* Text presets use the brand font, color, and name from designMD.
|
|
*/
|
|
export const StickersPanel: React.FC<StickersPanelProps> = ({ onClose }) => {
|
|
const {
|
|
designMD, brandContent,
|
|
layers, setLayers,
|
|
activeLayerId, setActiveLayerId,
|
|
setTimelineElements,
|
|
setSelectedElementId,
|
|
playerRef,
|
|
durationInFrames,
|
|
} = useEditor();
|
|
|
|
const brandName = designMD.brandName || 'Mi Marca';
|
|
const font = designMD.baseFont || 'system-ui';
|
|
const color = designMD.textColor || '#ffffff';
|
|
const social = designMD.socialHandles || {};
|
|
|
|
const brandContentThumbnails = (brandContent || [])
|
|
.filter(p => p.thumbnail)
|
|
.map(p => ({ src: p.thumbnail!, name: p.name, id: p.id }));
|
|
|
|
const legacyStickers = designMD.brandStickers || [];
|
|
|
|
// Add branded text element to a visual layer
|
|
const addBrandText = useCallback((content: string, fontSize?: number, y?: number) => {
|
|
const currentFrame = playerRef.current?.getCurrentFrame() || 0;
|
|
const newId = 'el-' + Date.now();
|
|
|
|
let targetLayerId = activeLayerId;
|
|
const activeLayer = layers.find(l => l.id === activeLayerId);
|
|
if (!activeLayer || activeLayer.type === 'brand' || activeLayer.type === 'video' || activeLayer.type === 'audio') {
|
|
let visualLayer = layers.find(l => l.type === 'visual' || l.type == null);
|
|
if (!visualLayer) {
|
|
visualLayer = { id: 'layer-' + Date.now(), name: 'Capa Gráfica 1', type: 'visual' };
|
|
setLayers(prev => [...prev, visualLayer!]);
|
|
}
|
|
targetLayerId = visualLayer.id;
|
|
setActiveLayerId(targetLayerId);
|
|
}
|
|
|
|
const newElement: TimelineElement = {
|
|
id: newId,
|
|
layerId: targetLayerId,
|
|
type: 'text',
|
|
content,
|
|
startFrame: currentFrame,
|
|
endFrame: Math.min(durationInFrames, currentFrame + 100),
|
|
x: 50,
|
|
y: y ?? 50,
|
|
fontSize,
|
|
fontFamily: font,
|
|
color: color,
|
|
useBranding: true,
|
|
};
|
|
|
|
setTimelineElements(prev => [...prev, newElement]);
|
|
setSelectedElementId(newId);
|
|
}, [layers, activeLayerId, playerRef, durationInFrames, setTimelineElements, setSelectedElementId, setLayers, setActiveLayerId, font, color]);
|
|
|
|
// Build social text presets
|
|
const socialPresets: { label: string; content: string; icon: React.ReactNode }[] = [];
|
|
if (social.instagram) socialPresets.push({ label: 'Instagram', content: social.instagram, icon: <Instagram size={12} /> });
|
|
if (social.tiktok) socialPresets.push({ label: 'TikTok', content: social.tiktok, icon: <AtSign size={12} /> });
|
|
if (social.twitter) socialPresets.push({ label: 'Twitter/X', content: social.twitter, icon: <AtSign size={12} /> });
|
|
if (social.youtube) socialPresets.push({ label: 'YouTube', content: social.youtube, icon: <AtSign size={12} /> });
|
|
if (social.website) socialPresets.push({ label: 'Web', content: social.website, icon: <Globe size={12} /> });
|
|
|
|
return (
|
|
<div className="w-64 bg-neutral-900 border-r border-neutral-800/60 flex flex-col h-full z-10 shrink-0 shadow-lg animate-in slide-in-from-left-2 duration-200">
|
|
<div className="p-3 border-b border-neutral-800 flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
|
<Stamp size={14} className="text-amber-400" />
|
|
Marca
|
|
</h3>
|
|
<button onClick={onClose} title="Cerrar Panel" className="text-neutral-400 hover:text-white p-1 rounded-md hover:bg-neutral-800 transition-colors">
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-3 flex-1 overflow-y-auto space-y-4">
|
|
|
|
{/* ═══ Textos de Marca ═══ */}
|
|
<div>
|
|
<span className="text-[10px] font-semibold text-neutral-500 uppercase tracking-widest mb-2 block">Textos de Marca</span>
|
|
<div className="space-y-1.5">
|
|
{/* Brand name — large */}
|
|
<button
|
|
onClick={() => addBrandText(brandName, 64, 40)}
|
|
title={`Añadir "${brandName}" como título`}
|
|
className="w-full flex items-center gap-2.5 px-3 py-2.5 bg-neutral-950/60 border border-amber-900/30 rounded-lg text-left hover:border-amber-500/40 hover:bg-amber-950/20 transition-all group"
|
|
>
|
|
<div className="w-8 h-8 rounded-md bg-amber-600/15 border border-amber-500/30 flex items-center justify-center shrink-0">
|
|
<Type size={14} className="text-amber-400" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<span className="text-xs font-bold text-white block truncate" style={{ fontFamily: font }}>{brandName}</span>
|
|
<span className="text-[9px] text-neutral-600">Título grande · 64px</span>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Brand name — subtitle */}
|
|
<button
|
|
onClick={() => addBrandText(brandName, 36, 50)}
|
|
title={`Añadir "${brandName}" como subtítulo`}
|
|
className="w-full flex items-center gap-2.5 px-3 py-2 bg-neutral-950/60 border border-neutral-800/60 rounded-lg text-left hover:border-amber-500/30 hover:bg-amber-950/10 transition-all group"
|
|
>
|
|
<div className="w-7 h-7 rounded-md bg-neutral-800 flex items-center justify-center shrink-0">
|
|
<Type size={12} className="text-neutral-400 group-hover:text-amber-400 transition-colors" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<span className="text-[11px] font-medium text-neutral-300 block truncate" style={{ fontFamily: font }}>{brandName}</span>
|
|
<span className="text-[9px] text-neutral-600">Subtítulo · 36px</span>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Brand name — small watermark */}
|
|
<button
|
|
onClick={() => addBrandText(brandName, 20, 90)}
|
|
title={`Añadir "${brandName}" como marca de agua`}
|
|
className="w-full flex items-center gap-2.5 px-3 py-2 bg-neutral-950/60 border border-neutral-800/60 rounded-lg text-left hover:border-amber-500/30 hover:bg-amber-950/10 transition-all group"
|
|
>
|
|
<div className="w-7 h-7 rounded-md bg-neutral-800 flex items-center justify-center shrink-0">
|
|
<Type size={10} className="text-neutral-500 group-hover:text-amber-400 transition-colors" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<span className="text-[10px] text-neutral-400 block truncate" style={{ fontFamily: font }}>{brandName}</span>
|
|
<span className="text-[9px] text-neutral-600">Marca de agua · 20px</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ═══ Redes Sociales ═══ */}
|
|
{socialPresets.length > 0 && (
|
|
<div>
|
|
<span className="text-[10px] font-semibold text-neutral-500 uppercase tracking-widest mb-2 block">Redes Sociales</span>
|
|
<div className="space-y-1.5">
|
|
{socialPresets.map((sp) => (
|
|
<button
|
|
key={sp.label}
|
|
onClick={() => addBrandText(sp.content, 28, 85)}
|
|
title={`Añadir ${sp.label}: ${sp.content}`}
|
|
className="w-full flex items-center gap-2.5 px-3 py-2 bg-neutral-950/60 border border-neutral-800/60 rounded-lg text-left hover:border-violet-500/30 hover:bg-violet-950/10 transition-all group"
|
|
>
|
|
<div className="w-7 h-7 rounded-md bg-violet-600/15 border border-violet-500/30 flex items-center justify-center shrink-0 text-violet-400">
|
|
{sp.icon}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<span className="text-[11px] font-medium text-neutral-300 block truncate">{sp.content}</span>
|
|
<span className="text-[9px] text-neutral-600">{sp.label} · 28px</span>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ═══ Contenido Visual de Marca ═══ */}
|
|
{brandContentThumbnails.length > 0 && (
|
|
<div>
|
|
<span className="text-[10px] font-semibold text-neutral-500 uppercase tracking-widest mb-2 block">Contenido Visual</span>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{brandContentThumbnails.map(item => (
|
|
<div
|
|
key={item.id}
|
|
className="aspect-square bg-neutral-800 rounded-lg overflow-hidden group relative cursor-grab active:cursor-grabbing flex items-center justify-center p-2"
|
|
draggable
|
|
onDragStart={(e) => {
|
|
e.dataTransfer.setData('text/plain', item.src);
|
|
e.dataTransfer.setData('application/json', JSON.stringify({ type: 'sticker', src: item.src, brandContentId: item.id }));
|
|
e.dataTransfer.effectAllowed = 'copy';
|
|
}}
|
|
>
|
|
<img src={item.src} className="w-full h-full object-contain group-hover:scale-110 transition-transform duration-300 drop-shadow-md" alt={item.name} draggable={false} />
|
|
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center pointer-events-none">
|
|
<span className="text-[10px] text-white bg-black/50 px-2 py-1 rounded">Arrastrar</span>
|
|
<span className="text-[8px] text-neutral-300 mt-0.5">{item.name}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Legacy Stickers */}
|
|
{legacyStickers.length > 0 && (
|
|
<div>
|
|
<span className="text-[10px] font-semibold text-neutral-500 uppercase tracking-widest mb-2 block">Stickers</span>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{legacyStickers.map((src, i) => (
|
|
<div
|
|
key={`sticker-${i}`}
|
|
className="aspect-square bg-neutral-800 rounded-lg overflow-hidden group relative cursor-grab active:cursor-grabbing flex items-center justify-center p-2"
|
|
draggable
|
|
onDragStart={(e) => {
|
|
e.dataTransfer.setData('text/plain', src);
|
|
e.dataTransfer.setData('application/json', JSON.stringify({ type: 'sticker', src }));
|
|
e.dataTransfer.effectAllowed = 'copy';
|
|
}}
|
|
>
|
|
<img src={src} className="w-full h-full object-contain group-hover:scale-110 transition-transform duration-300 drop-shadow-md" alt="Sticker" draggable={false} />
|
|
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
|
|
<span className="text-[10px] text-white bg-black/50 px-2 py-1 rounded">Arrastrar</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Upload */}
|
|
<FileDropZone
|
|
accept="image/*"
|
|
multiple
|
|
onFiles={() => {}}
|
|
label="Subir assets de marca"
|
|
sublabel="PNG con transparencia"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|