Fix canvas background to exact COOP gradient, remove module borders/gaps, add interactive Badge Editor SVG to PNG
This commit is contained in:
1 parent
65f37a74b8
commit
ab74922b93
5 files changed
+537
-99
No files matched your search
+133
-97
@@ -1,17 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { NewsletterCampaign, NewsletterModule, ProductItem } from '@/types/newsletter';
|
||||
import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter';
|
||||
import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign';
|
||||
import { renderEmailHtml } from '@/lib/emailRenderer';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { TopBar } from '@/components/editor/TopBar';
|
||||
import { ConfigPanel } from '@/components/editor/ConfigPanel';
|
||||
import { ProductImportModal } from '@/components/editor/ProductImportModal';
|
||||
import { BadgeManagerModal } from '@/components/editor/BadgeManagerModal';
|
||||
import { BadgeEditorModal } from '@/components/editor/BadgeEditorModal';
|
||||
import JSZip from 'jszip';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { ChevronUp, ChevronDown, Copy, Trash2 } from 'lucide-react';
|
||||
import { ChevronUp, ChevronDown, Copy, Trash2, Edit3 } from 'lucide-react';
|
||||
|
||||
export default function Home() {
|
||||
const [campaign, setCampaign] = useState<NewsletterCampaign>(DEFAULT_CAMPAIGN);
|
||||
@@ -22,13 +22,14 @@ export default function Home() {
|
||||
|
||||
// Modály
|
||||
const [isImportOpen, setIsImportOpen] = useState(false);
|
||||
const [isBadgeModalOpen, setIsBadgeModalOpen] = useState(false);
|
||||
const [isBadgeEditorOpen, setIsBadgeEditorOpen] = useState(false);
|
||||
const [targetProductIdForBadge, setTargetProductIdForBadge] = useState<string | null>(null);
|
||||
|
||||
// Načítanie existujúceho draftu z databázy
|
||||
useEffect(() => {
|
||||
async function loadInitial() {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
const { data } = await supabase
|
||||
.from('templates')
|
||||
.select('*')
|
||||
.order('updated_at', { ascending: false })
|
||||
@@ -36,8 +37,7 @@ export default function Home() {
|
||||
|
||||
if (data && data.length > 0) {
|
||||
const dbItem = data[0];
|
||||
// Ak má databáza rovnakú štruktúru, načítame ju, inak necháme DEFAULT_CAMPAIGN 1:1
|
||||
if (dbItem.data && dbItem.data.modules && dbItem.data.modules.length >= 6) {
|
||||
if (dbItem.data && dbItem.data.modules && dbItem.data.modules.length >= 7) {
|
||||
setCampaign({
|
||||
id: dbItem.id,
|
||||
title: dbItem.title,
|
||||
@@ -291,11 +291,36 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
// Kompilácia celého HTML pre responzívny náhľad v reálnom čase
|
||||
const fullHtml = renderEmailHtml(campaign);
|
||||
// Vybraný produkt pre editor cenoviek
|
||||
let targetProduct: ProductItem | null = null;
|
||||
if (targetProductIdForBadge && selectedModule?.data?.products) {
|
||||
targetProduct = selectedModule.data.products.find((p: any) => p.id === targetProductIdForBadge) || null;
|
||||
}
|
||||
|
||||
const handleApplyBadgeToProduct = (badge: BadgeTemplate, renderedPngUrl?: string) => {
|
||||
if (!targetProductIdForBadge || !selectedModule) return;
|
||||
const updatedProducts = (selectedModule.data.products || []).map((p: ProductItem) => {
|
||||
if (p.id === targetProductIdForBadge) {
|
||||
return {
|
||||
...p,
|
||||
badgeId: badge.id,
|
||||
customBadgePngUrl: renderedPngUrl || p.customBadgePngUrl
|
||||
};
|
||||
}
|
||||
return p;
|
||||
});
|
||||
|
||||
handleUpdateModule({
|
||||
...selectedModule,
|
||||
data: {
|
||||
...selectedModule.data,
|
||||
products: updatedProducts
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen overflow-hidden bg-[#001a33] font-sans">
|
||||
<div className="flex flex-col h-screen overflow-hidden bg-slate-950 font-sans">
|
||||
{/* 1. Horná lišta */}
|
||||
<TopBar
|
||||
campaign={campaign}
|
||||
@@ -310,89 +335,93 @@ export default function Home() {
|
||||
|
||||
{/* 2. Hlavný pracovný priestor */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Stredné plátno - Živý responzívny e-mail v iframe pre 100% izoláciu CSS a médií */}
|
||||
<main className="flex-1 overflow-y-auto p-4 md:p-8 flex items-start justify-center bg-[#000d1a]">
|
||||
<div className="w-full max-w-[680px] shadow-2xl rounded-2xl overflow-hidden border border-blue-950/50 bg-[#001a33]">
|
||||
{/* Zoznam modulov pre priamu editáciu */}
|
||||
<div className="p-3 bg-[#001a33] space-y-3">
|
||||
{campaign.modules.map((mod, idx) => {
|
||||
const isSelected = mod.id === selectedModuleId;
|
||||
{/* Stredné plátno - Presný originálny gradient a čisté rozloženie */}
|
||||
<main
|
||||
className="flex-1 overflow-y-auto p-4 md:p-8 flex items-start justify-center"
|
||||
style={{
|
||||
backgroundColor: '#001a33',
|
||||
backgroundImage: 'linear-gradient(135deg, #005bb5 0%, #000d1a 100%)'
|
||||
}}
|
||||
>
|
||||
{/* E-mail Container 1:1 k originálu */}
|
||||
<div className="w-full max-w-[600px] my-4">
|
||||
{campaign.modules.map((mod, idx) => {
|
||||
const isSelected = mod.id === selectedModuleId;
|
||||
|
||||
return (
|
||||
return (
|
||||
<div
|
||||
key={mod.id}
|
||||
onClick={() => setSelectedModuleId(mod.id)}
|
||||
className={`relative cursor-pointer transition-all duration-150 ${
|
||||
isSelected
|
||||
? 'outline-2 outline-offset-4 outline-red-500 rounded-2xl shadow-2xl'
|
||||
: 'hover:outline-1 hover:outline-offset-2 hover:outline-white/30 rounded-2xl'
|
||||
}`}
|
||||
>
|
||||
{/* Lišta s nástrojmi pre označený modul */}
|
||||
{isSelected && (
|
||||
<div className="absolute -top-3 right-4 z-40 flex items-center gap-1 bg-[#001a33] text-white px-3 py-1.5 rounded-full text-xs shadow-2xl border border-white/20 backdrop-blur-md">
|
||||
<span className="text-[10px] font-bold text-gray-300 mr-1.5 uppercase tracking-wider">
|
||||
{mod.title}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
moveModule(idx, 'up');
|
||||
}}
|
||||
disabled={idx === 0}
|
||||
className="hover:text-red-400 disabled:opacity-30 p-1"
|
||||
title="Posunúť hore"
|
||||
>
|
||||
<ChevronUp className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
moveModule(idx, 'down');
|
||||
}}
|
||||
disabled={idx === campaign.modules.length - 1}
|
||||
className="hover:text-red-400 disabled:opacity-30 p-1"
|
||||
title="Posunúť dole"
|
||||
>
|
||||
<ChevronDown className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
duplicateModule(idx);
|
||||
}}
|
||||
className="hover:text-blue-400 p-1 ml-1"
|
||||
title="Duplikovať"
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteModule(idx);
|
||||
}}
|
||||
className="hover:text-red-400 p-1 ml-1"
|
||||
title="Zmazať"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vykreslenie modulu bez vnútorných medzier navyše */}
|
||||
<div
|
||||
key={mod.id}
|
||||
onClick={() => setSelectedModuleId(mod.id)}
|
||||
className={`relative group cursor-pointer transition-all duration-200 rounded-2xl border-2 ${
|
||||
isSelected
|
||||
? 'border-red-500 ring-4 ring-red-500/20'
|
||||
: 'border-transparent hover:border-slate-700/60'
|
||||
}`}
|
||||
>
|
||||
{/* Lišta s nástrojmi pre označený modul */}
|
||||
{isSelected && (
|
||||
<div className="absolute top-2 right-2 z-30 flex items-center gap-1.5 bg-slate-900/95 backdrop-blur-md text-white px-2.5 py-1.5 rounded-xl text-xs shadow-2xl border border-slate-700">
|
||||
<span className="text-[10px] font-bold text-gray-400 mr-1 uppercase">
|
||||
{mod.title}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
moveModule(idx, 'up');
|
||||
}}
|
||||
disabled={idx === 0}
|
||||
className="hover:text-red-400 disabled:opacity-30 p-1"
|
||||
title="Posunúť hore"
|
||||
>
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
moveModule(idx, 'down');
|
||||
}}
|
||||
disabled={idx === campaign.modules.length - 1}
|
||||
className="hover:text-red-400 disabled:opacity-30 p-1"
|
||||
title="Posunúť dole"
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
duplicateModule(idx);
|
||||
}}
|
||||
className="hover:text-blue-400 p-1"
|
||||
title="Duplikovať"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteModule(idx);
|
||||
}}
|
||||
className="hover:text-red-400 p-1"
|
||||
title="Zmazať"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vykreslenie modulu 1:1 */}
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderEmailHtml({
|
||||
...campaign,
|
||||
modules: [mod]
|
||||
})
|
||||
}}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderEmailHtml({
|
||||
...campaign,
|
||||
modules: [mod]
|
||||
})
|
||||
}}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -401,23 +430,30 @@ export default function Home() {
|
||||
<ConfigPanel
|
||||
module={selectedModule}
|
||||
onUpdateModule={handleUpdateModule}
|
||||
onOpenBadgeManager={() => setIsBadgeModalOpen(true)}
|
||||
onOpenBadgeManager={(pId) => {
|
||||
setTargetProductIdForBadge(pId);
|
||||
setIsBadgeEditorOpen(true);
|
||||
}}
|
||||
onOpenImport={() => setIsImportOpen(true)}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Modály */}
|
||||
{/* Modál Hromadného importu */}
|
||||
<ProductImportModal
|
||||
isOpen={isImportOpen}
|
||||
onClose={() => setIsImportOpen(false)}
|
||||
onImportProducts={handleImportProducts}
|
||||
/>
|
||||
|
||||
<BadgeManagerModal
|
||||
isOpen={isBadgeModalOpen}
|
||||
onClose={() => setIsBadgeModalOpen(false)}
|
||||
onSelectBadge={() => {}}
|
||||
{/* Modál Editora cenoviek (SVG -> PNG) */}
|
||||
<BadgeEditorModal
|
||||
isOpen={isBadgeEditorOpen}
|
||||
onClose={() => setIsBadgeEditorOpen(false)}
|
||||
onSelectBadge={handleApplyBadgeToProduct}
|
||||
currentDiscount={targetProduct?.discountText || '25%'}
|
||||
currentPriceMain={targetProduct?.priceMain || '1'}
|
||||
currentPriceSub={targetProduct?.priceSub || '99'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { toPng } from 'html-to-image';
|
||||
import { Tag, Plus, Check, Trash2, X, Move, Type, Eye } from 'lucide-react';
|
||||
|
||||
interface BadgeEditorModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSelectBadge: (badge: BadgeTemplate, renderedPngUrl?: string) => void;
|
||||
currentDiscount?: string;
|
||||
currentPriceMain?: string;
|
||||
currentPriceSub?: string;
|
||||
}
|
||||
|
||||
export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSelectBadge,
|
||||
currentDiscount = '25%',
|
||||
currentPriceMain = '1',
|
||||
currentPriceSub = '99'
|
||||
}) => {
|
||||
const [badges, setBadges] = useState<BadgeTemplate[]>([
|
||||
{
|
||||
id: 'badge-akcia',
|
||||
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 [isExportingPng, setIsExportingPng] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const activeLayer = activeBadge.layersConfig.find((l) => l.id === selectedLayerId) || activeBadge.layersConfig[0];
|
||||
|
||||
const updateActiveLayer = (updates: Partial<BadgeLayer>) => {
|
||||
const updatedLayers = activeBadge.layersConfig.map((l) =>
|
||||
l.id === activeLayer.id ? { ...l, ...updates } : l
|
||||
);
|
||||
setActiveBadge({ ...activeBadge, layersConfig: updatedLayers });
|
||||
};
|
||||
|
||||
const addTextLayer = () => {
|
||||
const newL: BadgeLayer = {
|
||||
id: `l-${Date.now()}`,
|
||||
type: 'text',
|
||||
text: 'Nový text',
|
||||
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.layersConfig.length <= 1) return;
|
||||
setActiveBadge({
|
||||
...activeBadge,
|
||||
layersConfig: activeBadge.layersConfig.filter((l) => l.id !== id)
|
||||
});
|
||||
};
|
||||
|
||||
const replaceVariables = (txt: string) => {
|
||||
return txt
|
||||
.replace(/{zlava}/g, currentDiscount.replace('-', ''))
|
||||
.replace(/{cena}/g, `${currentPriceMain},${currentPriceSub} €`);
|
||||
};
|
||||
|
||||
const handleApplyBadge = async () => {
|
||||
if (!previewRef.current) {
|
||||
onSelectBadge(activeBadge);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsExportingPng(true);
|
||||
try {
|
||||
// SVG -> PNG render cez HTML5 Canvas/toPng
|
||||
const dataUrl = await toPng(previewRef.current, {
|
||||
backgroundColor: 'transparent',
|
||||
pixelRatio: 2
|
||||
});
|
||||
onSelectBadge(activeBadge, dataUrl);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error('Render error:', err);
|
||||
onSelectBadge(activeBadge);
|
||||
onClose();
|
||||
} finally {
|
||||
setIsExportingPng(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/75 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">
|
||||
{/* 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-2.5">
|
||||
<div className="w-8 h-8 rounded-lg bg-red-600 flex items-center justify-center text-white">
|
||||
<Tag className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-gray-900">
|
||||
Editor grafických cenoviek (SVG → PNG Pipeline)
|
||||
</h2>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-1.5 text-gray-400 hover:text-gray-700 rounded-xl hover:bg-gray-200/50">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-hidden grid grid-cols-12 divide-x divide-gray-100">
|
||||
{/* Ľavý stĺpec: Šablóny */}
|
||||
<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">
|
||||
Katalóg cenoviek
|
||||
</span>
|
||||
<div className="space-y-2">
|
||||
{badges.map((b) => (
|
||||
<button
|
||||
key={b.id}
|
||||
onClick={() => setActiveBadge(b)}
|
||||
className={`w-full text-left p-3 rounded-2xl border transition-all ${
|
||||
activeBadge.id === b.id
|
||||
? 'bg-white border-red-500 shadow-md ring-2 ring-red-500/10'
|
||||
: '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="w-full h-14 flex items-center justify-center transform scale-75 origin-center overflow-hidden"
|
||||
dangerouslySetInnerHTML={{ __html: b.svgRaw || '' }}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stredný stĺpec: Interaktívny Canvas / Náhľad */}
|
||||
<div className="col-span-5 p-6 flex flex-col items-center justify-center bg-slate-900/95 relative">
|
||||
<span className="absolute top-4 left-4 text-xs font-semibold text-slate-400 flex items-center gap-1.5">
|
||||
<Eye className="w-3.5 h-3.5" /> Živý vektorový náhľad
|
||||
</span>
|
||||
|
||||
{/* Plátno cenovky */}
|
||||
<div
|
||||
ref={previewRef}
|
||||
className="relative flex items-center justify-center p-8 select-none"
|
||||
style={{ minWidth: 200, minHeight: 160 }}
|
||||
>
|
||||
<div
|
||||
className="relative"
|
||||
dangerouslySetInnerHTML={{ __html: activeBadge.svgRaw || '' }}
|
||||
/>
|
||||
|
||||
{/* Textové vrstvy nad SVG podkladom */}
|
||||
{activeBadge.layersConfig.map((layer) => {
|
||||
const isSelected = layer.id === selectedLayerId;
|
||||
return (
|
||||
<div
|
||||
key={layer.id}
|
||||
onClick={() => 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 as any,
|
||||
color: layer.fill,
|
||||
fontFamily: 'Poppins, Arial, sans-serif',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
className={`px-1 rounded transition-all ${
|
||||
isSelected ? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-transparent' : ''
|
||||
}`}
|
||||
>
|
||||
{replaceVariables(layer.text || '')}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-slate-400 mt-4 text-center">
|
||||
Dostupné premenné: <code className="text-amber-300 font-mono">{`{zlava}`}</code> (hodnota: {currentDiscount}), <code className="text-amber-300 font-mono">{`{cena}`}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pravý stĺpec: Konfigurácia vrstiev */}
|
||||
<div className="col-span-4 p-5 overflow-y-auto space-y-5 bg-white">
|
||||
<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">
|
||||
Vrstvy textov ({activeBadge.layersConfig.length})
|
||||
</span>
|
||||
<button
|
||||
onClick={addTextLayer}
|
||||
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"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" /> Pridať text
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Zoznam vrstiev */}
|
||||
<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-3 py-1.5 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>
|
||||
|
||||
{/* Nastavenie aktívnej vrstvy */}
|
||||
{activeLayer && (
|
||||
<div className="space-y-4 pt-1">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">
|
||||
Text vrstvy (podporuje premennú {'{zlava}'})
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={activeLayer.text || ''}
|
||||
onChange={(e) => updateActiveLayer({ text: e.target.value })}
|
||||
className="w-full text-sm border border-gray-300 rounded-xl p-2.5 font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Veľkosť písma (px)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={activeLayer.fontSize || 16}
|
||||
onChange={(e) => updateActiveLayer({ fontSize: Number(e.target.value) })}
|
||||
className="w-full text-sm border border-gray-300 rounded-xl p-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Farba písma</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={activeLayer.fill || '#ffffff'}
|
||||
onChange={(e) => updateActiveLayer({ fill: e.target.value })}
|
||||
className="w-8 h-8 rounded-lg cursor-pointer border-0"
|
||||
/>
|
||||
<span className="text-xs font-mono">{activeLayer.fill}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Pozícia X (px)</label>
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="130"
|
||||
value={activeLayer.x}
|
||||
onChange={(e) => updateActiveLayer({ x: Number(e.target.value) })}
|
||||
className="w-full"
|
||||
/>
|
||||
<span className="text-[10px] text-gray-400">{activeLayer.x} px</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Pozícia Y (px)</label>
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="100"
|
||||
value={activeLayer.y}
|
||||
onChange={(e) => updateActiveLayer({ y: Number(e.target.value) })}
|
||||
className="w-full"
|
||||
/>
|
||||
<span className="text-[10px] text-gray-400">{activeLayer.y} px</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
onClick={() => removeLayer(activeLayer.id)}
|
||||
className="text-xs 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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</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}
|
||||
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"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
{isExportingPng ? 'Generujem PNG cenovku...' : 'Aplikovať cenovku na produkt'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -387,6 +387,20 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
Zobraziť slovenskú vlajku / srdce
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Grafická cenovka - Editor cenoviek */}
|
||||
<div className="pt-2 border-t border-gray-200 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Tag className="w-3.5 h-3.5 text-red-600" />
|
||||
<span className="text-xs font-semibold text-gray-700">Grafická cenovka</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onOpenBadgeManager && onOpenBadgeManager(p.id)}
|
||||
className="text-xs font-bold text-red-600 hover:text-white hover:bg-red-600 border border-red-200 px-2.5 py-1 rounded-lg transition-all"
|
||||
>
|
||||
{p.customBadgePngUrl ? 'Upraviť v editore' : 'Otvoriť Editor cenoviek'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -609,7 +609,15 @@ function renderExactProductCard(
|
||||
</tr>
|
||||
<tr height="1">
|
||||
<td style="padding: 0 8px 12px 8px; text-align: center;" valign="bottom">
|
||||
<!-- Vyladená Cenovka Premium v8/v11 1:1 -->
|
||||
<!-- Vyladená Cenovka Premium v8/v11 1:1 alebo vyrenderovaná PNG cenovka z editora -->
|
||||
${p.customBadgePngUrl ? `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" style="margin: 0 auto;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="${escapeHtml(p.customBadgePngUrl)}" width="110" alt="Cenovka" style="display: block; margin: 0 auto; max-width: 120px;">
|
||||
</td>
|
||||
</tr>
|
||||
</table>` : `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" style="width: 95px; margin: 0 auto;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
@@ -640,7 +648,7 @@ function renderExactProductCard(
|
||||
<img src="${SUPABASE_ASSETS_BASE}vlajka_srdce.png" width="40" alt="SK" style="display: block; margin-top: -26px; margin-right: -10px; padding: 4px; filter: drop-shadow(1px 0 0 #fefefe) drop-shadow(-1px 0 0 #fefefe) drop-shadow(0 1px 0 #fefefe) drop-shadow(0 -1px 0 #fefefe);">
|
||||
</td>
|
||||
</tr>` : ''}
|
||||
</table>
|
||||
</table>`}
|
||||
${p.unitPrice ? `<div style="font-size: 9px; color: #666666; margin-top: 4px; font-weight: 500; font-family: Arial, sans-serif;">${escapeHtml(p.unitPrice)}</div>` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface ProductItem {
|
||||
};
|
||||
cardBgColor?: string; // HEX napr. "#feeaea" (fantasticky), "#fef0c7" (domace), "#dcb085" (grill)
|
||||
showSlovakFlag?: boolean;
|
||||
customBadgePngUrl?: string; // Vyrenderovaná vlastná PNG cenovka z editora
|
||||
}
|
||||
|
||||
export interface AnnouncementModuleData {
|
||||
|
||||
Reference in new issue
Block a user