Feat: adjustable superscript index, loyalty badges, centered unit price, global badge apply, brevo url fix

This commit is contained in:
Jakub_Hostacny committed 2026-09-24 12:33:39 +02:00
1 parent 245e3f8471
commit 4e4e5e5af3
10 files changed
+539 -290

No files matched your search

+13 -158
View File
@@ -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: `<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"/>
<rect x="15" y="8" width="110" height="24" rx="6" fill="#e2001a"/>
</svg>`,
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: `<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="#e2001a"/>
<rect x="12" y="8" width="116" height="22" rx="5" fill="#ffffff" fill-opacity="0.2"/>
</svg>`,
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: `<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="#001a33"/>
<rect x="0" y="0" width="140" height="30" rx="14" fill="#003399"/>
</svg>`,
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: `<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="#005239"/>
<rect x="15" y="8" width="110" height="22" rx="6" fill="#003d2b"/>
</svg>`,
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 <tspan> ako horný index
// Formátovanie textu: ak obsahuje {cena} alebo {centy}, centy vložíme do <tspan> 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)}<tspan font-size="${subFontSize}px" dy="-0.45em" font-weight="${fontWeight}">${escapeXml(pSub)}</tspan><tspan dy="0.45em">${after}</tspan>`;
contentSvg = `${before}${escapeXml(pMain)}<tspan font-size="${subFontSize}px" dx="${subDx}" dy="${subDy}" font-weight="${fontWeight}">${escapeXml(pSub)}</tspan>${after ? `<tspan dy="${revertDy}">${after}</tspan>` : ''}`;
} else if (rawText.includes('{centy}')) {
const parts = rawText.split('{centy}');
const before = escapeXml(parts[0] || '');
const after = escapeXml(parts[1] || '');
contentSvg = `${before}<tspan font-size="${subFontSize}px" dx="${subDx}" dy="${subDy}" font-weight="${fontWeight}">${escapeXml(pSub)}</tspan>${after ? `<tspan dy="${revertDy}">${after}</tspan>` : ''}`;
} 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, '€');
+113 -63
View File
@@ -620,7 +620,7 @@ function renderExactProductCard(
</td>
</tr>
<tr height="1">
<td style="padding: 0 8px 12px 8px; text-align: center;" valign="bottom">
<td style="padding: 0 8px 4px 8px; text-align: center;" valign="bottom">
${(() => {
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(
</tr>` : ''}
</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>
<tr>
<td align="center" style="padding: 2px 8px 12px 8px; text-align: center;">
${p.unitPrice ? `<div style="font-size: 9px; color: #666666; font-weight: 500; font-family: Arial, sans-serif; text-align: center;">${escapeHtml(p.unitPrice)}</div>` : '<div style="font-size: 9px; line-height: 9px; height: 9px;">&nbsp;</div>'}
</td>
</tr>
</table>`;
@@ -739,69 +743,115 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
</td>
</tr>
<tr height="1">
<td style="padding: 0 8px 12px 8px; text-align: center;" valign="bottom">
<!-- Dual Sticker Block (Bez karty / S kartou) 1:1 -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: 0 auto; width: 140px;">
<tr>
<td colspan="2">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="width: 140px; margin: 0 auto;">
<tr>
<!-- Bez karty -->
<td width="42%" valign="bottom" style="padding-right: 0;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fefefe; border: 0; border-radius: 10px 0 0 10px; box-shadow: -2px 4px 8px rgba(0,0,0,0.12); overflow: hidden;">
<tr>
<td class="force-bg-red" bgcolor="#e2001a" style="background-color: #e2001a; padding: 4px 2px; text-align: center;">
<div style="font-size: 6px; color: #fefefe; font-weight: 700; text-transform: uppercase; line-height: 1; margin-bottom: 1px;">Bez karty</div>
<div style="font-family: 'Arial Narrow', Arial, sans-serif; font-size: 8px; color: #fefefe; font-weight: 900; line-height: 1;">zľava ${escapeHtml(p.noCardDiscount)}</div>
</td>
</tr>
<tr>
<td style="padding: 2px; text-align: center; color: #000000; font-family: 'Arial Narrow', Arial, sans-serif; font-weight: 900; line-height: 1;">
<span style="font-size: 20px;">${escapeHtml(p.noCardPriceMain)}</span><sup style="font-size: 10px; vertical-align: top; line-height: 1.2;">${escapeHtml(p.noCardPriceSub)}</sup>
</td>
</tr>
</table>
</td>
<!-- S kartou -->
<td width="58%" valign="top">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fefefe; border: 0; border-radius: 0 10px 10px 0; box-shadow: 4px 4px 12px rgba(0,0,0,0.15); overflow: hidden;">
<tr>
<td style="background-color: #fefefe; padding: 5px 4px 2px 4px; text-align: right;">
<div style="font-size: 7px; color: #e2001a; font-weight: 800; text-transform: uppercase; line-height: 1; text-align: center; margin-bottom: 1px;">S kartou</div>
<div style="text-align: right; line-height: 1;">
<span style="font-family: 'Arial Narrow', Arial, sans-serif; font-size: 8px; color: #000000; font-weight: 700; text-transform: lowercase;">zľava</span>
<span style="font-family: 'Arial Narrow', Arial, sans-serif; font-size: 18px; color: #000000; font-weight: 900; margin-left: 1px;">${escapeHtml(p.cardDiscount)}</span>
</div>
</td>
</tr>
<tr>
<td class="force-bg-red force-text-white" bgcolor="#e2001a" style="background-color: #e2001a; padding: 3px 2px; text-align: center; color: #fefefe; font-family: 'Arial Narrow', Arial, sans-serif; font-weight: 900; line-height: 1; border-radius: 0 0 10px 0;">
<span style="font-size: 48px;">${escapeHtml(p.cardPriceMain)}</span><sup style="font-size: 24px; vertical-align: top; line-height: 1.2;">${escapeHtml(p.cardPriceSub)}</sup>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
${p.showSlovakFlag ? `
<tr>
<td colspan="2" align="right" style="line-height: 0; padding: 0;">
<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>` : ''}
<tr>
<td width="42%" align="center" style="padding-top: 6px;">
<div style="font-size: 8px; color: #666666; opacity: 0.7; font-weight: 500; font-family: Arial, sans-serif;">${escapeHtml(p.noCardUnitPrice)}</div>
</td>
<td width="58%" align="center" style="padding-top: 6px;">
<div style="font-size: 9px; color: #e2001a; font-weight: 800; font-family: Arial, sans-serif;">${escapeHtml(p.cardUnitPrice)}</div>
</td>
</tr>
</table>
<td style="padding: 0 8px ${p.badgeCustomTemplate ? '4px' : '12px'} 8px; text-align: center;" valign="bottom">
${(() => {
// 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 `
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="${bAlign}" style="${marginStyle} width: ${bWidth}px; padding-top: ${padTop}px; padding-bottom: ${padBottom}px;">
<tr>
<td align="${bAlign}">
${renderCleanBadgeSvg(p.badgeCustomTemplate, loyaltyProductData, bWidth)}
</td>
</tr>
${p.showSlovakFlag ? `
<tr>
<td align="right" style="line-height: 0; padding: 0;">
<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>`;
}
// 2. Predvolený Dual Sticker Block (Bez karty / S kartou) 1:1
return `
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: 0 auto; width: 140px;">
<tr>
<td colspan="2">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="width: 140px; margin: 0 auto;">
<tr>
<!-- Bez karty -->
<td width="42%" valign="bottom" style="padding-right: 0;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fefefe; border: 0; border-radius: 10px 0 0 10px; box-shadow: -2px 4px 8px rgba(0,0,0,0.12); overflow: hidden;">
<tr>
<td class="force-bg-red" bgcolor="#e2001a" style="background-color: #e2001a; padding: 4px 2px; text-align: center;">
<div style="font-size: 6px; color: #fefefe; font-weight: 700; text-transform: uppercase; line-height: 1; margin-bottom: 1px;">Bez karty</div>
<div style="font-family: 'Arial Narrow', Arial, sans-serif; font-size: 8px; color: #fefefe; font-weight: 900; line-height: 1;">zľava ${escapeHtml(p.noCardDiscount)}</div>
</td>
</tr>
<tr>
<td style="padding: 2px; text-align: center; color: #000000; font-family: 'Arial Narrow', Arial, sans-serif; font-weight: 900; line-height: 1;">
<span style="font-size: 20px;">${escapeHtml(p.noCardPriceMain)}</span><sup style="font-size: 10px; vertical-align: top; line-height: 1.2;">${escapeHtml(p.noCardPriceSub)}</sup>
</td>
</tr>
</table>
</td>
<!-- S kartou -->
<td width="58%" valign="top">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fefefe; border: 0; border-radius: 0 10px 10px 0; box-shadow: 4px 4px 12px rgba(0,0,0,0.15); overflow: hidden;">
<tr>
<td style="background-color: #fefefe; padding: 5px 4px 2px 4px; text-align: right;">
<div style="font-size: 7px; color: #e2001a; font-weight: 800; text-transform: uppercase; line-height: 1; text-align: center; margin-bottom: 1px;">S kartou</div>
<div style="text-align: right; line-height: 1;">
<span style="font-family: 'Arial Narrow', Arial, sans-serif; font-size: 8px; color: #000000; font-weight: 700; text-transform: lowercase;">zľava</span>
<span style="font-family: 'Arial Narrow', Arial, sans-serif; font-size: 18px; color: #000000; font-weight: 900; margin-left: 1px;">${escapeHtml(p.cardDiscount)}</span>
</div>
</td>
</tr>
<tr>
<td class="force-bg-red force-text-white" bgcolor="#e2001a" style="background-color: #e2001a; padding: 3px 2px; text-align: center; color: #fefefe; font-family: 'Arial Narrow', Arial, sans-serif; font-weight: 900; line-height: 1; border-radius: 0 0 10px 0;">
<span style="font-size: 48px;">${escapeHtml(p.cardPriceMain)}</span><sup style="font-size: 24px; vertical-align: top; line-height: 1.2;">${escapeHtml(p.cardPriceSub)}</sup>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
${p.showSlovakFlag ? `
<tr>
<td colspan="2" align="right" style="line-height: 0; padding: 0;">
<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>` : ''}
<tr>
<td width="42%" align="center" style="padding-top: 6px;">
<div style="font-size: 8px; color: #666666; opacity: 0.7; font-weight: 500; font-family: Arial, sans-serif;">${escapeHtml(p.noCardUnitPrice)}</div>
</td>
<td width="58%" align="center" style="padding-top: 6px;">
<div style="font-size: 9px; color: #e2001a; font-weight: 800; font-family: Arial, sans-serif;">${escapeHtml(p.cardUnitPrice)}</div>
</td>
</tr>
</table>`;
})()}
</td>
</tr>
${p.badgeCustomTemplate ? `
<tr>
<td align="center" style="padding: 2px 8px 12px 8px; text-align: center;">
${p.cardUnitPrice || p.unitPrice ? `<div style="font-size: 9px; color: #666666; font-weight: 500; font-family: Arial, sans-serif; text-align: center;">${escapeHtml(p.cardUnitPrice || p.unitPrice)}</div>` : '<div style="font-size: 9px; line-height: 9px; height: 9px;">&nbsp;</div>'}
</td>
</tr>` : ''}
</table>`;
}