diff --git a/src/components/editor/BadgeEditorModal.tsx b/src/components/editor/BadgeEditorModal.tsx
index f9aa878..dcbbb60 100644
--- a/src/components/editor/BadgeEditorModal.tsx
+++ b/src/components/editor/BadgeEditorModal.tsx
@@ -1,15 +1,42 @@
'use client';
-import React, { useState, useRef, useEffect } from 'react';
+// Editor šablón cenoviek.
+// Plátno ukazuje SKUTOČNÚ cenovku (to isté SVG ako v e-maile); vrstvy sa vyberajú a ťahajú priamo na nej
+// v súradniciach viewBoxu. Klávesnica: šípky posun (Shift = viac), Delete zmaže vrstvu, Ctrl+Z / Ctrl+Y späť / znova,
+// Ctrl+S uloží.
+import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
import { supabase } from '@/lib/supabase';
-import { DEFAULT_BADGE_TEMPLATES, renderCleanBadgeSvg, svgToCleanPng, BADGE_PRICE_TOKEN_RE, badgePriceParts, replaceBadgeTextVars } from '@/lib/badgeUtils';
-import { Tag, Plus, Check, Trash2, X, Upload, Eye, Move, AlignLeft, AlignCenter, AlignRight, ZoomIn, ZoomOut, Copy } from 'lucide-react';
+import { renderCleanBadgeSvg, BadgeProductData } from '@/lib/badgeUtils';
+import {
+ Tag,
+ Plus,
+ Trash2,
+ X,
+ Upload,
+ Copy,
+ Undo2,
+ Redo2,
+ Save,
+ Check,
+ AlignLeft,
+ AlignCenter,
+ AlignRight,
+ ZoomIn,
+ ZoomOut,
+ Crosshair
+} from 'lucide-react';
+
+// ---------------------------------------------------------------------------
+// Pomocné funkcie a konštanty
+// ---------------------------------------------------------------------------
+
+const LOCAL_KEY = 'coop_custom_badges';
+const HISTORY_LIMIT = 60;
// Dočasné ID šablóny pred uložením do databázy (prefix badge- = ešte nie je v Supabase)
-function newLocalBadgeId(): string {
- return `badge-${Date.now()}`;
-}
+const newLocalBadgeId = () => `badge-${Date.now()}`;
+const newLayerId = () => `l-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
// Riadok tabuľky badges v Supabase pre danú šablónu
function toBadgeDbPayload(badge: BadgeTemplate) {
@@ -26,10 +53,163 @@ function toBadgeDbPayload(badge: BadgeTemplate) {
};
}
-// 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 BadgeRow {
+ id: string;
+ name: string;
+ svg_raw: string | null;
+ layers_config: BadgeLayer[] | { layers?: BadgeLayer[]; width?: number; align?: BadgeTemplate['align']; paddingTop?: number; paddingBottom?: number } | null;
+}
+
+function fromBadgeRow(d: BadgeRow): BadgeTemplate {
+ const cfg = d.layers_config && !Array.isArray(d.layers_config) ? d.layers_config : null;
+ return {
+ id: d.id,
+ name: d.name,
+ svgRaw: d.svg_raw || '',
+ layersConfig: cfg ? cfg.layers || [] : (d.layers_config as BadgeLayer[]) || [],
+ width: cfg?.width || 110,
+ align: cfg?.align || 'center',
+ paddingTop: cfg?.paddingTop ?? 4,
+ paddingBottom: cfg?.paddingBottom ?? 4
+ };
+}
+
+function saveLocal(list: BadgeTemplate[]) {
+ try {
+ localStorage.setItem(LOCAL_KEY, JSON.stringify(list));
+ } catch {
+ // localStorage nemusí byť dostupný
+ }
+}
+
+// viewBox šablóny (rovnako ako renderCleanBadgeSvg)
+function viewBoxOf(svgRaw?: string): { x: number; y: number; w: number; h: number } {
+ const vb = svgRaw?.match(/viewBox=["']([^"']+)["']/i)?.[1].trim().split(/[\s,]+/).map(Number);
+ if (vb && vb.length === 4 && vb[2] > 0 && vb[3] > 0) return { x: vb[0], y: vb[1], w: vb[2], h: vb[3] };
+ const w = parseFloat(svgRaw?.match(/width=["']([^"']+)["']/i)?.[1] || '');
+ const h = parseFloat(svgRaw?.match(/height=["']([^"']+)["']/i)?.[1] || '');
+ return w > 0 && h > 0 ? { x: 0, y: 0, w, h } : { x: 0, y: 0, w: 140, h: 95 };
+}
+
+const round1 = (n: number) => Math.round(n * 10) / 10;
+
+const FONTS: Array<{ name: string; source: 'google' | 'system' }> = [
+ { name: 'Poppins', source: 'google' },
+ { name: 'Montserrat', source: 'google' },
+ { name: 'Oswald', source: 'google' },
+ { name: 'Bebas Neue', source: 'google' },
+ { name: 'Anton', source: 'google' },
+ { name: 'Roboto Condensed', source: 'google' },
+ { name: 'Barlow Semi Condensed', source: 'google' },
+ { name: 'Barlow Condensed', source: 'google' },
+ { name: 'Inter', source: 'google' },
+ { name: 'Arial Narrow', source: 'system' },
+ { name: 'Arial', source: 'system' },
+ { name: 'Impact', source: 'system' }
+];
+
+const COLORS = ['#ffffff', '#000000', '#e2001a', '#ffde00', '#ffd100', '#003399', '#001a33', '#16a34a'];
+
+const WEIGHTS = [
+ ['400', 'Normal'],
+ ['600', 'SemiBold'],
+ ['700', 'Bold'],
+ ['800', 'ExtraBold'],
+ ['900', 'Black']
+];
+
+// Premenné v texte vrstvy
+const TOKENS: Array<{ token: string; label: string }> = [
+ { token: '{cena}', label: 'Cena' },
+ { token: '{zlava}', label: 'Zľava' },
+ { token: '{cena_s_kartou}', label: 'Cena s kartou' },
+ { token: '{zlava_s_kartou}', label: 'Zľava s kartou' },
+ { token: '{jednotkova_cena}', label: 'Jedn. cena' },
+ { token: '{mena}', label: '€' }
+];
+
+// Rýchle pridanie vrstiev
+const QUICK_LAYERS: Array<{ label: string; text: string; fontSize: number }> = [
+ { label: 'Cena', text: '{cena}', fontSize: 40 },
+ { label: 'Zľava', text: '-{zlava}', fontSize: 14 },
+ { label: 'Cena s kartou', text: '{cena_s_kartou}', fontSize: 40 },
+ { label: 'Zľava s kartou', text: '-{zlava_s_kartou}', fontSize: 14 },
+ { label: 'Text', text: 'AKCIA', fontSize: 16 }
+];
+
+function loadFont(layer: BadgeLayer) {
+ if (typeof document === 'undefined' || !layer.fontFamily) return;
+ if (layer.fontSource === 'adobe' && layer.fontUrl) {
+ if (!document.querySelector(`link[data-adobe-font="${layer.fontUrl}"]`)) {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = layer.fontUrl;
+ link.setAttribute('data-adobe-font', layer.fontUrl);
+ document.head.appendChild(link);
+ }
+ return;
+ }
+ if (layer.fontSource === 'system') return;
+ const clean = layer.fontFamily.trim().replace(/\s+/g, '+');
+ if (!document.querySelector(`link[data-google-font="${clean}"]`)) {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = `https://fonts.googleapis.com/css2?family=${clean}:wght@400;600;700;800;900&display=swap`;
+ link.setAttribute('data-google-font', clean);
+ document.head.appendChild(link);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Malé UI prvky
+// ---------------------------------------------------------------------------
+
+const Section: React.FC<{ title: string; action?: React.ReactNode; children: React.ReactNode }> = ({ title, action, children }) => (
+
+
+ {title}
+ {action}
+
+ {children}
+
+);
+
+const NumberSlider: React.FC<{
+ label: string;
+ value: number;
+ min: number;
+ max: number;
+ step?: number;
+ suffix?: string;
+ onChange: (v: number) => void;
+}> = ({ label, value, min, max, step = 1, suffix = '', onChange }) => (
+
+);
+
+// ---------------------------------------------------------------------------
+// Editor
+// ---------------------------------------------------------------------------
interface BadgeEditorModalProps {
isOpen: boolean;
@@ -44,6 +224,14 @@ interface BadgeEditorModalProps {
currentCardDiscount?: string;
}
+interface DragState {
+ layerId: string;
+ startX: number;
+ startY: number;
+ layerX: number;
+ layerY: number;
+}
+
export const BadgeEditorModal: React.FC = ({
isOpen,
onClose,
@@ -56,1370 +244,706 @@ export const BadgeEditorModal: React.FC = ({
currentCardPriceSub = '49',
currentCardDiscount = '30%'
}) => {
- const POPULAR_FONTS = [
- { name: 'Poppins', source: 'google' as const },
- { name: 'Montserrat', source: 'google' as const },
- { name: 'Oswald', source: 'google' as const },
- { name: 'Bebas Neue', source: 'google' as const },
- { name: 'Anton', source: 'google' as const },
- { name: 'Roboto Condensed', source: 'google' as const },
- { name: 'Barlow Semi Condensed', source: 'google' as const },
- { name: 'Inter', source: 'google' as const },
- { name: 'Open Sans', source: 'google' as const },
- { name: 'Arial Narrow', source: 'system' as const },
- { name: 'Impact', source: 'system' as const }
- ];
-
- const loadFontDynamically = (fontFamily: string, source: 'system' | 'google' | 'adobe' = 'google', url?: string) => {
- if (typeof document === 'undefined') return;
-
- if (source === 'adobe' && url) {
- const existing = document.querySelector(`link[data-adobe-font="${url}"]`);
- if (!existing) {
- const link = document.createElement('link');
- link.rel = 'stylesheet';
- link.href = url;
- link.setAttribute('data-adobe-font', url);
- document.head.appendChild(link);
- }
- } else if (source === 'google' && fontFamily) {
- const clean = fontFamily.trim().replace(/\s+/g, '+');
- const href = `https://fonts.googleapis.com/css2?family=${clean}:wght@400;600;700;800;900&display=swap`;
- const existing = document.querySelector(`link[data-google-font="${clean}"]`);
- if (!existing) {
- const link = document.createElement('link');
- link.rel = 'stylesheet';
- link.href = href;
- link.setAttribute('data-google-font', clean);
- document.head.appendChild(link);
- }
- }
- };
-
const [badges, setBadges] = useState([]);
- const [activeBadge, setActiveBadge] = useState(null);
- const [selectedLayerId, setSelectedLayerId] = useState(null);
- const [isExportingPng, setIsExportingPng] = useState(false);
- const [isSavingDb, setIsSavingDb] = useState(false);
+ const [badge, setBadge] = useState(null);
+ const [savedJson, setSavedJson] = useState('');
+ const [selectedId, setSelectedId] = useState(null);
+ const [history, setHistory] = useState<{ past: BadgeTemplate[]; future: BadgeTemplate[] }>({ past: [], future: [] });
+ const [zoom, setZoom] = useState(1);
+ const [saving, setSaving] = useState(false);
+ const [savedFlash, setSavedFlash] = useState(false);
+ const [testPrice, setTestPrice] = useState<{ main: string; sub: string } | null>(null);
+ const [boxes, setBoxes] = useState>({});
+ const [drag, setDrag] = useState(null);
+ const [snapX, setSnapX] = useState(false);
+ const canvasRef = useRef(null);
+ const fileRef = useRef(null);
- // 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 isDirty = !!badge && JSON.stringify(badge) !== savedJson;
+ const layer = badge?.layersConfig.find((l) => l.id === selectedId) || null;
+ const vb = useMemo(() => viewBoxOf(badge?.svgRaw), [badge?.svgRaw]);
+ const canvasWidth = Math.round(300 * zoom);
+ const unitsPerPx = vb.w / canvasWidth;
- 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;
- 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 product: BadgeProductData = {
+ priceMain: testPrice?.main || currentPriceMain,
+ priceSub: testPrice?.sub || currentPriceSub,
+ discountText: currentDiscount,
+ unitPrice: currentUnitPrice,
+ cardPriceMain: testPrice?.main || currentCardPriceMain,
+ cardPriceSub: testPrice?.sub || currentCardPriceSub,
+ cardDiscount: currentCardDiscount
};
- 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
- });
- };
+ // --- načítanie šablón -----------------------------------------------------
+ const open = useCallback((b: BadgeTemplate | null) => {
+ setBadge(b);
+ setSavedJson(b ? JSON.stringify(b) : '');
+ setSelectedId(b?.layersConfig[0]?.id ?? null);
+ setHistory({ past: [], future: [] });
+ }, []);
useEffect(() => {
- if (!dragState) return;
-
- const handleMouseMove = (e: MouseEvent) => {
- 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);
-
- 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) / canvasZoom;
- const deltaY = (touch.clientY - dragState.startY) / canvasZoom;
- 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);
-
+ if (!isOpen) return;
+ let cancelled = false;
+ (async () => {
+ let loaded: BadgeTemplate[] = [];
+ try {
+ const { data } = await supabase.from('badges').select('*').order('created_at', { ascending: false });
+ loaded = ((data as BadgeRow[]) || []).map(fromBadgeRow);
+ } catch {
+ // bez databázy použijeme kópiu v prehliadači
+ }
+ try {
+ const local = JSON.parse(localStorage.getItem(LOCAL_KEY) || '[]');
+ if (Array.isArray(local)) {
+ const ids = new Set(loaded.map((b) => b.id));
+ for (const b of local as BadgeTemplate[]) if (!ids.has(b.id)) loaded.push(b);
+ }
+ } catch {
+ // localStorage nemusí byť dostupný
+ }
+ if (cancelled) return;
+ setBadges(loaded);
+ open(loaded[0] || null);
+ })();
return () => {
- window.removeEventListener('mousemove', handleMouseMove);
- window.removeEventListener('mouseup', handleDragEnd);
- window.removeEventListener('touchmove', handleTouchMove);
- window.removeEventListener('touchend', handleDragEnd);
+ cancelled = true;
};
- }, [dragState, canvasZoom]);
+ }, [isOpen, open]);
- // Načítanie písiem pre všetky vrstvy aktívnej cenovky
+ // Písma vrstiev
useEffect(() => {
- if (activeBadge?.layersConfig) {
- activeBadge.layersConfig.forEach((l) => {
- if (l.fontFamily) {
- loadFontDynamically(l.fontFamily, l.fontSource || 'google', l.fontUrl);
- }
- });
- }
- }, [activeBadge]);
-
- // Načítanie existujúcich šablón zo Supabase + LocalStorage + Default Presets
- useEffect(() => {
- if (isOpen) {
- loadBadgesFromSupabase();
- }
- }, [isOpen]);
-
- const loadBadgesFromSupabase = async () => {
- let loaded: BadgeTemplate[] = [];
-
- try {
- const { data, error } = await supabase
- .from('badges')
- .select('*')
- .order('created_at', { ascending: false });
-
- if (data && !error && data.length > 0) {
- loaded = data.map((d: any) => {
- const isObj = d.layers_config && !Array.isArray(d.layers_config);
- return {
- id: d.id,
- name: d.name,
- svgRaw: d.svg_raw || '',
- layersConfig: isObj ? (d.layers_config.layers || []) : (d.layers_config || []),
- width: isObj ? (d.layers_config.width || 110) : 110,
- align: isObj ? (d.layers_config.align || 'center') : 'center',
- paddingTop: isObj ? (d.layers_config.paddingTop ?? 4) : 4,
- paddingBottom: isObj ? (d.layers_config.paddingBottom ?? 4) : 4
- };
- });
- }
- } catch (err) {
- console.warn('Error loading badges from Supabase, checking local storage:', err);
- }
-
- // Doplnenie z localStorage
- try {
- const localStr = localStorage.getItem('coop_custom_badges');
- if (localStr) {
- const localBadges = JSON.parse(localStr);
- if (Array.isArray(localBadges)) {
- const existingIds = new Set(loaded.map((b) => b.id));
- for (const b of localBadges) {
- if (!existingIds.has(b.id)) {
- loaded.push(b);
- existingIds.add(b.id);
- }
- }
- }
- }
- } catch (e) {}
-
- setBadges(loaded);
- if (!activeBadge && loaded.length > 0) {
- setActiveBadge(loaded[0]);
- if (loaded[0].layersConfig?.[0]) {
- setSelectedLayerId(loaded[0].layersConfig[0].id);
- }
+ badge?.layersConfig.forEach(loadFont);
+ }, [badge?.layersConfig]);
+
+ // --- úpravy s históriou ---------------------------------------------------
+ // record = zapísať predchádzajúci stav do histórie (počas ťahania sa zapisuje len na začiatku)
+ const commit = (next: BadgeTemplate, record = true) => {
+ if (record && badge) {
+ const prev = badge;
+ setHistory((h) => ({ past: [...h.past, prev].slice(-HISTORY_LIMIT), future: [] }));
}
+ setBadge(next);
};
- if (!isOpen) return null;
+ const updateLayer = (id: string, patch: Partial, record = true) => {
+ if (!badge) return;
+ commit({ ...badge, layersConfig: badge.layersConfig.map((l) => (l.id === id ? { ...l, ...patch } : l)) }, record);
+ };
- // Vytvorenie novej šablóny
- const handleCreateNewTemplate = () => {
- const newBadge: BadgeTemplate = {
- id: `badge-${Date.now()}`,
- name: 'Nová vlastná cenovka',
- svgRaw: ``,
+ const undo = useCallback(() => {
+ setHistory((h) => {
+ if (!h.past.length || !badge) return h;
+ const prev = h.past[h.past.length - 1];
+ setBadge(prev);
+ return { past: h.past.slice(0, -1), future: [badge, ...h.future] };
+ });
+ }, [badge]);
+
+ const redo = useCallback(() => {
+ setHistory((h) => {
+ if (!h.future.length || !badge) return h;
+ const next = h.future[0];
+ setBadge(next);
+ return { past: [...h.past, badge], future: h.future.slice(1) };
+ });
+ }, [badge]);
+
+ const addLayer = (text: string, fontSize: number) => {
+ if (!badge) return;
+ const l: BadgeLayer = {
+ id: newLayerId(),
+ type: 'text',
+ text,
+ x: round1(vb.x + vb.w / 2),
+ y: round1(vb.y + vb.h / 2),
+ fontSize,
+ fontWeight: '900',
+ fill: '#ffffff',
+ align: 'center'
+ };
+ commit({ ...badge, layersConfig: [...badge.layersConfig, l] });
+ setSelectedId(l.id);
+ };
+
+ const removeLayer = (id: string) => {
+ if (!badge) return;
+ commit({ ...badge, layersConfig: badge.layersConfig.filter((l) => l.id !== id) });
+ if (selectedId === id) setSelectedId(null);
+ };
+
+ const insertToken = (token: string) => {
+ if (!layer) return;
+ updateLayer(layer.id, { text: `${layer.text || ''}${token}` });
+ };
+
+ // --- meranie vrstiev na plátne (bounding box v súradniciach viewBoxu) -----
+ const svgHtml = badge ? renderCleanBadgeSvg(badge, product, canvasWidth) : '';
+ useLayoutEffect(() => {
+ const root = canvasRef.current;
+ if (!root || !badge) return;
+ const measure = () => {
+ const next: Record = {};
+ root.querySelectorAll('[data-layer-index]').forEach((t) => {
+ const l = badge.layersConfig[Number(t.dataset.layerIndex)];
+ if (!l) return;
+ try {
+ const b = t.getBBox();
+ next[l.id] = { x: b.x, y: b.y, w: b.width, h: b.height };
+ } catch {
+ // prvok ešte nie je vykreslený
+ }
+ });
+ setBoxes(next);
+ };
+ measure();
+ document.fonts?.ready.then(measure).catch(() => undefined);
+ }, [svgHtml, badge]);
+
+ // --- ťahanie ----------------------------------------------------------------
+ const onLayerPointerDown = (e: React.PointerEvent, l: BadgeLayer) => {
+ e.stopPropagation();
+ (e.currentTarget as Element).setPointerCapture(e.pointerId);
+ setSelectedId(l.id);
+ if (badge) setHistory((h) => ({ past: [...h.past, badge].slice(-HISTORY_LIMIT), future: [] }));
+ setDrag({ layerId: l.id, startX: e.clientX, startY: e.clientY, layerX: l.x, layerY: l.y });
+ };
+
+ const onPointerMove = (e: React.PointerEvent) => {
+ if (!drag || !badge) return;
+ let x = drag.layerX + (e.clientX - drag.startX) * unitsPerPx;
+ const y = drag.layerY + (e.clientY - drag.startY) * unitsPerPx;
+ const center = vb.x + vb.w / 2;
+ const snapped = Math.abs(x - center) < vb.w * 0.02;
+ if (snapped) x = center;
+ setSnapX(snapped);
+ updateLayer(drag.layerId, { x: round1(x), y: round1(y) }, false);
+ };
+
+ const endDrag = () => {
+ setDrag(null);
+ setSnapX(false);
+ };
+
+ // --- uloženie ---------------------------------------------------------------
+ const save = useCallback(async (): Promise => {
+ if (!badge) return null;
+ setSaving(true);
+ let saved: BadgeTemplate = badge;
+ try {
+ const payload = toBadgeDbPayload(badge);
+ const isDbRow = !badge.id.startsWith('badge-');
+ const { data, error } = isDbRow
+ ? await supabase.from('badges').update(payload).eq('id', badge.id).select()
+ : await supabase.from('badges').insert([payload]).select();
+ if (error) throw error;
+ if (data?.[0]) saved = { ...badge, id: (data[0] as BadgeRow).id };
+ } catch (err) {
+ console.warn('Šablóna sa uložila len v prehliadači:', err);
+ }
+ const list = badges.some((b) => b.id === badge.id)
+ ? badges.map((b) => (b.id === badge.id ? saved : b))
+ : [saved, ...badges];
+ setBadges(list);
+ saveLocal(list);
+ setBadge(saved);
+ setSavedJson(JSON.stringify(saved));
+ setSaving(false);
+ setSavedFlash(true);
+ setTimeout(() => setSavedFlash(false), 1500);
+ return saved;
+ }, [badge, badges]);
+
+ const confirmDiscard = () => !isDirty || confirm('Máte neuložené zmeny v šablóne. Zahodiť ich?');
+
+ const close = () => {
+ if (confirmDiscard()) onClose();
+ };
+
+ const selectTemplate = (b: BadgeTemplate) => {
+ if (b.id === badge?.id || !confirmDiscard()) return;
+ open(b);
+ };
+
+ const createTemplate = () => {
+ if (!confirmDiscard()) return;
+ const b: BadgeTemplate = {
+ id: newLocalBadgeId(),
+ name: 'Nová cenovka',
+ svgRaw: '',
width: 110,
align: 'center',
paddingTop: 4,
paddingBottom: 4,
layersConfig: [
- {
- id: `l-${Date.now()}-1`,
- type: 'text',
- text: 'AKCIA',
- x: 70,
- y: 24,
- fontSize: 16,
- fontWeight: '900',
- fill: '#e2001a',
- align: 'center'
- },
- {
- id: `l-${Date.now()}-2`,
- type: 'text',
- text: '{cena}',
- x: 70,
- y: 62,
- fontSize: 42,
- fontWeight: '900',
- fill: '#001a33',
- align: 'center'
- }
+ { id: newLayerId(), type: 'text', text: 'AKCIA', x: 70, y: 22, fontSize: 16, fontWeight: '900', fill: '#ffde00', align: 'center' },
+ { id: newLayerId(), type: 'text', text: '{cena}', x: 70, y: 60, fontSize: 42, fontWeight: '900', fill: '#ffffff', align: 'center' }
]
};
-
- setBadges([newBadge, ...badges]);
- setActiveBadge(newBadge);
- setSelectedLayerId(newBadge.layersConfig[0].id);
+ setBadges([b, ...badges]);
+ open(b);
+ setSavedJson(''); // nová šablóna je neuložená
};
- // Upload vlastného SVG alebo PNG súboru ako podkladu
- const handleBackgroundUpload = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file || !activeBadge) return;
+ const duplicateTemplate = async (b: BadgeTemplate) => {
+ if (!confirmDiscard()) return;
+ const copy: BadgeTemplate = { ...JSON.parse(JSON.stringify(b)), id: newLocalBadgeId(), name: `${b.name} (kópia)` };
+ let saved = copy;
+ try {
+ const { data, error } = await supabase.from('badges').insert([toBadgeDbPayload(copy)]).select();
+ if (!error && data?.[0]) saved = { ...copy, id: (data[0] as BadgeRow).id };
+ } catch {
+ // bez databázy ostane kópia v prehliadači
+ }
+ const list = [...badges];
+ list.splice(list.findIndex((x) => x.id === b.id) + 1, 0, saved);
+ setBadges(list);
+ saveLocal(list);
+ open(saved);
+ };
- if (file.type.includes('svg') || file.name.toLowerCase().endsWith('.svg')) {
- const reader = new FileReader();
- reader.onload = (event) => {
- const content = event.target?.result as string;
- if (content.includes('