feat: drag and drop text layers in badge editor, anchor flyers/recipes above footer, and editable titles in flyers section
This commit is contained in:
1 parent
99dd0f272e
commit
f9aa2297ba
3 files changed
+205
-43
No files matched your search
@@ -4,7 +4,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { toPng } from 'html-to-image';
|
||||
import { Tag, Plus, Check, Trash2, X, Upload, Eye, FileCode } from 'lucide-react';
|
||||
import { Tag, Plus, Check, Trash2, X, Upload, Eye, FileCode, Move } from 'lucide-react';
|
||||
|
||||
interface BadgeEditorModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -72,6 +72,98 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
const [isExportingPng, setIsExportingPng] = useState(false);
|
||||
const [isSavingDb, setIsSavingDb] = useState(false);
|
||||
|
||||
// Stav pre priamy Drag & Drop vrstiev na plátne
|
||||
const [dragState, setDragState] = useState<{
|
||||
layerId: string;
|
||||
startX: number;
|
||||
startY: number;
|
||||
layerStartX: number;
|
||||
layerStartY: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleLayerMouseDown = (e: React.MouseEvent, layer: BadgeLayer) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setSelectedLayerId(layer.id);
|
||||
setDragState({
|
||||
layerId: layer.id,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
layerStartX: layer.x,
|
||||
layerStartY: layer.y
|
||||
});
|
||||
};
|
||||
|
||||
const handleLayerTouchStart = (e: React.TouchEvent, layer: BadgeLayer) => {
|
||||
e.stopPropagation();
|
||||
const touch = e.touches[0];
|
||||
if (!touch) return;
|
||||
setSelectedLayerId(layer.id);
|
||||
setDragState({
|
||||
layerId: layer.id,
|
||||
startX: touch.clientX,
|
||||
startY: touch.clientY,
|
||||
layerStartX: layer.x,
|
||||
layerStartY: layer.y
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!dragState) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const deltaX = e.clientX - dragState.startX;
|
||||
const deltaY = e.clientY - dragState.startY;
|
||||
const newX = Math.round(dragState.layerStartX + deltaX);
|
||||
const newY = Math.round(dragState.layerStartY + deltaY);
|
||||
|
||||
setActiveBadge((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
layersConfig: prev.layersConfig.map((l) =>
|
||||
l.id === dragState.layerId ? { ...l, x: newX, y: newY } : l
|
||||
)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
const touch = e.touches[0];
|
||||
if (!touch) return;
|
||||
const deltaX = touch.clientX - dragState.startX;
|
||||
const deltaY = touch.clientY - dragState.startY;
|
||||
const newX = Math.round(dragState.layerStartX + deltaX);
|
||||
const newY = Math.round(dragState.layerStartY + deltaY);
|
||||
|
||||
setActiveBadge((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
layersConfig: prev.layersConfig.map((l) =>
|
||||
l.id === dragState.layerId ? { ...l, x: newX, y: newY } : l
|
||||
)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setDragState(null);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleDragEnd);
|
||||
window.addEventListener('touchmove', handleTouchMove);
|
||||
window.addEventListener('touchend', handleDragEnd);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', handleDragEnd);
|
||||
window.removeEventListener('touchmove', handleTouchMove);
|
||||
window.removeEventListener('touchend', handleDragEnd);
|
||||
};
|
||||
}, [dragState]);
|
||||
|
||||
// Načítanie písiem pre všetky vrstvy aktívnej cenovky
|
||||
useEffect(() => {
|
||||
if (activeBadge?.layersConfig) {
|
||||
@@ -388,6 +480,9 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
<span className="text-xs font-semibold text-slate-400 flex items-center gap-1.5">
|
||||
<Eye className="w-3.5 h-3.5" /> Náhľad cenovky
|
||||
</span>
|
||||
<span className="text-[10px] text-blue-400 bg-blue-950/60 border border-blue-800/60 px-2.5 py-0.5 rounded-full flex items-center gap-1">
|
||||
<Move className="w-3 h-3" /> Drag & Drop textových polí
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{activeBadge ? (
|
||||
@@ -395,22 +490,27 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
{/* Vektorové plátno pre export do PNG */}
|
||||
<div
|
||||
ref={previewRef}
|
||||
className="relative flex items-center justify-center p-8 select-none"
|
||||
style={{ minWidth: 160, minHeight: 120 }}
|
||||
className="relative inline-block select-none"
|
||||
>
|
||||
{/* SVG Podklad */}
|
||||
<div
|
||||
className="relative"
|
||||
className="block pointer-events-none"
|
||||
dangerouslySetInnerHTML={{ __html: activeBadge.svgRaw || '' }}
|
||||
/>
|
||||
|
||||
{/* Dynamické textové vrstvy nad SVG podkladom */}
|
||||
{/* Dynamické textové vrstvy nad SVG podkladom (Drag & Drop) */}
|
||||
{activeBadge.layersConfig.map((layer) => {
|
||||
const isSelected = layer.id === selectedLayerId;
|
||||
const isDraggingThis = dragState?.layerId === layer.id;
|
||||
return (
|
||||
<div
|
||||
key={layer.id}
|
||||
onClick={() => setSelectedLayerId(layer.id)}
|
||||
onMouseDown={(e) => handleLayerMouseDown(e, layer)}
|
||||
onTouchStart={(e) => handleLayerTouchStart(e, layer)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedLayerId(layer.id);
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${layer.x}px`,
|
||||
@@ -423,15 +523,24 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
? `'${layer.fontFamily}', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`
|
||||
: 'Poppins, Arial, sans-serif',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer'
|
||||
cursor: isDraggingThis ? 'grabbing' : 'grab',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
className={`px-1.5 py-0.5 rounded transition-all ${
|
||||
className={`px-1.5 py-0.5 rounded transition-all select-none ${
|
||||
isSelected
|
||||
? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-transparent bg-blue-500/10'
|
||||
: 'hover:outline hover:outline-1 hover:outline-white/40'
|
||||
? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-slate-900 bg-blue-500/20 shadow-lg'
|
||||
: 'hover:outline hover:outline-1 hover:outline-white/60 hover:bg-white/10'
|
||||
}`}
|
||||
title="Kliknite a ťahajte myšou pre zmenu pozície (Drag & Drop)"
|
||||
>
|
||||
{replaceVariables(layer.text || '')}
|
||||
|
||||
{/* Indikátor pozície a uchopenia */}
|
||||
{isSelected && (
|
||||
<span className="absolute -top-5 left-1/2 -translate-x-1/2 text-[9px] font-mono font-bold bg-slate-900 text-blue-300 px-1.5 py-0.5 rounded shadow border border-slate-700 pointer-events-none whitespace-nowrap flex items-center gap-1 z-20">
|
||||
<Move className="w-2.5 h-2.5" /> {layer.x}px, {layer.y}px
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in new issue
Block a user