Ikona Duplikovat pri sablone vytvori kopiu "(kopia)", hned ju ulozi a otvori na upravu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1398 lines
60 KiB
TypeScript
1398 lines
60 KiB
TypeScript
'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, Copy } from 'lucide-react';
|
||
|
||
// Riadok tabuľky badges v Supabase pre danú šablónu
|
||
function toBadgeDbPayload(badge: BadgeTemplate) {
|
||
return {
|
||
name: badge.name,
|
||
svg_raw: badge.svgRaw,
|
||
layers_config: {
|
||
layers: badge.layersConfig,
|
||
width: badge.width || 110,
|
||
align: badge.align || 'center',
|
||
paddingTop: badge.paddingTop ?? 4,
|
||
paddingBottom: badge.paddingBottom ?? 4
|
||
}
|
||
};
|
||
}
|
||
|
||
// 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<BadgeEditorModalProps> = ({
|
||
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<BadgeTemplate[]>([]);
|
||
const [activeBadge, setActiveBadge] = useState<BadgeTemplate | null>(null);
|
||
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(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<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
|
||
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: `<svg width="140" height="95" viewBox="0 0 140 95" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<rect width="140" height="95" rx="14" fill="#ffd100"/>
|
||
</svg>`,
|
||
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<HTMLInputElement>) => {
|
||
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('<svg')) {
|
||
setActiveBadge({
|
||
...activeBadge,
|
||
svgRaw: content
|
||
});
|
||
} else {
|
||
alert('Nahraný súbor nie je platný formát SVG.');
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
} else if (
|
||
file.type.includes('png') ||
|
||
file.type.includes('image/') ||
|
||
file.name.toLowerCase().endsWith('.png') ||
|
||
file.name.toLowerCase().endsWith('.jpg') ||
|
||
file.name.toLowerCase().endsWith('.jpeg') ||
|
||
file.name.toLowerCase().endsWith('.webp')
|
||
) {
|
||
const reader = new FileReader();
|
||
reader.onload = (event) => {
|
||
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 = `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><image href="${dataUrl}" xlink:href="${dataUrl}" width="${w}" height="${h}" preserveAspectRatio="xMidYMid meet"/></svg>`;
|
||
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<BadgeLayer>) => {
|
||
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)
|
||
});
|
||
};
|
||
|
||
// Duplikovanie šablóny: kópia sa hneď uloží (Supabase, inak localStorage) a otvorí na úpravu
|
||
const duplicateBadgeTemplate = async (badge: BadgeTemplate) => {
|
||
const copy: BadgeTemplate = {
|
||
...JSON.parse(JSON.stringify(badge)),
|
||
id: `badge-${Date.now()}`,
|
||
name: `${badge.name} (kópia)`
|
||
};
|
||
let saved = copy;
|
||
try {
|
||
const { data, error } = await supabase
|
||
.from('badges')
|
||
.insert([toBadgeDbPayload(copy)])
|
||
.select();
|
||
if (!error && data && data[0]) {
|
||
saved = { ...copy, id: data[0].id };
|
||
}
|
||
} catch (e) {}
|
||
|
||
const list = [...badges];
|
||
const idx = list.findIndex((b) => b.id === badge.id);
|
||
list.splice(idx + 1, 0, saved);
|
||
setBadges(list);
|
||
setActiveBadge(saved);
|
||
setSelectedLayerId(saved.layersConfig?.[0]?.id ?? null);
|
||
try {
|
||
localStorage.setItem('coop_custom_badges', JSON.stringify(list));
|
||
} catch (e) {}
|
||
};
|
||
|
||
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 = toBadgeDbPayload(activeBadge);
|
||
|
||
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 (
|
||
<span>
|
||
{parts[0]}
|
||
<span>{currentPriceMain}</span>
|
||
<span
|
||
style={{
|
||
display: 'inline-block',
|
||
fontSize: subFontSize,
|
||
transform: `translate(${subX}, ${subY})`,
|
||
fontWeight: layer.fontWeight || '700',
|
||
lineHeight: 1
|
||
}}
|
||
>
|
||
{currentPriceSub}
|
||
</span>
|
||
{parts[1]}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
if (raw.includes('{centy}')) {
|
||
const parts = raw.split('{centy}');
|
||
return (
|
||
<span>
|
||
{parts[0]}
|
||
<span
|
||
style={{
|
||
display: 'inline-block',
|
||
fontSize: subFontSize,
|
||
transform: `translate(${subX}, ${subY})`,
|
||
fontWeight: layer.fontWeight || '700',
|
||
lineHeight: 1
|
||
}}
|
||
>
|
||
{currentPriceSub}
|
||
</span>
|
||
{parts[1]}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4">
|
||
<div className="bg-white rounded-3xl shadow-2xl max-w-6xl w-full max-h-[94vh] overflow-hidden flex flex-col border border-gray-100">
|
||
{/* Header */}
|
||
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-slate-50">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-9 h-9 rounded-xl bg-[#e2001a] flex items-center justify-center text-white shadow-sm">
|
||
<Tag className="w-5 h-5" />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-base font-bold text-gray-900">
|
||
Editor grafických cenoviek (Opakovane použiteľné šablóny)
|
||
</h2>
|
||
<p className="text-xs text-gray-500">
|
||
Nastavte rozmery, zarovnanie a rozmiestnenie vrstiev. Cena a centy sú dynamické a nezamŕzajú v rastri.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button onClick={onClose} className="p-2 text-gray-400 hover:text-gray-700 rounded-xl hover:bg-gray-200/50">
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Content Body */}
|
||
<div className="flex-1 overflow-hidden grid grid-cols-12 divide-x divide-gray-100">
|
||
{/* 1. Stĺpec: Šablóny a Vytvorenie novej */}
|
||
<div className="col-span-3 p-4 overflow-y-auto space-y-3 bg-gray-50/50">
|
||
<div className="flex items-center justify-between px-1">
|
||
<span className="text-[11px] font-bold uppercase tracking-wider text-gray-400">
|
||
Šablóny ({badges.length})
|
||
</span>
|
||
<button
|
||
onClick={handleCreateNewTemplate}
|
||
className="text-xs font-bold text-[#e2001a] hover:text-red-700 flex items-center gap-1 bg-red-50 hover:bg-red-100 px-2 py-1 rounded-lg transition-colors"
|
||
>
|
||
<Plus className="w-3.5 h-3.5" /> Nová
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
{badges.map((b) => (
|
||
<div
|
||
key={b.id}
|
||
onClick={() => {
|
||
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'
|
||
}`}
|
||
>
|
||
<div className="flex items-center justify-between mb-1">
|
||
<p className="text-xs font-bold text-gray-800 truncate">{b.name}</p>
|
||
<div className="flex items-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
duplicateBadgeTemplate(b);
|
||
}}
|
||
className="text-gray-300 hover:text-blue-600 p-1 rounded transition-colors"
|
||
title="Duplikovať šablónu"
|
||
>
|
||
<Copy className="w-3.5 h-3.5" />
|
||
</button>
|
||
{/* Zmazať sa dá každá šablóna, aj staršie predvolené (badge-preset-*) uložené v prehliadači */}
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
deleteBadgeTemplate(b.id);
|
||
}}
|
||
className="text-gray-300 hover:text-red-600 p-1 rounded transition-colors"
|
||
title="Zmazať šablónu"
|
||
>
|
||
<Trash2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div
|
||
className="w-full h-12 flex items-center justify-center transform scale-75 origin-center overflow-hidden pointer-events-none"
|
||
dangerouslySetInnerHTML={{
|
||
__html: renderCleanBadgeSvg(
|
||
b,
|
||
{
|
||
priceMain: currentPriceMain,
|
||
priceSub: currentPriceSub,
|
||
discountText: currentDiscount,
|
||
unitPrice: currentUnitPrice
|
||
},
|
||
90
|
||
)
|
||
}}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 2. Stĺpec: Vektorový Canvas & Náhľad s Drag & Drop */}
|
||
<div className="col-span-5 min-h-0 flex flex-col bg-slate-950">
|
||
{/* 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">
|
||
<Eye className="w-3.5 h-3.5" /> Náhľad na plátne
|
||
</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
|
||
</span>
|
||
</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 ? (
|
||
<>
|
||
{/* 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 */}
|
||
<div
|
||
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`
|
||
}}
|
||
>
|
||
<div
|
||
className="relative select-none"
|
||
style={{
|
||
width: `${activeBadge.width || 110}px`
|
||
}}
|
||
>
|
||
{/* SVG Podklad */}
|
||
<div
|
||
className="block pointer-events-none w-full"
|
||
dangerouslySetInnerHTML={{ __html: activeBadge.svgRaw || '' }}
|
||
/>
|
||
|
||
{/* 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}
|
||
onMouseDown={(e) => 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 && (
|
||
<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>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="shrink-0 flex justify-center px-4 pb-4 pt-2">
|
||
{/* Info o premenných a centoch */}
|
||
<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>
|
||
<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">
|
||
{'{cena}'} → {currentPriceMain}
|
||
<sup>{currentPriceSub}</sup> (bez €)
|
||
</span>
|
||
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-emerald-300">
|
||
{'{zlava}'} → {currentDiscount.replace('-', '')}
|
||
</span>
|
||
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-blue-300">
|
||
{'{mena}'} → €
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<p className="m-auto text-sm text-slate-500">Vyberte alebo vytvorte novú šablónu cenovky.</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 3. Stĺpec: Konfigurácia rozmerov, zarovnania a vrstiev */}
|
||
<div className="col-span-4 p-5 overflow-y-auto space-y-5 bg-white">
|
||
{activeBadge ? (
|
||
<>
|
||
{/* Názov šablóny */}
|
||
<div>
|
||
<label className="block text-xs font-semibold text-gray-700 mb-1">Názov šablóny</label>
|
||
<input
|
||
type="text"
|
||
value={activeBadge.name}
|
||
onChange={(e) => setActiveBadge({ ...activeBadge, name: e.target.value })}
|
||
className="w-full text-xs font-bold border border-gray-300 rounded-xl p-2.5"
|
||
/>
|
||
</div>
|
||
|
||
{/* Sekcia: Rozmery, zarovnanie a odsadenie cenovky */}
|
||
<div className="bg-gray-50 p-3.5 rounded-2xl border border-gray-200 space-y-3">
|
||
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block">
|
||
Rozmery a Zarovnanie cenovky
|
||
</span>
|
||
|
||
{/* Šírka cenovky */}
|
||
<div>
|
||
<div className="flex justify-between items-center mb-1">
|
||
<label className="text-[11px] font-semibold text-gray-700">Šírka cenovky</label>
|
||
<span className="text-[11px] font-bold text-gray-900 bg-white px-2 py-0.5 rounded border border-gray-200">
|
||
{activeBadge.width || 110} px
|
||
</span>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min="70"
|
||
max="180"
|
||
step="2"
|
||
value={activeBadge.width || 110}
|
||
onChange={(e) => setActiveBadge({ ...activeBadge, width: Number(e.target.value) })}
|
||
className="w-full accent-[#e2001a]"
|
||
/>
|
||
</div>
|
||
|
||
{/* Zarovnanie na karte produktu */}
|
||
<div>
|
||
<label className="block text-[11px] font-semibold text-gray-700 mb-1">
|
||
Zarovnanie cenovky na produkte
|
||
</label>
|
||
<div className="grid grid-cols-3 gap-1 bg-gray-200 p-0.5 rounded-lg text-xs font-bold">
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveBadge({ ...activeBadge, align: 'left' })}
|
||
className={`py-1.5 flex items-center justify-center gap-1 rounded transition-all ${
|
||
(activeBadge.align || 'center') === 'left'
|
||
? 'bg-white text-gray-900 shadow-sm'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
<AlignLeft className="w-3.5 h-3.5" /> Vľavo
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveBadge({ ...activeBadge, align: 'center' })}
|
||
className={`py-1.5 flex items-center justify-center gap-1 rounded transition-all ${
|
||
(activeBadge.align || 'center') === 'center'
|
||
? 'bg-white text-gray-900 shadow-sm'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
<AlignCenter className="w-3.5 h-3.5" /> Na stred
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveBadge({ ...activeBadge, align: 'right' })}
|
||
className={`py-1.5 flex items-center justify-center gap-1 rounded transition-all ${
|
||
(activeBadge.align || 'center') === 'right'
|
||
? 'bg-white text-gray-900 shadow-sm'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
<AlignRight className="w-3.5 h-3.5" /> Vpravo
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Odsadenie (Padding) */}
|
||
<div className="grid grid-cols-2 gap-2 pt-1">
|
||
<div>
|
||
<label className="block text-[10px] text-gray-600 mb-0.5">Odsadenie zhora (px)</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
max="40"
|
||
value={activeBadge.paddingTop ?? 4}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-[10px] text-gray-600 mb-0.5">Odsadenie zdola (px)</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
max="40"
|
||
value={activeBadge.paddingBottom ?? 4}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Upload vlastného SVG alebo PNG podkladu */}
|
||
<div className="border border-dashed border-gray-300 rounded-2xl p-3 bg-gray-50/60">
|
||
<label className="block text-xs font-bold text-gray-800 mb-1 flex items-center gap-1.5">
|
||
<Upload className="w-3.5 h-3.5 text-[#e2001a]" />
|
||
Nahrať vlastný podklad cenovky (SVG alebo PNG)
|
||
</label>
|
||
<input
|
||
type="file"
|
||
accept=".svg,.png,.jpg,.jpeg,.webp,image/svg+xml,image/png,image/jpeg,image/webp"
|
||
onChange={handleBackgroundUpload}
|
||
className="text-xs text-gray-500 file:mr-2 file:py-1 file:px-2.5 file:rounded-lg file:border-0 file:text-xs file:font-semibold file:bg-gray-200 file:text-gray-700 hover:file:bg-gray-300 cursor-pointer w-full"
|
||
/>
|
||
<p className="text-[10px] text-gray-500 mt-1">
|
||
Podporuje SVG vektorové tvary aj priehľadné PNG obrázky.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Rýchle pridanie vrstiev */}
|
||
<div>
|
||
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block mb-2">
|
||
Pridať vrstvu
|
||
</span>
|
||
<div className="flex flex-wrap gap-1.5">
|
||
<button
|
||
onClick={() => addTextLayer('{cena}')}
|
||
className="text-[11px] font-bold bg-amber-50 text-amber-800 border border-amber-200 hover:bg-amber-100 px-2 py-1 rounded-lg"
|
||
>
|
||
+ Cena bez € ({'{cena}'})
|
||
</button>
|
||
<button
|
||
onClick={() => addTextLayer('-{zlava}')}
|
||
className="text-[11px] font-bold bg-red-50 text-red-800 border border-red-200 hover:bg-red-100 px-2 py-1 rounded-lg"
|
||
>
|
||
+ Zľava %
|
||
</button>
|
||
<button
|
||
onClick={() => addTextLayer('€')}
|
||
className="text-[11px] font-bold bg-blue-50 text-blue-800 border border-blue-200 hover:bg-blue-100 px-2 py-1 rounded-lg"
|
||
>
|
||
+ Znak €
|
||
</button>
|
||
<button
|
||
onClick={() => addTextLayer('{jednotkova_cena}')}
|
||
className="text-[11px] font-bold bg-emerald-50 text-emerald-800 border border-emerald-200 hover:bg-emerald-100 px-2 py-1 rounded-lg"
|
||
>
|
||
+ Merná cena
|
||
</button>
|
||
<button
|
||
onClick={() => addTextLayer('AKCIA')}
|
||
className="text-[11px] font-bold bg-gray-100 text-gray-800 border border-gray-200 hover:bg-gray-200 px-2 py-1 rounded-lg"
|
||
>
|
||
+ Vlastný text
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Zoznam a nastavenie vrstiev */}
|
||
<div className="space-y-3 pt-2 border-t border-gray-100">
|
||
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block">
|
||
Vrstvy ({activeBadge.layersConfig.length})
|
||
</span>
|
||
<div className="flex gap-1.5 overflow-x-auto pb-1">
|
||
{activeBadge.layersConfig.map((l, idx) => (
|
||
<button
|
||
key={l.id}
|
||
onClick={() => setSelectedLayerId(l.id)}
|
||
className={`text-xs px-2.5 py-1 rounded-lg font-bold border transition-all whitespace-nowrap ${
|
||
selectedLayerId === l.id
|
||
? 'bg-slate-900 text-white border-slate-900'
|
||
: 'bg-gray-100 text-gray-600 border-gray-200'
|
||
}`}
|
||
>
|
||
Vrstva {idx + 1}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{activeLayer && (
|
||
<div className="space-y-3 bg-gray-50 p-3.5 rounded-2xl border border-gray-200">
|
||
<div>
|
||
<label className="block text-[11px] font-semibold text-gray-700 mb-1">
|
||
Text / Premenná
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={activeLayer.text || ''}
|
||
onChange={(e) => updateActiveLayer({ text: e.target.value })}
|
||
className="w-full text-xs font-bold border border-gray-300 rounded-lg p-2 bg-white"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<div>
|
||
<label className="block text-[10px] text-gray-500 mb-0.5">Veľkosť fontu (px)</label>
|
||
<input
|
||
type="number"
|
||
value={activeLayer.fontSize || 16}
|
||
onChange={(e) => updateActiveLayer({ fontSize: Number(e.target.value) })}
|
||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-[10px] text-gray-500 mb-0.5">Farba</label>
|
||
<div className="flex items-center gap-1.5">
|
||
<input
|
||
type="color"
|
||
value={activeLayer.fill || '#ffffff'}
|
||
onChange={(e) => updateActiveLayer({ fill: e.target.value })}
|
||
className="w-7 h-7 rounded cursor-pointer border-0"
|
||
/>
|
||
<span className="text-[10px] font-mono">{activeLayer.fill}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Font výber */}
|
||
<div className="space-y-2 pt-2 border-t border-gray-200">
|
||
<label className="block text-[11px] font-semibold text-gray-700">
|
||
Písmo (Font) a Hrúbka
|
||
</label>
|
||
|
||
<div className="grid grid-cols-3 gap-1 bg-gray-200 p-0.5 rounded-lg text-[10px] font-bold text-center">
|
||
<button
|
||
type="button"
|
||
onClick={() => updateActiveLayer({ fontSource: 'google' })}
|
||
className={`py-1 rounded ${
|
||
(activeLayer.fontSource || 'google') === 'google'
|
||
? 'bg-white text-gray-900 shadow-sm'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
Google
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => updateActiveLayer({ fontSource: 'adobe' })}
|
||
className={`py-1 rounded ${
|
||
activeLayer.fontSource === 'adobe'
|
||
? 'bg-white text-gray-900 shadow-sm'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
Adobe
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => updateActiveLayer({ fontSource: 'system' })}
|
||
className={`py-1 rounded ${
|
||
activeLayer.fontSource === 'system'
|
||
? 'bg-white text-gray-900 shadow-sm'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
Systém
|
||
</button>
|
||
</div>
|
||
|
||
{(activeLayer.fontSource || 'google') === 'google' && (
|
||
<div className="space-y-2">
|
||
<select
|
||
value={
|
||
POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins'))
|
||
? activeLayer.fontFamily || 'Poppins'
|
||
: 'custom'
|
||
}
|
||
onChange={(e) => {
|
||
if (e.target.value !== 'custom') {
|
||
loadFontDynamically(e.target.value, 'google');
|
||
updateActiveLayer({ fontFamily: e.target.value, fontSource: 'google' });
|
||
}
|
||
}}
|
||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
|
||
>
|
||
{POPULAR_FONTS.filter((f) => f.source === 'google').map((f) => (
|
||
<option key={f.name} value={f.name}>
|
||
{f.name}
|
||
</option>
|
||
))}
|
||
<option value="custom">Iný Google Font...</option>
|
||
</select>
|
||
|
||
{(!POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins')) ||
|
||
activeLayer.fontFamily === 'custom') && (
|
||
<input
|
||
type="text"
|
||
placeholder="Názov Google fontu (napr. Montserrat)"
|
||
value={activeLayer.fontFamily === 'custom' ? '' : activeLayer.fontFamily || ''}
|
||
onChange={(e) => {
|
||
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"
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{activeLayer.fontSource === 'adobe' && (
|
||
<div className="space-y-2">
|
||
<input
|
||
type="text"
|
||
placeholder="Adobe Typekit CSS URL"
|
||
value={activeLayer.fontUrl || ''}
|
||
onChange={(e) => {
|
||
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]"
|
||
/>
|
||
<input
|
||
type="text"
|
||
placeholder="Font Family (napr. futura-pt)"
|
||
value={activeLayer.fontFamily || ''}
|
||
onChange={(e) => {
|
||
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"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{activeLayer.fontSource === 'system' && (
|
||
<select
|
||
value={activeLayer.fontFamily || 'Arial Narrow'}
|
||
onChange={(e) => updateActiveLayer({ fontFamily: e.target.value, fontSource: 'system' })}
|
||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
|
||
>
|
||
<option value="Arial Narrow">Arial Narrow</option>
|
||
<option value="Arial">Arial</option>
|
||
<option value="Impact">Impact</option>
|
||
<option value="Trebuchet MS">Trebuchet MS</option>
|
||
<option value="Helvetica">Helvetica</option>
|
||
</select>
|
||
)}
|
||
|
||
<div>
|
||
<label className="block text-[10px] text-gray-500 mb-0.5">Hrúbka písma (Weight)</label>
|
||
<select
|
||
value={activeLayer.fontWeight || '700'}
|
||
onChange={(e) => updateActiveLayer({ fontWeight: e.target.value })}
|
||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
|
||
>
|
||
<option value="400">400 - Normal</option>
|
||
<option value="600">600 - SemiBold</option>
|
||
<option value="700">700 - Bold</option>
|
||
<option value="800">800 - ExtraBold</option>
|
||
<option value="900">900 - Black</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<div>
|
||
<label className="block text-[10px] text-gray-500 mb-0.5">Pozícia X ({activeLayer.x}px)</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="140"
|
||
value={activeLayer.x}
|
||
onChange={(e) => updateActiveLayer({ x: Number(e.target.value) })}
|
||
className="w-full"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-[10px] text-gray-500 mb-0.5">Pozícia Y ({activeLayer.y}px)</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="110"
|
||
value={activeLayer.y}
|
||
onChange={(e) => updateActiveLayer({ y: Number(e.target.value) })}
|
||
className="w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Nastavenie veľkosti a pozície horného indexu (centov) */}
|
||
{(activeLayer.text?.includes('{cena}') || activeLayer.text?.includes('{centy}')) && (
|
||
<div className="pt-2.5 pb-1 border-t border-blue-200/80 bg-blue-50/70 p-3 rounded-xl border border-blue-100 space-y-2.5">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-[11px] font-bold text-blue-900">
|
||
Nastavenie horného indexu (Centov)
|
||
</span>
|
||
<span className="text-[10px] text-blue-700 bg-blue-100 px-1.5 py-0.5 rounded font-mono font-bold">
|
||
{activeLayer.indexFontSize ?? Math.max(10, Math.round((activeLayer.fontSize || 16) * 0.55))}px
|
||
</span>
|
||
</div>
|
||
|
||
{/* Veľkosť indexu */}
|
||
<div>
|
||
<div className="flex justify-between items-center text-[10px] text-gray-600 mb-0.5">
|
||
<span>Veľkosť písma indexu</span>
|
||
<span className="font-bold text-gray-800">
|
||
{activeLayer.indexFontSize ?? Math.max(10, Math.round((activeLayer.fontSize || 16) * 0.55))} px
|
||
</span>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min="8"
|
||
max="40"
|
||
value={activeLayer.indexFontSize ?? Math.max(10, Math.round((activeLayer.fontSize || 16) * 0.55))}
|
||
onChange={(e) => updateActiveLayer({ indexFontSize: Number(e.target.value) })}
|
||
className="w-full accent-blue-600"
|
||
/>
|
||
</div>
|
||
|
||
{/* Vertikálny posun (Hore / Dole) */}
|
||
<div>
|
||
<div className="flex justify-between items-center text-[10px] text-gray-600 mb-0.5">
|
||
<span>Vertikálna pozícia (Posun Hore / Dole)</span>
|
||
<span className="font-bold text-gray-800">
|
||
{activeLayer.indexOffsetY ?? -10} px
|
||
</span>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min="-30"
|
||
max="20"
|
||
value={activeLayer.indexOffsetY ?? -10}
|
||
onChange={(e) => updateActiveLayer({ indexOffsetY: Number(e.target.value) })}
|
||
className="w-full accent-blue-600"
|
||
/>
|
||
</div>
|
||
|
||
{/* Horizontálna medzera (Vľavo / Vpravo) */}
|
||
<div>
|
||
<div className="flex justify-between items-center text-[10px] text-gray-600 mb-0.5">
|
||
<span>Horizontálna medzera (Vľavo / Vpravo)</span>
|
||
<span className="font-bold text-gray-800">
|
||
{activeLayer.indexOffsetX ?? 2} px
|
||
</span>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min="-10"
|
||
max="25"
|
||
value={activeLayer.indexOffsetX ?? 2}
|
||
onChange={(e) => updateActiveLayer({ indexOffsetX: Number(e.target.value) })}
|
||
className="w-full accent-blue-600"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="pt-1 flex justify-between items-center">
|
||
<button
|
||
onClick={() => removeLayer(activeLayer.id)}
|
||
className="text-[11px] text-red-600 hover:text-red-700 font-semibold flex items-center gap-1"
|
||
>
|
||
<Trash2 className="w-3 h-3" /> Zmazať vrstvu
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tlačidlo: Uložiť šablónu do DB */}
|
||
<div className="pt-2">
|
||
<button
|
||
onClick={handleSaveToSupabase}
|
||
disabled={isSavingDb}
|
||
className="w-full py-2.5 bg-gray-900 hover:bg-black text-white text-xs font-bold rounded-xl transition-colors shadow-sm flex items-center justify-center gap-1.5"
|
||
>
|
||
<Check className="w-4 h-4 text-emerald-400" />
|
||
{isSavingDb ? 'Ukladám do Supabase...' : 'Uložiť túto šablónu (pre opakované použitie)'}
|
||
</button>
|
||
</div>
|
||
</>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div className="px-6 py-4 border-t border-gray-100 bg-gray-50 flex items-center justify-between">
|
||
<button onClick={onClose} className="px-4 py-2 text-sm font-semibold text-gray-600 hover:text-gray-900">
|
||
Zrušiť
|
||
</button>
|
||
<button
|
||
onClick={handleApplyBadge}
|
||
disabled={isExportingPng || !activeBadge}
|
||
className="px-6 py-2.5 text-sm font-bold bg-[#e2001a] text-white rounded-xl hover:bg-red-700 disabled:opacity-50 transition-colors shadow-md flex items-center gap-2"
|
||
>
|
||
<Check className="w-4 h-4" />
|
||
{isExportingPng ? 'Pripravujem cenovku...' : 'Aplikovať cenovku na produkt'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|