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 <noreply@anthropic.com>
This commit is contained in:
1 parent
61c8c2fb76
commit
48d071d48e
1 file changed
+101
-14
@@ -4,7 +4,12 @@ import React, { useState, useRef, useEffect } from 'react';
|
|||||||
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import { DEFAULT_BADGE_TEMPLATES, renderCleanBadgeSvg, svgToCleanPng } from '@/lib/badgeUtils';
|
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 {
|
interface BadgeEditorModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -71,6 +76,33 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
const [isExportingPng, setIsExportingPng] = useState(false);
|
const [isExportingPng, setIsExportingPng] = useState(false);
|
||||||
const [isSavingDb, setIsSavingDb] = 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<HTMLDivElement>(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
|
// Stav pre priamy Drag & Drop vrstiev na plátne
|
||||||
const [dragState, setDragState] = useState<{
|
const [dragState, setDragState] = useState<{
|
||||||
layerId: string;
|
layerId: string;
|
||||||
@@ -111,8 +143,8 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
if (!dragState) return;
|
if (!dragState) return;
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
const deltaX = e.clientX - dragState.startX;
|
const deltaX = (e.clientX - dragState.startX) / canvasZoom;
|
||||||
const deltaY = e.clientY - dragState.startY;
|
const deltaY = (e.clientY - dragState.startY) / canvasZoom;
|
||||||
const newX = Math.round(dragState.layerStartX + deltaX);
|
const newX = Math.round(dragState.layerStartX + deltaX);
|
||||||
const newY = Math.round(dragState.layerStartY + deltaY);
|
const newY = Math.round(dragState.layerStartY + deltaY);
|
||||||
|
|
||||||
@@ -130,8 +162,8 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
const handleTouchMove = (e: TouchEvent) => {
|
const handleTouchMove = (e: TouchEvent) => {
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
if (!touch) return;
|
if (!touch) return;
|
||||||
const deltaX = touch.clientX - dragState.startX;
|
const deltaX = (touch.clientX - dragState.startX) / canvasZoom;
|
||||||
const deltaY = touch.clientY - dragState.startY;
|
const deltaY = (touch.clientY - dragState.startY) / canvasZoom;
|
||||||
const newX = Math.round(dragState.layerStartX + deltaX);
|
const newX = Math.round(dragState.layerStartX + deltaX);
|
||||||
const newY = Math.round(dragState.layerStartY + deltaY);
|
const newY = Math.round(dragState.layerStartY + deltaY);
|
||||||
|
|
||||||
@@ -161,7 +193,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
window.removeEventListener('touchmove', handleTouchMove);
|
window.removeEventListener('touchmove', handleTouchMove);
|
||||||
window.removeEventListener('touchend', handleDragEnd);
|
window.removeEventListener('touchend', handleDragEnd);
|
||||||
};
|
};
|
||||||
}, [dragState]);
|
}, [dragState, canvasZoom]);
|
||||||
|
|
||||||
// Načítanie písiem pre všetky vrstvy aktívnej cenovky
|
// Načítanie písiem pre všetky vrstvy aktívnej cenovky
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -645,22 +677,73 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2. Stĺpec: Vektorový Canvas & Náhľad s Drag & Drop */}
|
{/* 2. Stĺpec: Vektorový Canvas & Náhľad s Drag & Drop */}
|
||||||
<div className="col-span-5 p-6 flex flex-col items-center justify-center bg-slate-950 relative">
|
<div className="col-span-5 min-h-0 flex flex-col bg-slate-950">
|
||||||
<div className="absolute top-4 left-4 flex items-center gap-2">
|
{/* Lišta plátna: názov + ovládanie priblíženia */}
|
||||||
|
<div className="shrink-0 flex items-center justify-between gap-2 px-4 pt-4 pb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-semibold text-slate-400 flex items-center gap-1.5">
|
<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 na plátne
|
<Eye className="w-3.5 h-3.5" /> Náhľad na plátne
|
||||||
</span>
|
</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">
|
<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 vrstiev
|
<Move className="w-3 h-3" /> Drag & Drop
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{activeBadge && (
|
||||||
|
<div className="flex items-center gap-1 bg-slate-900/90 border border-slate-700 rounded-full px-1.5 py-1 text-slate-300">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={zoomOut}
|
||||||
|
disabled={canvasZoom <= CANVAS_ZOOM_MIN}
|
||||||
|
className="p-1 rounded-full hover:bg-slate-700 hover:text-white disabled:opacity-30 disabled:hover:bg-transparent"
|
||||||
|
title="Oddialiť (Ctrl + koliesko)"
|
||||||
|
>
|
||||||
|
<ZoomOut className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={CANVAS_ZOOM_MIN}
|
||||||
|
max={CANVAS_ZOOM_MAX}
|
||||||
|
step={0.05}
|
||||||
|
value={canvasZoom}
|
||||||
|
onChange={(e) => setCanvasZoom(Number(e.target.value))}
|
||||||
|
className="w-24 h-1 accent-blue-400 cursor-pointer"
|
||||||
|
title="Priblíženie plátna"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={zoomIn}
|
||||||
|
disabled={canvasZoom >= CANVAS_ZOOM_MAX}
|
||||||
|
className="p-1 rounded-full hover:bg-slate-700 hover:text-white disabled:opacity-30 disabled:hover:bg-transparent"
|
||||||
|
title="Priblížiť (Ctrl + koliesko)"
|
||||||
|
>
|
||||||
|
<ZoomIn className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCanvasZoom(1)}
|
||||||
|
className="min-w-[44px] text-[10px] font-mono font-bold hover:text-white"
|
||||||
|
title="Obnoviť na 100 %"
|
||||||
|
>
|
||||||
|
{Math.round(canvasZoom * 100)} %
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{activeBadge ? (
|
{activeBadge ? (
|
||||||
<div className="flex flex-col items-center justify-center w-full">
|
<>
|
||||||
|
{/* Priblížiteľná a posúvateľná oblasť plátna (vyplní zvyšnú výšku stĺpca) */}
|
||||||
|
<div ref={canvasAreaRef} className="flex-1 min-h-0 overflow-auto flex">
|
||||||
|
{/* Rezerva okolo cenovky, aby vrstvy a popisky presahujúce podklad neboli orezané */}
|
||||||
|
<div className="m-auto shrink-0 p-10">
|
||||||
{/* Obal simulujúci veľkosť, zarovnanie a odsadenie */}
|
{/* Obal simulujúci veľkosť, zarovnanie a odsadenie */}
|
||||||
<div
|
<div
|
||||||
className="w-full max-w-xs flex"
|
className="flex"
|
||||||
style={{
|
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:
|
justifyContent:
|
||||||
activeBadge.align === 'left'
|
activeBadge.align === 'left'
|
||||||
? 'flex-start'
|
? 'flex-start'
|
||||||
@@ -672,7 +755,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative select-none transition-all"
|
className="relative select-none"
|
||||||
style={{
|
style={{
|
||||||
width: `${activeBadge.width || 110}px`
|
width: `${activeBadge.width || 110}px`
|
||||||
}}
|
}}
|
||||||
@@ -730,9 +813,12 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="shrink-0 flex justify-center px-4 pb-4 pt-2">
|
||||||
{/* Info o premenných a centoch */}
|
{/* Info o premenných a centoch */}
|
||||||
<div className="mt-6 bg-slate-900/80 border border-slate-800 rounded-xl p-3 text-[11px] text-slate-300 space-y-1.5 max-w-sm">
|
<div className="bg-slate-900/80 border border-slate-800 rounded-xl p-3 text-[11px] text-slate-300 space-y-1.5 max-w-sm">
|
||||||
<span className="font-bold text-gray-400 block mb-0.5">Dostupné dynamické značky:</span>
|
<span className="font-bold text-gray-400 block mb-0.5">Dostupné dynamické značky:</span>
|
||||||
<div className="flex flex-wrap gap-1.5 text-[10px]">
|
<div className="flex flex-wrap gap-1.5 text-[10px]">
|
||||||
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
|
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
|
||||||
@@ -748,8 +834,9 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-slate-500">Vyberte alebo vytvorte novú šablónu cenovky.</p>
|
<p className="m-auto text-sm text-slate-500">Vyberte alebo vytvorte novú šablónu cenovky.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user