From 48d071d48e779f33ed35213a49939df02c0b7e2a Mon Sep 17 00:00:00 2001 From: Jandl-hostacny Date: Mon, 19 Oct 2026 10:03:01 +0200 Subject: [PATCH] feat: priblizenie platna v editore cenoviek (uloha 9) - posuvnik a tlacidla +/- (50-400 %), klik na percenta vrati 100 %, Ctrl + koliesko - tahanie vrstiev prepocitava posun podla priblizenia - platno sa posuva a vyplna vysku stlpca, vrch cenovky sa uz neoreze Co-Authored-By: Claude Opus 5.5 --- src/components/editor/BadgeEditorModal.tsx | 285 ++++++++++++++------- 1 file changed, 186 insertions(+), 99 deletions(-) diff --git a/src/components/editor/BadgeEditorModal.tsx b/src/components/editor/BadgeEditorModal.tsx index b120adf..95f26a8 100644 --- a/src/components/editor/BadgeEditorModal.tsx +++ b/src/components/editor/BadgeEditorModal.tsx @@ -4,7 +4,12 @@ import React, { useState, useRef, useEffect } from 'react'; import { BadgeTemplate, BadgeLayer } from '@/types/newsletter'; import { supabase } from '@/lib/supabase'; import { DEFAULT_BADGE_TEMPLATES, renderCleanBadgeSvg, svgToCleanPng } from '@/lib/badgeUtils'; -import { Tag, Plus, Check, Trash2, X, Upload, Eye, Move, AlignLeft, AlignCenter, AlignRight } from 'lucide-react'; +import { Tag, Plus, Check, Trash2, X, Upload, Eye, Move, AlignLeft, AlignCenter, AlignRight, ZoomIn, ZoomOut } from 'lucide-react'; + +// Priblíženie plátna v editore: rozsah posuvníka a kroky tlačidiel +/− +const CANVAS_ZOOM_MIN = 0.5; +const CANVAS_ZOOM_MAX = 4; +const CANVAS_ZOOM_STEPS = [0.5, 0.75, 1, 1.5, 2, 3, 4]; interface BadgeEditorModalProps { isOpen: boolean; @@ -71,6 +76,33 @@ export const BadgeEditorModal: React.FC = ({ const [isExportingPng, setIsExportingPng] = useState(false); const [isSavingDb, setIsSavingDb] = useState(false); + // Priblíženie plátna (len pre editor, neovplyvňuje výslednú cenovku) + const [canvasZoom, setCanvasZoom] = useState(1); + const canvasAreaRef = useRef(null); + const hasActiveBadge = !!activeBadge; + + const zoomIn = () => + setCanvasZoom((z) => CANVAS_ZOOM_STEPS.find((s) => s > z) ?? z); + const zoomOut = () => + setCanvasZoom((z) => [...CANVAS_ZOOM_STEPS].reverse().find((s) => s < z) ?? z); + + // Ctrl/Cmd + koliesko myši na plátne = priblíženie / oddialenie + useEffect(() => { + const el = canvasAreaRef.current; + if (!el) return; + const handleWheel = (e: WheelEvent) => { + if (!e.ctrlKey && !e.metaKey) return; + e.preventDefault(); + setCanvasZoom((z) => + e.deltaY < 0 + ? CANVAS_ZOOM_STEPS.find((s) => s > z) ?? z + : [...CANVAS_ZOOM_STEPS].reverse().find((s) => s < z) ?? z + ); + }; + el.addEventListener('wheel', handleWheel, { passive: false }); + return () => el.removeEventListener('wheel', handleWheel); + }, [isOpen, hasActiveBadge]); + // Stav pre priamy Drag & Drop vrstiev na plátne const [dragState, setDragState] = useState<{ layerId: string; @@ -111,8 +143,8 @@ export const BadgeEditorModal: React.FC = ({ if (!dragState) return; const handleMouseMove = (e: MouseEvent) => { - const deltaX = e.clientX - dragState.startX; - const deltaY = e.clientY - dragState.startY; + const deltaX = (e.clientX - dragState.startX) / canvasZoom; + const deltaY = (e.clientY - dragState.startY) / canvasZoom; const newX = Math.round(dragState.layerStartX + deltaX); const newY = Math.round(dragState.layerStartY + deltaY); @@ -130,8 +162,8 @@ export const BadgeEditorModal: React.FC = ({ 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 deltaX = (touch.clientX - dragState.startX) / canvasZoom; + const deltaY = (touch.clientY - dragState.startY) / canvasZoom; const newX = Math.round(dragState.layerStartX + deltaX); const newY = Math.round(dragState.layerStartY + deltaY); @@ -161,7 +193,7 @@ export const BadgeEditorModal: React.FC = ({ window.removeEventListener('touchmove', handleTouchMove); window.removeEventListener('touchend', handleDragEnd); }; - }, [dragState]); + }, [dragState, canvasZoom]); // Načítanie písiem pre všetky vrstvy aktívnej cenovky useEffect(() => { @@ -645,111 +677,166 @@ export const BadgeEditorModal: React.FC = ({ {/* 2. Stĺpec: Vektorový Canvas & Náhľad s Drag & Drop */} -
-
- - Náhľad na plátne - - - Drag & Drop vrstiev - +
+ {/* Lišta plátna: názov + ovládanie priblíženia */} +
+
+ + Náhľad na plátne + + + Drag & Drop + +
+ + {activeBadge && ( +
+ + setCanvasZoom(Number(e.target.value))} + className="w-24 h-1 accent-blue-400 cursor-pointer" + title="Priblíženie plátna" + /> + + +
+ )}
{activeBadge ? ( -
- {/* Obal simulujúci veľkosť, zarovnanie a odsadenie */} -
-
- {/* SVG Podklad */} + <> + {/* Priblížiteľná a posúvateľná oblasť plátna (vyplní zvyšnú výšku stĺpca) */} +
+ {/* Rezerva okolo cenovky, aby vrstvy a popisky presahujúce podklad neboli orezané */} +
+ {/* Obal simulujúci veľkosť, zarovnanie a odsadenie */}
- - {/* Dynamické textové vrstvy nad SVG podkladom (Drag & Drop) */} - {activeBadge.layersConfig.map((layer) => { - const isSelected = layer.id === selectedLayerId; - const isDraggingThis = dragState?.layerId === layer.id; - return ( + className="flex" + style={{ + zoom: canvasZoom, + // Obal má na obrazovke stále ~320 px (simulácia zarovnania), pri veľkom priblížení sa rozšíri na celú cenovku + width: `${Math.max(activeBadge.width || 110, 320 / canvasZoom)}px`, + justifyContent: + activeBadge.align === 'left' + ? 'flex-start' + : activeBadge.align === 'right' + ? 'flex-end' + : 'center', + paddingTop: `${activeBadge.paddingTop || 0}px`, + paddingBottom: `${activeBadge.paddingBottom || 0}px` + }} + > +
+ {/* SVG Podklad */}
handleLayerMouseDown(e, layer)} - onTouchStart={(e) => handleLayerTouchStart(e, layer)} - onClick={(e) => { - e.stopPropagation(); - setSelectedLayerId(layer.id); - }} - style={{ - position: 'absolute', - left: `${layer.x}px`, - top: `${layer.y}px`, - transform: 'translate(-50%, -50%)', - fontSize: `${layer.fontSize}px`, - fontWeight: (layer.fontWeight || '700') as any, - color: layer.fill, - fontFamily: layer.fontFamily - ? `'${layer.fontFamily}', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` - : 'Poppins, Arial, sans-serif', - whiteSpace: 'nowrap', - cursor: isDraggingThis ? 'grabbing' : 'grab', - userSelect: 'none' - }} - className={`px-1.5 py-0.5 rounded transition-all select-none ${ - isSelected - ? '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="Ťahajte myšou pre zmenu pozície" - > - {renderLayerVisualContent(layer)} + className="block pointer-events-none w-full" + dangerouslySetInnerHTML={{ __html: activeBadge.svgRaw || '' }} + /> - {isSelected && ( - - {layer.x}px, {layer.y}px - - )} -
- ); - })} + {/* Dynamické textové vrstvy nad SVG podkladom (Drag & Drop) */} + {activeBadge.layersConfig.map((layer) => { + const isSelected = layer.id === selectedLayerId; + const isDraggingThis = dragState?.layerId === layer.id; + return ( +
handleLayerMouseDown(e, layer)} + onTouchStart={(e) => handleLayerTouchStart(e, layer)} + onClick={(e) => { + e.stopPropagation(); + setSelectedLayerId(layer.id); + }} + style={{ + position: 'absolute', + left: `${layer.x}px`, + top: `${layer.y}px`, + transform: 'translate(-50%, -50%)', + fontSize: `${layer.fontSize}px`, + fontWeight: (layer.fontWeight || '700') as any, + color: layer.fill, + fontFamily: layer.fontFamily + ? `'${layer.fontFamily}', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` + : 'Poppins, Arial, sans-serif', + whiteSpace: 'nowrap', + cursor: isDraggingThis ? 'grabbing' : 'grab', + userSelect: 'none' + }} + className={`px-1.5 py-0.5 rounded transition-all select-none ${ + isSelected + ? '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="Ťahajte myšou pre zmenu pozície" + > + {renderLayerVisualContent(layer)} + + {isSelected && ( + + {layer.x}px, {layer.y}px + + )} +
+ ); + })} +
+
- {/* Info o premenných a centoch */} -
- Dostupné dynamické značky: -
- - {'{cena}'} → {currentPriceMain} - {currentPriceSub} (bez €) - - - {'{zlava}'} → {currentDiscount.replace('-', '')} - - - {'{mena}'} → € - +
+ {/* Info o premenných a centoch */} +
+ Dostupné dynamické značky: +
+ + {'{cena}'} → {currentPriceMain} + {currentPriceSub} (bez €) + + + {'{zlava}'} → {currentDiscount.replace('-', '')} + + + {'{mena}'} → € + +
-
+ ) : ( -

Vyberte alebo vytvorte novú šablónu cenovky.

+

Vyberte alebo vytvorte novú šablónu cenovky.

)}