Fix module canvas backgrounds, clear dummy badges, add SVG upload, new template creation, and {jednotkova_cena} variable

This commit is contained in:
Jakub_Hostacny committed 2026-09-24 11:22:23 +02:00
1 parent ab74922b93
commit 3dc5778155
3 files changed
+360 -135

No files matched your search

+4 -6
View File
@@ -3,7 +3,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter'; import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter';
import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign'; import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign';
import { renderEmailHtml } from '@/lib/emailRenderer'; import { renderEmailHtml, renderSingleModuleHtml } from '@/lib/emailRenderer';
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import { TopBar } from '@/components/editor/TopBar'; import { TopBar } from '@/components/editor/TopBar';
import { ConfigPanel } from '@/components/editor/ConfigPanel'; import { ConfigPanel } from '@/components/editor/ConfigPanel';
@@ -409,13 +409,10 @@ export default function Home() {
</div> </div>
)} )}
{/* Vykreslenie modulu bez vnútorných medzier navyše */} {/* Vykreslenie čistého modulu bez obaľovacieho gradientu na každom bloku */}
<div <div
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: renderEmailHtml({ __html: renderSingleModuleHtml(mod, campaign)
...campaign,
modules: [mod]
})
}} }}
className="pointer-events-none" className="pointer-events-none"
/> />
@@ -454,6 +451,7 @@ export default function Home() {
currentDiscount={targetProduct?.discountText || '25%'} currentDiscount={targetProduct?.discountText || '25%'}
currentPriceMain={targetProduct?.priceMain || '1'} currentPriceMain={targetProduct?.priceMain || '1'}
currentPriceSub={targetProduct?.priceSub || '99'} currentPriceSub={targetProduct?.priceSub || '99'}
currentUnitPrice={targetProduct?.unitPrice || '1kg = 4,91 €'}
/> />
</div> </div>
); );
+352 -129
View File
@@ -1,10 +1,10 @@
'use client'; 'use client';
import React, { useState, useRef } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter'; import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import { toPng } from 'html-to-image'; import { toPng } from 'html-to-image';
import { Tag, Plus, Check, Trash2, X, Move, Type, Eye } from 'lucide-react'; import { Tag, Plus, Check, Trash2, X, Upload, Eye, FileCode } from 'lucide-react';
interface BadgeEditorModalProps { interface BadgeEditorModalProps {
isOpen: boolean; isOpen: boolean;
@@ -13,6 +13,7 @@ interface BadgeEditorModalProps {
currentDiscount?: string; currentDiscount?: string;
currentPriceMain?: string; currentPriceMain?: string;
currentPriceSub?: string; currentPriceSub?: string;
currentUnitPrice?: string;
} }
export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
@@ -21,79 +22,133 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
onSelectBadge, onSelectBadge,
currentDiscount = '25%', currentDiscount = '25%',
currentPriceMain = '1', currentPriceMain = '1',
currentPriceSub = '99' currentPriceSub = '99',
currentUnitPrice = '1kg = 4,91 €'
}) => { }) => {
const [badges, setBadges] = useState<BadgeTemplate[]>([ const [badges, setBadges] = useState<BadgeTemplate[]>([]);
{ const [activeBadge, setActiveBadge] = useState<BadgeTemplate | null>(null);
id: 'badge-akcia', const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
name: 'Klasik Žltá AKCIA',
svgRaw: `<svg width="140" height="90" viewBox="0 0 140 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="140" height="90" rx="12" fill="#ffd100"/>
<rect x="4" y="4" width="132" height="82" rx="10" stroke="#e0001b" stroke-width="3" stroke-dasharray="4 2"/>
</svg>`,
layersConfig: [
{ id: '1', type: 'text', text: 'AKCIA', x: 70, y: 32, fontSize: 20, fontWeight: '900', fill: '#e0001b', align: 'center' },
{ id: '2', type: 'text', text: '-{zlava}', x: 70, y: 68, fontSize: 28, fontWeight: '900', fill: '#001a33', align: 'center' }
]
},
{
id: 'badge-super-cena',
name: 'Červená SUPER CENA',
svgRaw: `<svg width="140" height="90" viewBox="0 0 140 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="140" height="90" rx="14" fill="#e2001a"/>
<circle cx="20" cy="20" r="10" fill="#ffde00" opacity="0.3"/>
<circle cx="120" cy="70" r="15" fill="#ffde00" opacity="0.2"/>
</svg>`,
layersConfig: [
{ id: '1', type: 'text', text: 'SUPER CENA', x: 70, y: 32, fontSize: 16, fontWeight: '900', fill: '#ffffff', align: 'center' },
{ id: '2', type: 'text', text: '-{zlava}', x: 70, y: 68, fontSize: 30, fontWeight: '900', fill: '#ffd100', align: 'center' }
]
},
{
id: 'badge-hviezda',
name: 'Explózia zliav (Hviezda)',
svgRaw: `<svg width="140" height="110" viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M70 5L85 38L120 28L105 60L135 80L98 88L90 120L70 95L50 120L42 88L5 80L35 60L20 28L55 38L70 5Z" fill="#ffde00" stroke="#e2001a" stroke-width="4"/>
</svg>`,
layersConfig: [
{ id: '1', type: 'text', text: 'TOP', x: 70, y: 46, fontSize: 14, fontWeight: '900', fill: '#e2001a', align: 'center' },
{ id: '2', type: 'text', text: '-{zlava}', x: 70, y: 76, fontSize: 24, fontWeight: '900', fill: '#001a33', align: 'center' }
]
},
{
id: 'badge-kruh',
name: 'Kruhová percentuálna zľava',
svgRaw: `<svg width="110" height="110" viewBox="0 0 110 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="55" cy="55" r="50" fill="#e2001a" stroke="#ffd100" stroke-width="5"/>
</svg>`,
layersConfig: [
{ id: '1', type: 'text', text: 'ZĽAVA', x: 55, y: 40, fontSize: 12, fontWeight: '800', fill: '#ffffff', align: 'center' },
{ id: '2', type: 'text', text: '-{zlava}', x: 55, y: 74, fontSize: 26, fontWeight: '900', fill: '#ffd100', align: 'center' }
]
}
]);
const [activeBadge, setActiveBadge] = useState<BadgeTemplate>(badges[0]);
const [selectedLayerId, setSelectedLayerId] = useState<string>('2');
const previewRef = useRef<HTMLDivElement>(null); const previewRef = useRef<HTMLDivElement>(null);
const [isExportingPng, setIsExportingPng] = useState(false); const [isExportingPng, setIsExportingPng] = useState(false);
const [isSavingDb, setIsSavingDb] = useState(false);
// Načítanie existujúcich šablón zo Supabase
useEffect(() => {
if (isOpen) {
loadBadgesFromSupabase();
}
}, [isOpen]);
const loadBadgesFromSupabase = async () => {
try {
const { data, error } = await supabase
.from('badges')
.select('*')
.order('created_at', { ascending: false });
if (data && !error && data.length > 0) {
const loaded: BadgeTemplate[] = data.map((d: any) => ({
id: d.id,
name: d.name,
svgRaw: d.svg_raw || '',
layersConfig: d.layers_config || []
}));
setBadges(loaded);
setActiveBadge(loaded[0]);
if (loaded[0]?.layersConfig?.[0]) {
setSelectedLayerId(loaded[0].layersConfig[0].id);
}
} else {
// Ak je databáza prázdna, necháme čistý stav
setBadges([]);
setActiveBadge(null);
}
} catch (err) {
console.error('Error loading badges:', err);
}
};
if (!isOpen) return null; if (!isOpen) return null;
const activeLayer = activeBadge.layersConfig.find((l) => l.id === selectedLayerId) || activeBadge.layersConfig[0]; // Vytvorenie úplne novej prázdnej šablóny cenovky
const handleCreateNewTemplate = () => {
const newBadge: BadgeTemplate = {
id: `badge-${Date.now()}`,
name: 'Nová vlastná cenovka',
svgRaw: `<svg width="140" height="90" viewBox="0 0 140 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="140" height="90" rx="12" fill="#ffd100"/>
</svg>`,
layersConfig: [
{
id: `l-${Date.now()}-1`,
type: 'text',
text: 'AKCIA',
x: 70,
y: 30,
fontSize: 18,
fontWeight: '900',
fill: '#e2001a',
align: 'center'
},
{
id: `l-${Date.now()}-2`,
type: 'text',
text: '-{zlava}',
x: 70,
y: 65,
fontSize: 26,
fontWeight: '900',
fill: '#001a33',
align: 'center'
}
]
};
setBadges([newBadge, ...badges]);
setActiveBadge(newBadge);
setSelectedLayerId(newBadge.layersConfig[0].id);
};
// Upload vlastného SVG súboru ako pozadia
const handleSvgUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !activeBadge) return;
const reader = new FileReader();
reader.onload = (event) => {
const content = event.target?.result as string;
if (content.includes('<svg')) {
// Vyčistenie a nastavenie rozmerov
setActiveBadge({
...activeBadge,
svgRaw: content
});
} else {
alert('Nahraný súbor nie je platný formát SVG.');
}
};
reader.readAsText(file);
};
const activeLayer =
activeBadge?.layersConfig?.find((l) => l.id === selectedLayerId) ||
activeBadge?.layersConfig?.[0] ||
null;
const updateActiveLayer = (updates: Partial<BadgeLayer>) => { const updateActiveLayer = (updates: Partial<BadgeLayer>) => {
if (!activeBadge || !activeLayer) return;
const updatedLayers = activeBadge.layersConfig.map((l) => const updatedLayers = activeBadge.layersConfig.map((l) =>
l.id === activeLayer.id ? { ...l, ...updates } : l l.id === activeLayer.id ? { ...l, ...updates } : l
); );
setActiveBadge({ ...activeBadge, layersConfig: updatedLayers }); setActiveBadge({ ...activeBadge, layersConfig: updatedLayers });
}; };
const addTextLayer = () => { const addTextLayer = (presetText: string = 'Text') => {
if (!activeBadge) return;
const newL: BadgeLayer = { const newL: BadgeLayer = {
id: `l-${Date.now()}`, id: `l-${Date.now()}`,
type: 'text', type: 'text',
text: 'Nový text', text: presetText,
x: 70, x: 70,
y: 50, y: 50,
fontSize: 16, fontSize: 16,
@@ -109,20 +164,62 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
}; };
const removeLayer = (id: string) => { const removeLayer = (id: string) => {
if (activeBadge.layersConfig.length <= 1) return; if (!activeBadge || activeBadge.layersConfig.length <= 1) return;
setActiveBadge({ setActiveBadge({
...activeBadge, ...activeBadge,
layersConfig: activeBadge.layersConfig.filter((l) => l.id !== id) layersConfig: activeBadge.layersConfig.filter((l) => l.id !== id)
}); });
}; };
const replaceVariables = (txt: string) => { const deleteBadgeTemplate = async (badgeId: string) => {
return txt try {
.replace(/{zlava}/g, currentDiscount.replace('-', '')) await supabase.from('badges').delete().eq('id', badgeId);
.replace(/{cena}/g, `${currentPriceMain},${currentPriceSub} €`); } catch (e) {}
const remaining = badges.filter((b) => b.id !== badgeId);
setBadges(remaining);
setActiveBadge(remaining[0] || null);
}; };
// Dynamické nahradenie premenných: {zlava}, {cena}, {jednotkova_cena}
const replaceVariables = (txt: string) => {
if (!txt) return '';
return txt
.replace(/{zlava}/g, currentDiscount.replace('-', ''))
.replace(/{cena}/g, `${currentPriceMain},${currentPriceSub} €`)
.replace(/{jednotkova_cena}/g, currentUnitPrice);
};
// Uloženie šablóny do Supabase
const handleSaveToSupabase = async () => {
if (!activeBadge) return;
setIsSavingDb(true);
try {
const { data, error } = await supabase
.from('badges')
.upsert([
{
id: activeBadge.id.startsWith('badge-') ? undefined : activeBadge.id,
name: activeBadge.name,
svg_raw: activeBadge.svgRaw,
layers_config: activeBadge.layersConfig
}
])
.select();
if (data && data.length > 0) {
alert('Šablóna cenovky bola úspešne uložená do databázy!');
loadBadgesFromSupabase();
}
} catch (err: any) {
alert(`Chyba pri ukladaní do databázy: ${err.message}`);
} finally {
setIsSavingDb(false);
}
};
// Render do PNG a aplikovanie na produkt
const handleApplyBadge = async () => { const handleApplyBadge = async () => {
if (!activeBadge) return;
if (!previewRef.current) { if (!previewRef.current) {
onSelectBadge(activeBadge); onSelectBadge(activeBadge);
onClose(); onClose();
@@ -131,7 +228,6 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
setIsExportingPng(true); setIsExportingPng(true);
try { try {
// SVG -> PNG render cez HTML5 Canvas/toPng
const dataUrl = await toPng(previewRef.current, { const dataUrl = await toPng(previewRef.current, {
backgroundColor: 'transparent', backgroundColor: 'transparent',
pixelRatio: 2 pixelRatio: 2
@@ -148,74 +244,116 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
}; };
return ( return (
<div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-md flex items-center justify-center p-4"> <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-4xl w-full max-h-[92vh] overflow-hidden flex flex-col border border-gray-100"> <div className="bg-white rounded-3xl shadow-2xl max-w-5xl w-full max-h-[92vh] overflow-hidden flex flex-col border border-gray-100">
{/* Header */} {/* Header */}
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-slate-50"> <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-2.5"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-red-600 flex items-center justify-center text-white"> <div className="w-9 h-9 rounded-xl bg-[#e2001a] flex items-center justify-center text-white shadow-sm">
<Tag className="w-4 h-4" /> <Tag className="w-5 h-5" />
</div> </div>
<div> <div>
<h2 className="text-base font-bold text-gray-900"> <h2 className="text-base font-bold text-gray-900">
Editor grafických cenoviek (SVG &rarr; PNG Pipeline) Editor a generátor grafických cenoviek (SVG &rarr; PNG)
</h2> </h2>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Upravte texty, premenné a pozície. Pri uložení sa cenovka automaticky vyrenderuje do orezaného PNG pre e-mail. Vytvorte vlastnú šablónu, nahrajte SVG tvar, pridajte dynamické premenné a vyrenderujte PNG pre e-mail.
</p> </p>
</div> </div>
</div> </div>
<button onClick={onClose} className="p-1.5 text-gray-400 hover:text-gray-700 rounded-xl hover:bg-gray-200/50"> <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" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
{/* Content */} {/* Content Body */}
<div className="flex-1 overflow-hidden grid grid-cols-12 divide-x divide-gray-100"> <div className="flex-1 overflow-hidden grid grid-cols-12 divide-x divide-gray-100">
{/* Ľavý stĺpec: Šablóny */} {/* 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="col-span-3 p-4 overflow-y-auto space-y-3 bg-gray-50/50">
<span className="text-[11px] font-bold uppercase tracking-wider text-gray-400 block px-1"> <div className="flex items-center justify-between px-1">
Katalóg cenoviek <span className="text-[11px] font-bold uppercase tracking-wider text-gray-400">
Vaše šablóny ({badges.length})
</span> </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>
{badges.length === 0 ? (
<div className="p-6 text-center border-2 border-dashed border-gray-200 rounded-2xl bg-white">
<Tag className="w-8 h-8 text-gray-300 mx-auto mb-2" />
<p className="text-xs text-gray-500 mb-3">Zatiaľ nemáte žiadne šablóny cenoviek.</p>
<button
onClick={handleCreateNewTemplate}
className="w-full py-2 bg-[#e2001a] text-white text-xs font-bold rounded-xl shadow-xs hover:bg-red-700"
>
Vytvoriť prvú šablónu
</button>
</div>
) : (
<div className="space-y-2"> <div className="space-y-2">
{badges.map((b) => ( {badges.map((b) => (
<button <div
key={b.id} key={b.id}
onClick={() => setActiveBadge(b)} onClick={() => {
className={`w-full text-left p-3 rounded-2xl border transition-all ${ setActiveBadge(b);
activeBadge.id === b.id if (b.layersConfig?.[0]) setSelectedLayerId(b.layersConfig[0].id);
? 'bg-white border-red-500 shadow-md ring-2 ring-red-500/10' }}
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' : 'bg-white/80 border-gray-200 hover:border-gray-300'
}`} }`}
> >
<p className="text-xs font-bold text-gray-800 truncate mb-1">{b.name}</p> <div className="flex items-center justify-between mb-1">
<p className="text-xs font-bold text-gray-800 truncate">{b.name}</p>
<button
onClick={(e) => {
e.stopPropagation();
deleteBadgeTemplate(b.id);
}}
className="text-gray-300 hover:text-red-600 p-1 rounded transition-colors opacity-0 group-hover:opacity-100"
title="Zmazať šablónu"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
<div <div
className="w-full h-14 flex items-center justify-center transform scale-75 origin-center overflow-hidden" className="w-full h-12 flex items-center justify-center transform scale-75 origin-center overflow-hidden"
dangerouslySetInnerHTML={{ __html: b.svgRaw || '' }} dangerouslySetInnerHTML={{ __html: b.svgRaw || '' }}
/> />
</button> </div>
))} ))}
</div> </div>
)}
</div> </div>
{/* Stredný stĺpec: Interaktívny Canvas / Náhľad */} {/* 2. Stĺpec: Vektorový Canvas & Náhľad */}
<div className="col-span-5 p-6 flex flex-col items-center justify-center bg-slate-900/95 relative"> <div className="col-span-5 p-6 flex flex-col items-center justify-center bg-slate-950 relative">
<span className="absolute top-4 left-4 text-xs font-semibold text-slate-400 flex items-center gap-1.5"> <div className="absolute top-4 left-4 flex items-center gap-2">
<Eye className="w-3.5 h-3.5" /> Živý vektorový náhľad <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 cenovky
</span> </span>
</div>
{/* Plátno cenovky */} {activeBadge ? (
<div className="flex flex-col items-center justify-center">
{/* Vektorové plátno pre export do PNG */}
<div <div
ref={previewRef} ref={previewRef}
className="relative flex items-center justify-center p-8 select-none" className="relative flex items-center justify-center p-8 select-none"
style={{ minWidth: 200, minHeight: 160 }} style={{ minWidth: 160, minHeight: 120 }}
> >
{/* SVG Podklad */}
<div <div
className="relative" className="relative"
dangerouslySetInnerHTML={{ __html: activeBadge.svgRaw || '' }} dangerouslySetInnerHTML={{ __html: activeBadge.svgRaw || '' }}
/> />
{/* Textové vrstvy nad SVG podkladom */} {/* Dynamické textové vrstvy nad SVG podkladom */}
{activeBadge.layersConfig.map((layer) => { {activeBadge.layersConfig.map((layer) => {
const isSelected = layer.id === selectedLayerId; const isSelected = layer.id === selectedLayerId;
return ( return (
@@ -234,8 +372,10 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
cursor: 'pointer' cursor: 'pointer'
}} }}
className={`px-1 rounded transition-all ${ className={`px-1.5 py-0.5 rounded transition-all ${
isSelected ? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-transparent' : '' isSelected
? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-transparent bg-blue-500/10'
: 'hover:outline hover:outline-1 hover:outline-white/40'
}`} }`}
> >
{replaceVariables(layer.text || '')} {replaceVariables(layer.text || '')}
@@ -244,32 +384,103 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
})} })}
</div> </div>
<p className="text-[11px] text-slate-400 mt-4 text-center"> {/* Dostupné premenné */}
Dostupné premenné: <code className="text-amber-300 font-mono">{`{zlava}`}</code> (hodnota: {currentDiscount}), <code className="text-amber-300 font-mono">{`{cena}`}</code> <div className="mt-4 bg-slate-900/80 border border-slate-800 rounded-xl p-3 text-[11px] text-slate-300 space-y-1 max-w-sm">
<span className="font-bold text-gray-400 block mb-1">Dostupné dynamické premenné:</span>
<div className="flex flex-wrap gap-2">
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
{'{zlava}'} &rarr; {currentDiscount}
</span>
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
{'{cena}'} &rarr; {currentPriceMain},{currentPriceSub} €
</span>
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
{'{jednotkova_cena}'} &rarr; {currentUnitPrice}
</span>
</div>
</div>
</div>
) : (
<p className="text-sm text-slate-500">Vyberte alebo vytvorte novú šablónu cenovky.</p>
)}
</div>
{/* 3. Stĺpec: Konfigurácia SVG podkladu 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>
{/* Upload vlastného SVG podkladu */}
<div className="border border-dashed border-gray-300 rounded-2xl p-3.5 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ý SVG tvar (Podklad)
</label>
<input
type="file"
accept=".svg"
onChange={handleSvgUpload}
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-400 mt-1">
Nahrajte ľubovoľný tvar: hviezdu, bublinu, štítok alebo percento.
</p> </p>
</div> </div>
{/* Pravý stĺpec: Konfigurácia vrstiev */} {/* Rýchle pridanie premenných ako vrstvy */}
<div className="col-span-4 p-5 overflow-y-auto space-y-5 bg-white"> <div>
<div className="flex items-center justify-between border-b border-gray-100 pb-3"> <span className="text-xs font-bold text-gray-800 uppercase tracking-wider block mb-2">
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider"> Pridať vrstvu s premennou
Vrstvy textov ({activeBadge.layersConfig.length})
</span> </span>
<div className="flex flex-wrap gap-1.5">
<button <button
onClick={addTextLayer} onClick={() => addTextLayer('-{zlava}')}
className="text-xs font-bold text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100 px-2.5 py-1 rounded-lg flex items-center gap-1 transition-colors" 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"
> >
<Plus className="w-3.5 h-3.5" /> Pridať text + Zľava %
</button> </button>
<button
onClick={() => addTextLayer('{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"
>
+ Cena €
</button>
<button
onClick={() => addTextLayer('{jednotkova_cena}')}
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"
>
+ Merná cena
</button>
<button
onClick={() => addTextLayer('SUPER CENA')}
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> </div>
{/* Zoznam vrstiev */} {/* Zoznam 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"> <div className="flex gap-1.5 overflow-x-auto pb-1">
{activeBadge.layersConfig.map((l, idx) => ( {activeBadge.layersConfig.map((l, idx) => (
<button <button
key={l.id} key={l.id}
onClick={() => setSelectedLayerId(l.id)} onClick={() => setSelectedLayerId(l.id)}
className={`text-xs px-3 py-1.5 rounded-lg font-bold border transition-all whitespace-nowrap ${ className={`text-xs px-2.5 py-1 rounded-lg font-bold border transition-all whitespace-nowrap ${
selectedLayerId === l.id selectedLayerId === l.id
? 'bg-slate-900 text-white border-slate-900' ? 'bg-slate-900 text-white border-slate-900'
: 'bg-gray-100 text-gray-600 border-gray-200' : 'bg-gray-100 text-gray-600 border-gray-200'
@@ -282,81 +493,93 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
{/* Nastavenie aktívnej vrstvy */} {/* Nastavenie aktívnej vrstvy */}
{activeLayer && ( {activeLayer && (
<div className="space-y-4 pt-1"> <div className="space-y-3 bg-gray-50 p-3.5 rounded-2xl border border-gray-200">
<div> <div>
<label className="block text-xs font-semibold text-gray-700 mb-1"> <label className="block text-[11px] font-semibold text-gray-700 mb-1">
Text vrstvy (podporuje premennú {'{zlava}'}) Text / Premenná
</label> </label>
<input <input
type="text" type="text"
value={activeLayer.text || ''} value={activeLayer.text || ''}
onChange={(e) => updateActiveLayer({ text: e.target.value })} onChange={(e) => updateActiveLayer({ text: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-xl p-2.5 font-medium" className="w-full text-xs font-bold border border-gray-300 rounded-lg p-2 bg-white"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-2">
<div> <div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Veľkosť písma (px)</label> <label className="block text-[10px] text-gray-500 mb-0.5">Veľkosť fontu (px)</label>
<input <input
type="number" type="number"
value={activeLayer.fontSize || 16} value={activeLayer.fontSize || 16}
onChange={(e) => updateActiveLayer({ fontSize: Number(e.target.value) })} onChange={(e) => updateActiveLayer({ fontSize: Number(e.target.value) })}
className="w-full text-sm border border-gray-300 rounded-xl p-2" className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Farba písma</label> <label className="block text-[10px] text-gray-500 mb-0.5">Farba</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5">
<input <input
type="color" type="color"
value={activeLayer.fill || '#ffffff'} value={activeLayer.fill || '#ffffff'}
onChange={(e) => updateActiveLayer({ fill: e.target.value })} onChange={(e) => updateActiveLayer({ fill: e.target.value })}
className="w-8 h-8 rounded-lg cursor-pointer border-0" className="w-7 h-7 rounded cursor-pointer border-0"
/> />
<span className="text-xs font-mono">{activeLayer.fill}</span> <span className="text-[10px] font-mono">{activeLayer.fill}</span>
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-2">
<div> <div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Pozícia X (px)</label> <label className="block text-[10px] text-gray-500 mb-0.5">Pozícia X ({activeLayer.x}px)</label>
<input <input
type="range" type="range"
min="10" min="0"
max="130" max="140"
value={activeLayer.x} value={activeLayer.x}
onChange={(e) => updateActiveLayer({ x: Number(e.target.value) })} onChange={(e) => updateActiveLayer({ x: Number(e.target.value) })}
className="w-full" className="w-full"
/> />
<span className="text-[10px] text-gray-400">{activeLayer.x} px</span>
</div> </div>
<div> <div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Pozícia Y (px)</label> <label className="block text-[10px] text-gray-500 mb-0.5">Pozícia Y ({activeLayer.y}px)</label>
<input <input
type="range" type="range"
min="10" min="0"
max="100" max="110"
value={activeLayer.y} value={activeLayer.y}
onChange={(e) => updateActiveLayer({ y: Number(e.target.value) })} onChange={(e) => updateActiveLayer({ y: Number(e.target.value) })}
className="w-full" className="w-full"
/> />
<span className="text-[10px] text-gray-400">{activeLayer.y} px</span>
</div> </div>
</div> </div>
<div className="pt-2"> <div className="pt-1 flex justify-between items-center">
<button <button
onClick={() => removeLayer(activeLayer.id)} onClick={() => removeLayer(activeLayer.id)}
className="text-xs text-red-600 hover:text-red-700 font-semibold flex items-center gap-1" className="text-[11px] text-red-600 hover:text-red-700 font-semibold flex items-center gap-1"
> >
<Trash2 className="w-3.5 h-3.5" /> Odstrániť túto vrstvu <Trash2 className="w-3 h-3" /> Zmazať vrstvu
</button> </button>
</div> </div>
</div> </div>
)} )}
</div> </div>
{/* Uloženie šablóny do databázy */}
<div className="pt-2">
<button
onClick={handleSaveToSupabase}
disabled={isSavingDb}
className="w-full py-2 bg-gray-900 text-white text-xs font-bold rounded-xl hover:bg-black transition-colors"
>
{isSavingDb ? 'Ukladám do Supabase...' : 'Uložiť túto šablónu do Supabase'}
</button>
</div>
</>
) : null}
</div>
</div> </div>
{/* Footer */} {/* Footer */}
@@ -366,8 +589,8 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
</button> </button>
<button <button
onClick={handleApplyBadge} onClick={handleApplyBadge}
disabled={isExportingPng} disabled={isExportingPng || !activeBadge}
className="px-6 py-2.5 text-sm font-bold bg-[#e2001a] text-white rounded-xl hover:bg-red-700 transition-colors shadow-md flex items-center gap-2" 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" /> <Check className="w-4 h-4" />
{isExportingPng ? 'Generujem PNG cenovku...' : 'Aplikovať cenovku na produkt'} {isExportingPng ? 'Generujem PNG cenovku...' : 'Aplikovať cenovku na produkt'}
+4
View File
@@ -197,6 +197,10 @@ export function renderEmailHtml(campaign: NewsletterCampaign): string {
</html>`; </html>`;
} }
export function renderSingleModuleHtml(m: any, campaign: NewsletterCampaign): string {
return renderModule(m, campaign);
}
function renderModule(m: any, campaign: NewsletterCampaign): string { function renderModule(m: any, campaign: NewsletterCampaign): string {
switch (m.type) { switch (m.type) {
case 'announcement': case 'announcement':