From 3dc57781555c9e7dc7b6057732248db9c0941e08 Mon Sep 17 00:00:00 2001 From: Jandl-hostacny Date: Thu, 24 Sep 2026 11:22:23 +0200 Subject: [PATCH] Fix module canvas backgrounds, clear dummy badges, add SVG upload, new template creation, and {jednotkova_cena} variable --- src/app/page.tsx | 10 +- src/components/editor/BadgeEditorModal.tsx | 687 ++++++++++++++------- src/lib/emailRenderer.ts | 4 + 3 files changed, 463 insertions(+), 238 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index a9f6ad1..ee21eb1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react'; import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter'; 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 { TopBar } from '@/components/editor/TopBar'; import { ConfigPanel } from '@/components/editor/ConfigPanel'; @@ -409,13 +409,10 @@ export default function Home() { )} - {/* Vykreslenie modulu bez vnútorných medzier navyše */} + {/* Vykreslenie čistého modulu bez obaľovacieho gradientu na každom bloku */}
@@ -454,6 +451,7 @@ export default function Home() { currentDiscount={targetProduct?.discountText || '25%'} currentPriceMain={targetProduct?.priceMain || '1'} currentPriceSub={targetProduct?.priceSub || '99'} + currentUnitPrice={targetProduct?.unitPrice || '1kg = 4,91 €'} />
); diff --git a/src/components/editor/BadgeEditorModal.tsx b/src/components/editor/BadgeEditorModal.tsx index 2445122..27df8fd 100644 --- a/src/components/editor/BadgeEditorModal.tsx +++ b/src/components/editor/BadgeEditorModal.tsx @@ -1,10 +1,10 @@ 'use client'; -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } 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'; +import { Tag, Plus, Check, Trash2, X, Upload, Eye, FileCode } from 'lucide-react'; interface BadgeEditorModalProps { isOpen: boolean; @@ -13,6 +13,7 @@ interface BadgeEditorModalProps { currentDiscount?: string; currentPriceMain?: string; currentPriceSub?: string; + currentUnitPrice?: string; } export const BadgeEditorModal: React.FC = ({ @@ -21,79 +22,133 @@ export const BadgeEditorModal: React.FC = ({ onSelectBadge, currentDiscount = '25%', currentPriceMain = '1', - currentPriceSub = '99' + currentPriceSub = '99', + currentUnitPrice = '1kg = 4,91 €' }) => { - const [badges, setBadges] = useState([ - { - id: 'badge-akcia', - name: 'Klasik Žltá AKCIA', - svgRaw: ` - - - `, - 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: ` - - - - `, - 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: ` - - `, - 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: ` - - `, - 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(badges[0]); - const [selectedLayerId, setSelectedLayerId] = useState('2'); + const [badges, setBadges] = useState([]); + const [activeBadge, setActiveBadge] = useState(null); + const [selectedLayerId, setSelectedLayerId] = useState(null); const previewRef = useRef(null); 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; - 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: ` + + `, + 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) => { + 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(' l.id === selectedLayerId) || + activeBadge?.layersConfig?.[0] || + null; const updateActiveLayer = (updates: Partial) => { + if (!activeBadge || !activeLayer) return; const updatedLayers = activeBadge.layersConfig.map((l) => l.id === activeLayer.id ? { ...l, ...updates } : l ); setActiveBadge({ ...activeBadge, layersConfig: updatedLayers }); }; - const addTextLayer = () => { + const addTextLayer = (presetText: string = 'Text') => { + if (!activeBadge) return; const newL: BadgeLayer = { id: `l-${Date.now()}`, type: 'text', - text: 'Nový text', + text: presetText, x: 70, y: 50, fontSize: 16, @@ -109,20 +164,62 @@ export const BadgeEditorModal: React.FC = ({ }; const removeLayer = (id: string) => { - if (activeBadge.layersConfig.length <= 1) return; + if (!activeBadge || 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 deleteBadgeTemplate = async (badgeId: string) => { + 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); }; + // 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 () => { + if (!activeBadge) return; if (!previewRef.current) { onSelectBadge(activeBadge); onClose(); @@ -131,7 +228,6 @@ export const BadgeEditorModal: React.FC = ({ setIsExportingPng(true); try { - // SVG -> PNG render cez HTML5 Canvas/toPng const dataUrl = await toPng(previewRef.current, { backgroundColor: 'transparent', pixelRatio: 2 @@ -148,214 +244,341 @@ export const BadgeEditorModal: React.FC = ({ }; return ( -
-
+
+
{/* Header */}
-
-
- +
+
+

- Editor grafických cenoviek (SVG → PNG Pipeline) + Editor a generátor grafických cenoviek (SVG → PNG)

- 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.

-
- {/* Content */} + {/* Content Body */}
- {/* Ľavý stĺpec: Šablóny */} + {/* 1. Stĺpec: Šablóny a Vytvorenie novej */}
- - Katalóg cenoviek - -
- {badges.map((b) => ( - - ))} -
-
- - {/* Stredný stĺpec: Interaktívny Canvas / Náhľad */} -
- - Živý vektorový náhľad - - - {/* Plátno cenovky */} -
-
- - {/* Textové vrstvy nad SVG podkladom */} - {activeBadge.layersConfig.map((layer) => { - const isSelected = layer.id === selectedLayerId; - return ( -
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 || '')} -
- ); - })} -
- -

- Dostupné premenné: {`{zlava}`} (hodnota: {currentDiscount}), {`{cena}`} -

-
- - {/* Pravý stĺpec: Konfigurácia vrstiev */} -
-
- - Vrstvy textov ({activeBadge.layersConfig.length}) +
+ + Vaše šablóny ({badges.length})
- {/* Zoznam vrstiev */} -
- {activeBadge.layersConfig.map((l, idx) => ( + {badges.length === 0 ? ( +
+ +

Zatiaľ nemáte žiadne šablóny cenoviek.

- ))} +
+ ) : ( +
+ {badges.map((b) => ( +
{ + setActiveBadge(b); + if (b.layersConfig?.[0]) setSelectedLayerId(b.layersConfig[0].id); + }} + className={`w-full text-left p-3 rounded-2xl border transition-all cursor-pointer relative group ${ + activeBadge?.id === b.id + ? 'bg-white border-[#e2001a] shadow-md ring-2 ring-red-500/10' + : 'bg-white/80 border-gray-200 hover:border-gray-300' + }`} + > +
+

{b.name}

+ +
+
+
+ ))} +
+ )} +
+ + {/* 2. Stĺpec: Vektorový Canvas & Náhľad */} +
+
+ + Náhľad cenovky +
- {/* Nastavenie aktívnej vrstvy */} - {activeLayer && ( -
+ {activeBadge ? ( +
+ {/* Vektorové plátno pre export do PNG */} +
+ {/* SVG Podklad */} +
+ + {/* Dynamické textové vrstvy nad SVG podkladom */} + {activeBadge.layersConfig.map((layer) => { + const isSelected = layer.id === selectedLayerId; + return ( +
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.5 py-0.5 rounded transition-all ${ + 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 || '')} +
+ ); + })} +
+ + {/* Dostupné premenné */} +
+ Dostupné dynamické premenné: +
+ + {'{zlava}'} → {currentDiscount} + + + {'{cena}'} → {currentPriceMain},{currentPriceSub} € + + + {'{jednotkova_cena}'} → {currentUnitPrice} + +
+
+
+ ) : ( +

Vyberte alebo vytvorte novú šablónu cenovky.

+ )} +
+ + {/* 3. Stĺpec: Konfigurácia SVG podkladu a Vrstiev */} +
+ {activeBadge ? ( + <> + {/* Názov šablóny */}
- + updateActiveLayer({ text: e.target.value })} - className="w-full text-sm border border-gray-300 rounded-xl p-2.5 font-medium" + 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" />
-
-
- - updateActiveLayer({ fontSize: Number(e.target.value) })} - className="w-full text-sm border border-gray-300 rounded-xl p-2" - /> + {/* Upload vlastného SVG podkladu */} +
+ + +

+ Nahrajte ľubovoľný tvar: hviezdu, bublinu, štítok alebo percento. +

+
+ + {/* Rýchle pridanie premenných ako vrstvy */} +
+ + Pridať vrstvu s premennou + +
+ + + +
-
- -
- updateActiveLayer({ fill: e.target.value })} - className="w-8 h-8 rounded-lg cursor-pointer border-0" - /> - {activeLayer.fill} +
+ + {/* Zoznam vrstiev */} +
+ + Vrstvy ({activeBadge.layersConfig.length}) + +
+ {activeBadge.layersConfig.map((l, idx) => ( + + ))} +
+ + {/* Nastavenie aktívnej vrstvy */} + {activeLayer && ( +
+
+ + updateActiveLayer({ text: e.target.value })} + className="w-full text-xs font-bold border border-gray-300 rounded-lg p-2 bg-white" + /> +
+ +
+
+ + updateActiveLayer({ fontSize: Number(e.target.value) })} + className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white" + /> +
+
+ +
+ updateActiveLayer({ fill: e.target.value })} + className="w-7 h-7 rounded cursor-pointer border-0" + /> + {activeLayer.fill} +
+
+
+ +
+
+ + updateActiveLayer({ x: Number(e.target.value) })} + className="w-full" + /> +
+
+ + updateActiveLayer({ y: Number(e.target.value) })} + className="w-full" + /> +
+
+ +
+ +
-
-
- -
-
- - updateActiveLayer({ x: Number(e.target.value) })} - className="w-full" - /> - {activeLayer.x} px -
-
- - updateActiveLayer({ y: Number(e.target.value) })} - className="w-full" - /> - {activeLayer.y} px -
+ )}
+ {/* Uloženie šablóny do databázy */}
-
- )} + + ) : null}
@@ -366,8 +589,8 @@ export const BadgeEditorModal: React.FC = ({