'use client'; 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, 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; onClose: () => void; onSelectBadge: (badge: BadgeTemplate, renderedPngUrl?: string) => void; currentDiscount?: string; currentPriceMain?: string; currentPriceSub?: string; currentUnitPrice?: string; } export const BadgeEditorModal: React.FC = ({ isOpen, onClose, onSelectBadge, currentDiscount = '25%', currentPriceMain = '1', currentPriceSub = '99', currentUnitPrice = '1kg = 4,91 €' }) => { 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); // 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; 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) / 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); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleDragEnd); window.removeEventListener('touchmove', handleTouchMove); window.removeEventListener('touchend', handleDragEnd); }; }, [dragState, canvasZoom]); // Načítanie písiem pre všetky vrstvy aktívnej cenovky 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); } } }; if (!isOpen) return null; // Vytvorenie novej šablóny const handleCreateNewTemplate = () => { const newBadge: BadgeTemplate = { id: `badge-${Date.now()}`, name: 'Nová vlastná 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' } ] }; setBadges([newBadge, ...badges]); setActiveBadge(newBadge); setSelectedLayerId(newBadge.layersConfig[0].id); }; // 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; 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(' { const dataUrl = event.target?.result as string; const img = new Image(); img.onload = () => { const w = img.naturalWidth || 140; const h = img.naturalHeight || 95; const svgContent = ``; setActiveBadge({ ...activeBadge, svgRaw: svgContent }); }; img.src = dataUrl; }; reader.readAsDataURL(file); } else { alert('Podporované sú len grafické súbory vo formáte SVG alebo PNG.'); } }; const activeLayer = activeBadge?.layersConfig?.find((l) => l.id === selectedLayerId) || activeBadge?.layersConfig?.[0] || null; const updateActiveLayer = (updates: Partial) => { if (!activeBadge || !activeLayer) return; const updatedLayers = activeBadge.layersConfig.map((l) => l.id === activeLayer.id ? { ...l, ...updates } : l ); setActiveBadge({ ...activeBadge, layersConfig: updatedLayers }); }; const addTextLayer = (presetText: string = 'Text') => { if (!activeBadge) return; const newL: BadgeLayer = { id: `l-${Date.now()}`, type: 'text', text: presetText, x: 70, y: 50, fontSize: 16, fontWeight: '800', fill: '#ffffff', align: 'center' }; setActiveBadge({ ...activeBadge, layersConfig: [...activeBadge.layersConfig, newL] }); setSelectedLayerId(newL.id); }; const removeLayer = (id: string) => { if (!activeBadge || activeBadge.layersConfig.length <= 1) return; setActiveBadge({ ...activeBadge, layersConfig: activeBadge.layersConfig.filter((l) => l.id !== id) }); }; const deleteBadgeTemplate = async (badgeId: string) => { if (confirm('Naozaj chcete zmazať túto šablónu cenovky?')) { // Predvolené šablóny existujú len v prehliadači (localStorage), v databáze nie sú if (!badgeId.startsWith('badge-preset-')) { try { await supabase.from('badges').delete().eq('id', badgeId); } catch (e) {} } const remaining = badges.filter((b) => b.id !== badgeId); setBadges(remaining); setActiveBadge(remaining[0] || null); try { localStorage.setItem('coop_custom_badges', JSON.stringify(remaining)); } catch (e) {} } }; // Uloženie šablóny do Supabase + LocalStorage (garantované uchovanie) const handleSaveToSupabase = async () => { if (!activeBadge) return; setIsSavingDb(true); try { const payload: any = { name: activeBadge.name, svg_raw: activeBadge.svgRaw, layers_config: { layers: activeBadge.layersConfig, width: activeBadge.width || 110, align: activeBadge.align || 'center', paddingTop: activeBadge.paddingTop ?? 4, paddingBottom: activeBadge.paddingBottom ?? 4 } }; let savedBadge: BadgeTemplate | null = null; const isExistingUuid = activeBadge.id && !activeBadge.id.startsWith('badge-'); if (isExistingUuid) { const { data, error } = await supabase .from('badges') .update(payload) .eq('id', activeBadge.id) .select(); if (error) throw error; if (data && data[0]) { savedBadge = { id: data[0].id, name: data[0].name, svgRaw: data[0].svg_raw, layersConfig: activeBadge.layersConfig, width: activeBadge.width, align: activeBadge.align, paddingTop: activeBadge.paddingTop, paddingBottom: activeBadge.paddingBottom }; } } else { const { data, error } = await supabase .from('badges') .insert([payload]) .select(); if (error) throw error; if (data && data[0]) { savedBadge = { id: data[0].id, name: data[0].name, svgRaw: data[0].svg_raw, layersConfig: activeBadge.layersConfig, width: activeBadge.width, align: activeBadge.align, paddingTop: activeBadge.paddingTop, paddingBottom: activeBadge.paddingBottom }; } } const targetToSave = savedBadge || activeBadge; const updatedList = badges.map((b) => (b.id === activeBadge.id ? targetToSave : b)); if (!updatedList.some((b) => b.id === targetToSave.id)) { updatedList.unshift(targetToSave); } setBadges(updatedList); setActiveBadge(targetToSave); try { localStorage.setItem('coop_custom_badges', JSON.stringify(updatedList)); } catch (e) {} alert('Šablóna cenovky bola úspešne uložená a je pripravená na opätovné použitie pre všetky produkty!'); } catch (err: any) { console.warn('Supabase save error, persisting to local cache:', err); try { const updatedList = badges.map((b) => (b.id === activeBadge.id ? activeBadge : b)); localStorage.setItem('coop_custom_badges', JSON.stringify(updatedList)); alert('Šablóna bola úspešne uložená v prehliadači.'); } catch (e) { alert(`Chyba pri ukladaní: ${err.message}`); } } finally { setIsSavingDb(false); } }; // Vykreslenie vrstvy v React náhľade s centami ako horný index (sup) a BEZ znaku € const renderLayerVisualContent = (layer: BadgeLayer) => { const raw = layer.text || ''; const baseFontSize = layer.fontSize || 16; const subFontSize = layer.indexFontSize ? `${layer.indexFontSize}px` : `${Math.max(10, Math.round(baseFontSize * 0.55))}px`; const subY = layer.indexOffsetY !== undefined ? `${layer.indexOffsetY}px` : '-10px'; const subX = layer.indexOffsetX !== undefined ? `${layer.indexOffsetX}px` : '2px'; if (raw.includes('{cena}')) { const parts = raw.split('{cena}'); return ( {parts[0]} {currentPriceMain} {currentPriceSub} {parts[1]} ); } if (raw.includes('{centy}')) { const parts = raw.split('{centy}'); return ( {parts[0]} {currentPriceSub} {parts[1]} ); } const replaced = raw .replace(/{cela_suma}/g, currentPriceMain) .replace(/{zlava}/g, currentDiscount.replace('-', '')) .replace(/{jednotkova_cena}/g, currentUnitPrice) .replace(/{mena}/g, '€'); return replaced; }; // Aplikovanie cenovky na produkt: čisté vektorové SVG a čistý PNG (BEZ akéhokoľvek screenshotu nástrojov/rámčekov) const handleApplyBadge = async () => { if (!activeBadge) return; setIsExportingPng(true); try { const cleanSvg = renderCleanBadgeSvg( activeBadge, { priceMain: currentPriceMain, priceSub: currentPriceSub, discountText: currentDiscount, unitPrice: currentUnitPrice }, activeBadge.width || 110 ); const cleanPng = await svgToCleanPng(cleanSvg, 140, 95); onSelectBadge(activeBadge, cleanPng); onClose(); } catch (err) { console.error('Apply badge error:', err); onSelectBadge(activeBadge); onClose(); } finally { setIsExportingPng(false); } }; return (
{/* Header */}

Editor grafických cenoviek (Opakovane použiteľné šablóny)

Nastavte rozmery, zarovnanie a rozmiestnenie vrstiev. Cena a centy sú dynamické a nezamŕzajú v rastri.

{/* Content Body */}
{/* 1. Stĺpec: Šablóny a Vytvorenie novej */}
Šablóny ({badges.length})
{badges.map((b) => (
{ setActiveBadge(b); if (b.layersConfig?.[0]) setSelectedLayerId(b.layersConfig[0].id); }} className={`w-full text-left p-3 rounded-2xl border transition-all cursor-pointer relative group ${ activeBadge?.id === b.id ? 'bg-white border-[#e2001a] shadow-md ring-2 ring-red-500/10' : 'bg-white/80 border-gray-200 hover:border-gray-300' }`} >

{b.name}

{/* Zmazať sa dá každá šablóna, aj staršie predvolené (badge-preset-*) uložené v prehliadači */}
))}
{/* 2. Stĺpec: Vektorový Canvas & Náhľad s Drag & Drop */}
{/* 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 ? ( <> {/* 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 */}
{/* SVG Podklad */}
{/* 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}'} → €
) : (

Vyberte alebo vytvorte novú šablónu cenovky.

)}
{/* 3. Stĺpec: Konfigurácia rozmerov, zarovnania a vrstiev */}
{activeBadge ? ( <> {/* Názov šablóny */}
setActiveBadge({ ...activeBadge, name: e.target.value })} className="w-full text-xs font-bold border border-gray-300 rounded-xl p-2.5" />
{/* Sekcia: Rozmery, zarovnanie a odsadenie cenovky */}
Rozmery a Zarovnanie cenovky {/* Šírka cenovky */}
{activeBadge.width || 110} px
setActiveBadge({ ...activeBadge, width: Number(e.target.value) })} className="w-full accent-[#e2001a]" />
{/* Zarovnanie na karte produktu */}
{/* Odsadenie (Padding) */}
setActiveBadge({ ...activeBadge, paddingTop: Number(e.target.value) })} className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium" />
setActiveBadge({ ...activeBadge, paddingBottom: Number(e.target.value) })} className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium" />
{/* Upload vlastného SVG alebo PNG podkladu */}

Podporuje SVG vektorové tvary aj priehľadné PNG obrázky.

{/* Rýchle pridanie vrstiev */}
Pridať vrstvu
{/* Zoznam a nastavenie vrstiev */}
Vrstvy ({activeBadge.layersConfig.length})
{activeBadge.layersConfig.map((l, idx) => ( ))}
{activeLayer && (
updateActiveLayer({ text: e.target.value })} className="w-full text-xs font-bold border border-gray-300 rounded-lg p-2 bg-white" />
updateActiveLayer({ fontSize: Number(e.target.value) })} className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white" />
updateActiveLayer({ fill: e.target.value })} className="w-7 h-7 rounded cursor-pointer border-0" /> {activeLayer.fill}
{/* Font výber */}
{(activeLayer.fontSource || 'google') === 'google' && (
{(!POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins')) || activeLayer.fontFamily === 'custom') && ( { const val = e.target.value; loadFontDynamically(val, 'google'); updateActiveLayer({ fontFamily: val, fontSource: 'google' }); }} className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white" /> )}
)} {activeLayer.fontSource === 'adobe' && (
{ const url = e.target.value; if (activeLayer.fontFamily) { loadFontDynamically(activeLayer.fontFamily, 'adobe', url); } updateActiveLayer({ fontUrl: url, fontSource: 'adobe' }); }} className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-mono text-[10px]" /> { const family = e.target.value; if (activeLayer.fontUrl) { loadFontDynamically(family, 'adobe', activeLayer.fontUrl); } updateActiveLayer({ fontFamily: family, fontSource: 'adobe' }); }} className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white" />
)} {activeLayer.fontSource === 'system' && ( )}
updateActiveLayer({ x: Number(e.target.value) })} className="w-full" />
updateActiveLayer({ y: Number(e.target.value) })} className="w-full" />
{/* Nastavenie veľkosti a pozície horného indexu (centov) */} {(activeLayer.text?.includes('{cena}') || activeLayer.text?.includes('{centy}')) && (
Nastavenie horného indexu (Centov) {activeLayer.indexFontSize ?? Math.max(10, Math.round((activeLayer.fontSize || 16) * 0.55))}px
{/* Veľkosť indexu */}
Veľkosť písma indexu {activeLayer.indexFontSize ?? Math.max(10, Math.round((activeLayer.fontSize || 16) * 0.55))} px
updateActiveLayer({ indexFontSize: Number(e.target.value) })} className="w-full accent-blue-600" />
{/* Vertikálny posun (Hore / Dole) */}
Vertikálna pozícia (Posun Hore / Dole) {activeLayer.indexOffsetY ?? -10} px
updateActiveLayer({ indexOffsetY: Number(e.target.value) })} className="w-full accent-blue-600" />
{/* Horizontálna medzera (Vľavo / Vpravo) */}
Horizontálna medzera (Vľavo / Vpravo) {activeLayer.indexOffsetX ?? 2} px
updateActiveLayer({ indexOffsetX: Number(e.target.value) })} className="w-full accent-blue-600" />
)}
)}
{/* Tlačidlo: Uložiť šablónu do DB */}
) : null}
{/* Footer */}
); };