diff --git a/src/app/api/brevo/export/route.ts b/src/app/api/brevo/export/route.ts index 76da96e..7848e4e 100644 --- a/src/app/api/brevo/export/route.ts +++ b/src/app/api/brevo/export/route.ts @@ -63,8 +63,8 @@ export async function POST(req: NextRequest) { success: true, updated: true, campaignId: Number(existingCampaignId), - brevoUrl: `https://app.brevo.com/campaigns/email/edit/${existingCampaignId}`, - brevoListingUrl: 'https://app.brevo.com/campaigns/listing', + brevoUrl: 'https://app.brevo.com/campaigns', + brevoListingUrl: 'https://app.brevo.com/campaigns', message: 'Kampaň bola úspešne aktualizovaná v Brevo!' }); } @@ -113,8 +113,8 @@ export async function POST(req: NextRequest) { success: true, updated: false, campaignId: data.id, - brevoUrl: `https://app.brevo.com/campaigns/email/edit/${data.id}`, - brevoListingUrl: 'https://app.brevo.com/campaigns/listing', + brevoUrl: 'https://app.brevo.com/campaigns', + brevoListingUrl: 'https://app.brevo.com/campaigns', message: 'Kampaň bola úspešne vytvorená v Brevo!' }); } catch (error: any) { diff --git a/src/app/api/brevo/verify/route.ts b/src/app/api/brevo/verify/route.ts index 92cf54c..5d9845b 100644 --- a/src/app/api/brevo/verify/route.ts +++ b/src/app/api/brevo/verify/route.ts @@ -51,8 +51,8 @@ export async function GET(req: NextRequest) { name: data.name, status: data.status, subject: data.subject, - brevoUrl: `https://app.brevo.com/campaigns/email/edit/${data.id}`, - brevoListingUrl: 'https://app.brevo.com/campaigns/listing' + brevoUrl: 'https://app.brevo.com/campaigns', + brevoListingUrl: 'https://app.brevo.com/campaigns' }); } catch (err: any) { console.error('Brevo verify error:', err); diff --git a/src/app/page.tsx b/src/app/page.tsx index 8f0a49f..bec7256 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -144,7 +144,7 @@ export default function Home() { modules: enforceAnchors(dbItem.data.modules) }); if (dbItem.brevo_campaign_id) { - setBrevoUrl(`https://app.brevo.com/campaigns/email/edit/${dbItem.brevo_campaign_id}`); + setBrevoUrl('https://app.brevo.com/campaigns'); } } } @@ -522,14 +522,14 @@ export default function Home() { }; // Vybraný produkt pre editor cenoviek - let targetProduct: ProductItem | null = null; + let targetProduct: any = 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) => { + const updatedProducts = (selectedModule.data.products || []).map((p: any) => { if (p.id === targetProductIdForBadge) { return { ...p, @@ -561,6 +561,34 @@ export default function Home() { }); }; + // Hromadná aplikácia cenovky na všetky produkty v celej kampani (všetky sekcie + vernostné zľavy) + const handleApplyBadgeToAll = (badge: BadgeTemplate) => { + setCampaign((prev) => ({ + ...prev, + modules: prev.modules.map((m) => { + if (m.type === 'products_grid' || m.type === 'loyalty_club') { + return { + ...m, + data: { + ...m.data, + products: (m.data.products || []).map((prod: any) => ({ + ...prod, + badgeId: badge.id, + badgeCustomTemplate: badge, + badgeWidth: badge.width ?? prod.badgeWidth ?? 110, + badgeAlign: badge.align ?? prod.badgeAlign ?? 'center', + badgePaddingTop: badge.paddingTop ?? prod.badgePaddingTop ?? 4, + badgePaddingBottom: badge.paddingBottom ?? prod.badgePaddingBottom ?? 4, + customBadgePngUrl: undefined + })) + } + }; + } + return m; + }) + })); + }; + const topCount = campaign.modules.filter((m) => isTopModule(m)).length; const firstBottomIdx = campaign.modules.findIndex((m) => isBottomModule(m)); @@ -722,6 +750,7 @@ export default function Home() { { setTargetProductIdForBadge(pId); @@ -744,10 +773,10 @@ export default function Home() { isOpen={isBadgeEditorOpen} onClose={() => setIsBadgeEditorOpen(false)} onSelectBadge={handleApplyBadgeToProduct} - currentDiscount={targetProduct?.discountText || '25%'} - currentPriceMain={targetProduct?.priceMain || '1'} - currentPriceSub={targetProduct?.priceSub || '99'} - currentUnitPrice={targetProduct?.unitPrice || '1kg = 4,91 €'} + currentDiscount={targetProduct?.discountText || targetProduct?.cardDiscount || '25%'} + currentPriceMain={targetProduct?.priceMain || targetProduct?.cardPriceMain || '1'} + currentPriceSub={targetProduct?.priceSub || targetProduct?.cardPriceSub || '99'} + currentUnitPrice={targetProduct?.unitPrice || targetProduct?.cardUnitPrice || '1kg = 4,91 €'} /> {/* Modál Moje newslettre (Ukladanie, duplikácia, vytváranie a prepínanie) */} @@ -759,7 +788,7 @@ export default function Home() { setCampaign({ ...selected, modules: enforceAnchors(selected.modules) }); setSelectedModuleId(selected.modules[1]?.id || selected.modules[0]?.id || null); if (selected.brevoCampaignId) { - setBrevoUrl(`https://app.brevo.com/campaigns/email/edit/${selected.brevoCampaignId}`); + setBrevoUrl('https://app.brevo.com/campaigns'); } else { setBrevoUrl(null); } diff --git a/src/components/editor/BadgeEditorModal.tsx b/src/components/editor/BadgeEditorModal.tsx index ee0206c..675096f 100644 --- a/src/components/editor/BadgeEditorModal.tsx +++ b/src/components/editor/BadgeEditorModal.tsx @@ -226,20 +226,11 @@ export const BadgeEditorModal: React.FC = ({ } } catch (e) {} - // Doplnenie predpripravených dizajnových šablón (DEFAULT_BADGE_TEMPLATES) - const allBadges = [...loaded]; - const existingIds = new Set(allBadges.map((b) => b.id)); - for (const preset of DEFAULT_BADGE_TEMPLATES) { - if (!existingIds.has(preset.id)) { - allBadges.push(preset); - } - } - - setBadges(allBadges); - if (!activeBadge && allBadges.length > 0) { - setActiveBadge(allBadges[0]); - if (allBadges[0].layersConfig?.[0]) { - setSelectedLayerId(allBadges[0].layersConfig[0].id); + setBadges(loaded); + if (!activeBadge && loaded.length > 0) { + setActiveBadge(loaded[0]); + if (loaded[0].layersConfig?.[0]) { + setSelectedLayerId(loaded[0].layersConfig[0].id); } } }; @@ -452,15 +443,28 @@ export const BadgeEditorModal: React.FC = ({ // Vykreslenie vrstvy v React náhľade s centami ako horný index (sup) a BEZ znaku € const renderLayerVisualContent = (layer: BadgeLayer) => { const raw = layer.text || ''; + const baseFontSize = layer.fontSize || 16; + const subFontSize = layer.indexFontSize ? `${layer.indexFontSize}px` : `${Math.max(10, Math.round(baseFontSize * 0.55))}px`; + const subY = layer.indexOffsetY !== undefined ? `${layer.indexOffsetY}px` : '-10px'; + const subX = layer.indexOffsetX !== undefined ? `${layer.indexOffsetX}px` : '2px'; + if (raw.includes('{cena}')) { const parts = raw.split('{cena}'); return ( {parts[0]} {currentPriceMain} - + {currentPriceSub} - + {parts[1]} ); @@ -471,7 +475,17 @@ export const BadgeEditorModal: React.FC = ({ return ( {parts[0]} - {currentPriceSub} + + {currentPriceSub} + {parts[1]} ); @@ -1101,6 +1115,74 @@ export const BadgeEditorModal: React.FC = ({ + {/* Nastavenie veľkosti a pozície horného indexu (centov) */} + {(activeLayer.text?.includes('{cena}') || activeLayer.text?.includes('{centy}')) && ( +
+
+ + Nastavenie horného indexu (Centov) + + + {activeLayer.indexFontSize ?? Math.max(10, Math.round((activeLayer.fontSize || 16) * 0.55))}px + +
+ + {/* Veľkosť indexu */} +
+
+ Veľkosť písma indexu + + {activeLayer.indexFontSize ?? Math.max(10, Math.round((activeLayer.fontSize || 16) * 0.55))} px + +
+ updateActiveLayer({ indexFontSize: Number(e.target.value) })} + className="w-full accent-blue-600" + /> +
+ + {/* Vertikálny posun (Hore / Dole) */} +
+
+ Vertikálna pozícia (Posun Hore / Dole) + + {activeLayer.indexOffsetY ?? -10} px + +
+ updateActiveLayer({ indexOffsetY: Number(e.target.value) })} + className="w-full accent-blue-600" + /> +
+ + {/* Horizontálna medzera (Vľavo / Vpravo) */} +
+
+ Horizontálna medzera (Vľavo / Vpravo) + + {activeLayer.indexOffsetX ?? 2} px + +
+ updateActiveLayer({ indexOffsetX: Number(e.target.value) })} + className="w-full accent-blue-600" + /> +
+
+ )} +
- {/* Tlačidlo: Aplikovať túto cenovku na všetky produkty v sekcii */} + {/* Tlačidlá aplikácie cenovky */} {p.badgeCustomTemplate && ( - +
+ + {onApplyBadgeToAll && ( + + )} +
)} {/* Rýchle ovládanie veľkosti (Width), zarovnania a paddingu */} @@ -1005,6 +1029,222 @@ export const ConfigPanel: React.FC = ({ Zobraziť slovenskú vlajku / srdce + + {/* Grafická cenovka a šablóna pre vernostné produkty */} +
+
+
+ + Cenovka +
+ +
+ + {/* Výber šablóny */} +
+ + +
+ + {/* Tlačidlá aplikácie cenovky */} + {p.badgeCustomTemplate && ( +
+ + {onApplyBadgeToAll && ( + + )} +
+ )} + + {/* Rýchle ovládanie veľkosti (Width), zarovnania a paddingu pre vernostnú cenovku */} + {p.badgeCustomTemplate && ( +
+ {/* Šírka */} +
+
+ Veľkosť cenovky (Šírka) + + {p.badgeWidth || (p.badgeCustomTemplate ? 120 : 140)} px + +
+ { + const products = [...module.data.products]; + products[pIdx].badgeWidth = Number(e.target.value); + updateData({ products }); + }} + className="w-full accent-red-600 cursor-pointer" + /> +
+ + {/* Zarovnanie */} +
+ + Zarovnanie cenovky + +
+ + + +
+
+ + {/* Padding zhora a zdola */} +
+
+ + { + const products = [...module.data.products]; + products[pIdx].badgePaddingTop = Number(e.target.value); + updateData({ products }); + }} + className="w-full text-xs border border-gray-300 rounded-lg p-1 bg-white font-medium" + /> +
+
+ + { + const products = [...module.data.products]; + products[pIdx].badgePaddingBottom = Number(e.target.value); + updateData({ products }); + }} + className="w-full text-xs border border-gray-300 rounded-lg p-1 bg-white font-medium" + /> +
+
+
+ )} +
))} diff --git a/src/components/editor/SavedCampaignsModal.tsx b/src/components/editor/SavedCampaignsModal.tsx index dd6a737..3522e41 100644 --- a/src/components/editor/SavedCampaignsModal.tsx +++ b/src/components/editor/SavedCampaignsModal.tsx @@ -293,7 +293,7 @@ export const SavedCampaignsModal: React.FC = ({ {camp.brevo_campaign_id && ( = ({
{/* Odkaz na úpravu v Brevo */} - Upraviť v Brevo + Otvoriť v Brevo {/* Tlačidlo na aktualizáciu existujúcej kampane */} @@ -244,17 +244,6 @@ export const TopBar: React.FC = ({ > - - {/* Odkaz na celkový zoznam kampaní */} - - Zoznam -
) : ( /* Kampaň v Brevo nie je overená / neexistuje: Zobrazujeme tlačidlo na prvotné odoslanie */ diff --git a/src/lib/badgeUtils.ts b/src/lib/badgeUtils.ts index 7e4ca82..867ea7b 100644 --- a/src/lib/badgeUtils.ts +++ b/src/lib/badgeUtils.ts @@ -1,159 +1,7 @@ import { BadgeTemplate, BadgeLayer } from '@/types/newsletter'; -export const DEFAULT_BADGE_TEMPLATES: BadgeTemplate[] = [ - { - id: 'badge-preset-yellow-akcia', - name: 'COOP Akcia (Žltá)', - svgRaw: ` - - - `, - width: 110, - align: 'center', - paddingTop: 4, - paddingBottom: 4, - layersConfig: [ - { - id: 'l-akcia-1', - type: 'text', - text: 'AKCIA', - x: 70, - y: 20, - fontSize: 14, - fontWeight: '900', - fontFamily: 'Poppins', - fill: '#ffffff', - align: 'center' - }, - { - id: 'l-akcia-2', - type: 'text', - text: '{cena}', - x: 70, - y: 62, - fontSize: 42, - fontWeight: '900', - fontFamily: 'Arial Narrow', - fill: '#001a33', - align: 'center' - } - ] - }, - { - id: 'badge-preset-red-zlava', - name: 'COOP Zľava (Červená)', - svgRaw: ` - - - `, - width: 110, - align: 'center', - paddingTop: 4, - paddingBottom: 4, - layersConfig: [ - { - id: 'l-zlava-1', - type: 'text', - text: 'ZĽAVA -{zlava}', - x: 70, - y: 19, - fontSize: 13, - fontWeight: '900', - fontFamily: 'Poppins', - fill: '#ffffff', - align: 'center' - }, - { - id: 'l-zlava-2', - type: 'text', - text: '{cena}', - x: 70, - y: 62, - fontSize: 42, - fontWeight: '900', - fontFamily: 'Arial Narrow', - fill: '#ffd100', - align: 'center' - } - ] - }, - { - id: 'badge-preset-blue-supercena', - name: 'Super Cena (Tmavomodrá)', - svgRaw: ` - - - `, - width: 110, - align: 'center', - paddingTop: 4, - paddingBottom: 4, - layersConfig: [ - { - id: 'l-super-1', - type: 'text', - text: 'SUPER CENA', - x: 70, - y: 16, - fontSize: 13, - fontWeight: '900', - fontFamily: 'Poppins', - fill: '#ffffff', - align: 'center' - }, - { - id: 'l-super-2', - type: 'text', - text: '{cena}', - x: 70, - y: 62, - fontSize: 42, - fontWeight: '900', - fontFamily: 'Arial Narrow', - fill: '#ffd100', - align: 'center' - } - ] - }, - { - id: 'badge-preset-green-cerstve', - name: 'Bio & Čerstvé (Zelená)', - svgRaw: ` - - - `, - width: 110, - align: 'center', - paddingTop: 4, - paddingBottom: 4, - layersConfig: [ - { - id: 'l-green-1', - type: 'text', - text: 'ČERSTVÉ', - x: 70, - y: 19, - fontSize: 13, - fontWeight: '900', - fontFamily: 'Poppins', - fill: '#a3e635', - align: 'center' - }, - { - id: 'l-green-2', - type: 'text', - text: '{cena}', - x: 70, - y: 62, - fontSize: 42, - fontWeight: '900', - fontFamily: 'Arial Narrow', - fill: '#ffffff', - align: 'center' - } - ] - } -]; +// Vymazané všetky preddefinované šablóny — v systéme sú výhradne vlastné cenovky vytvorené používateľom +export const DEFAULT_BADGE_TEMPLATES: BadgeTemplate[] = []; export interface BadgeProductData { priceMain: string; @@ -224,7 +72,10 @@ export function renderCleanBadgeSvg( const textLayersSvg = (template.layersConfig || []).map((layer) => { const rawText = layer.text || ''; const fontSize = layer.fontSize || 16; - const subFontSize = Math.max(10, Math.round(fontSize * 0.55)); + const subFontSize = layer.indexFontSize || Math.max(10, Math.round(fontSize * 0.55)); + const subDy = layer.indexOffsetY !== undefined ? `${layer.indexOffsetY}px` : '-0.45em'; + const subDx = layer.indexOffsetX !== undefined ? `${layer.indexOffsetX}px` : '1px'; + const revertDy = layer.indexOffsetY !== undefined ? `${-layer.indexOffsetY}px` : '0.45em'; const fontWeight = layer.fontWeight || '700'; const fill = layer.fill || '#ffffff'; const fontFamily = layer.fontFamily @@ -235,17 +86,21 @@ export function renderCleanBadgeSvg( if (layer.align === 'left') anchor = 'start'; if (layer.align === 'right') anchor = 'end'; - // Formátovanie textu: ak obsahuje {cena}, centy vložíme do ako horný index + // Formátovanie textu: ak obsahuje {cena} alebo {centy}, centy vložíme do ako horný index let contentSvg = ''; if (rawText.includes('{cena}')) { const parts = rawText.split('{cena}'); const before = escapeXml(parts[0] || ''); const after = escapeXml(parts[1] || ''); - contentSvg = `${before}${escapeXml(pMain)}${escapeXml(pSub)}${after}`; + contentSvg = `${before}${escapeXml(pMain)}${escapeXml(pSub)}${after ? `${after}` : ''}`; + } else if (rawText.includes('{centy}')) { + const parts = rawText.split('{centy}'); + const before = escapeXml(parts[0] || ''); + const after = escapeXml(parts[1] || ''); + contentSvg = `${before}${escapeXml(pSub)}${after ? `${after}` : ''}`; } else { let replaced = rawText .replace(/{cela_suma}/g, pMain) - .replace(/{centy}/g, pSub) .replace(/{zlava}/g, discount) .replace(/{jednotkova_cena}/g, unitPrice) .replace(/{mena}/g, '€'); diff --git a/src/lib/emailRenderer.ts b/src/lib/emailRenderer.ts index d42d2ad..b9df4de 100644 --- a/src/lib/emailRenderer.ts +++ b/src/lib/emailRenderer.ts @@ -620,7 +620,7 @@ function renderExactProductCard( - + ${(() => { const bWidth = p.badgeWidth || (p.badgeCustomTemplate ? (p.badgeCustomTemplate.width || 110) : 95); const bAlign = p.badgeAlign || (p.badgeCustomTemplate ? (p.badgeCustomTemplate.align || 'center') : 'center'); @@ -701,7 +701,11 @@ function renderExactProductCard( ` : ''} `; })()} - ${p.unitPrice ? `
${escapeHtml(p.unitPrice)}
` : ''} + + + + + ${p.unitPrice ? `
${escapeHtml(p.unitPrice)}
` : '
 
'} `; @@ -739,69 +743,115 @@ function renderExactLoyaltyCard(p: any, validity: string): string { - - - - - - - ${p.showSlovakFlag ? ` - - - ` : ''} - - - - -
- - - - - - - -
- - - - - - - -
-
Bez karty
-
zľava ${escapeHtml(p.noCardDiscount)}
-
- ${escapeHtml(p.noCardPriceMain)}${escapeHtml(p.noCardPriceSub)} -
-
- - - - - - - -
-
S kartou
-
- zľava - ${escapeHtml(p.cardDiscount)} -
-
- ${escapeHtml(p.cardPriceMain)}${escapeHtml(p.cardPriceSub)} -
-
-
- SK -
-
${escapeHtml(p.noCardUnitPrice)}
-
-
${escapeHtml(p.cardUnitPrice)}
-
+ + ${(() => { + // 1. Ak má vernostný produkt vlastnú dynamickú šablónu cenovky + if (p.badgeCustomTemplate) { + const bWidth = p.badgeWidth || p.badgeCustomTemplate.width || 120; + const bAlign = p.badgeAlign || p.badgeCustomTemplate.align || 'center'; + const padTop = p.badgePaddingTop ?? (p.badgeCustomTemplate.paddingTop ?? 0); + const padBottom = p.badgePaddingBottom ?? (p.badgeCustomTemplate.paddingBottom ?? 0); + + let marginStyle = 'margin: 0 auto;'; + if (bAlign === 'left') { + marginStyle = 'margin: 0 auto 0 0; margin-right: auto; margin-left: 0;'; + } else if (bAlign === 'right') { + marginStyle = 'margin: 0 0 0 auto; margin-left: auto; margin-right: 0;'; + } + + const loyaltyProductData = { + priceMain: p.cardPriceMain || p.priceMain || '0', + priceSub: p.cardPriceSub || p.priceSub || '00', + discountText: p.cardDiscount || p.discountText || '', + unitPrice: p.cardUnitPrice || p.unitPrice || '' + }; + + return ` + + + + + ${p.showSlovakFlag ? ` + + + ` : ''} +
+ ${renderCleanBadgeSvg(p.badgeCustomTemplate, loyaltyProductData, bWidth)} +
+ SK +
`; + } + + // 2. Predvolený Dual Sticker Block (Bez karty / S kartou) 1:1 + return ` + + + + + ${p.showSlovakFlag ? ` + + + ` : ''} + + + + +
+ + + + + + + +
+ + + + + + + +
+
Bez karty
+
zľava ${escapeHtml(p.noCardDiscount)}
+
+ ${escapeHtml(p.noCardPriceMain)}${escapeHtml(p.noCardPriceSub)} +
+
+ + + + + + + +
+
S kartou
+
+ zľava + ${escapeHtml(p.cardDiscount)} +
+
+ ${escapeHtml(p.cardPriceMain)}${escapeHtml(p.cardPriceSub)} +
+
+
+ SK +
+
${escapeHtml(p.noCardUnitPrice)}
+
+
${escapeHtml(p.cardUnitPrice)}
+
`; + })()} + ${p.badgeCustomTemplate ? ` + + + ${p.cardUnitPrice || p.unitPrice ? `
${escapeHtml(p.cardUnitPrice || p.unitPrice)}
` : '
 
'} + + ` : ''} `; } diff --git a/src/types/newsletter.ts b/src/types/newsletter.ts index 74a209c..4b8b4dc 100644 --- a/src/types/newsletter.ts +++ b/src/types/newsletter.ts @@ -156,6 +156,10 @@ export interface BadgeLayer { fontUrl?: string; fill: string; align?: 'left' | 'center' | 'right'; + // Nastavenia horného indexu (centov) + indexFontSize?: number; // Veľkosť horného indexu v px + indexOffsetY?: number; // Vertikálny posun horného indexu v px (posun hore / dole) + indexOffsetX?: number; // Horizontálny posun horného indexu v px (medzera) } export interface BadgeTemplate {