feat: produktove karty v style letaku a zjednoduseny panel produktu
Karta podla sablony coop_nws_009: fotka cez celu kartu, cenovka cez fotku vpravo dole, stitky predajni nad cenovkou, pod tym texty. - v editore sa vizual sklada zivo (HTML/CSS); pri exporte (ZIP/Brevo) sa ulozi ako JPG a do e-mailu ide ako jeden obrazok - funguje rovnako vo vsetkych klientoch; nezmenene karty sa znova nenahravaju - /api/img: proxy obrazkov pre nahlad (inak by sa vizual nedal ulozit) - novy ProductEditor pre bezne aj vernostne produkty: Texty, Fotka (zoom/posun), Cenovka (sablona, velkost, ceny), Dostupnost, Dalsie moznosti; zbalene po produktoch - alt texty kariet s cenou a zlavou, mobilne styly textu, ochrana farieb v tmavom rezime (zelena karta, Apple Mail) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
139e3569d0
commit
25771afc79
9 files changed
+1009
-1756
No files matched your search
@@ -0,0 +1,347 @@
|
||||
// Produktová karta v štýle letáku (vzor coop_nws_009):
|
||||
// hore "vizuál" – fotka produktu cez celú kartu, cenovka cez fotku vpravo dole, štítky predajní nad cenovkou,
|
||||
// dole texty – názov (tučne) + gramáž, jednotková cena, poznámka.
|
||||
//
|
||||
// V editore (mode 'preview') sa vizuál skladá živo z HTML/CSS. Pri exporte (ZIP/Brevo) sa vizuál z náhľadu
|
||||
// uloží ako JPG (src/lib/cardImages.ts) a do e-mailu ide ako jeden obrázok – rovnako ako karta_produkt_*.jpg
|
||||
// v šablóne. Vďaka tomu karta vyzerá rovnako vo všetkých klientoch (Gmail, Outlook, mobil).
|
||||
import { CardProduct, NewsletterModule } from '@/types/newsletter';
|
||||
import { renderCleanBadgeSvg, BadgeProductData } from './badgeUtils';
|
||||
import { SUPABASE_ASSETS_BASE } from './defaultCampaign';
|
||||
import { pickAlt } from './altText';
|
||||
import { escapeHtml, escapeWithBreaks } from './html';
|
||||
|
||||
export type CardVariant = 'standard' | 'loyalty';
|
||||
export type RenderMode = 'preview' | 'export';
|
||||
|
||||
export interface CardContext {
|
||||
variant: CardVariant;
|
||||
mode: RenderMode;
|
||||
cardWidth: number; // nominálna šírka karty v px (2 stĺpce = 294, 1 stĺpec = 600)
|
||||
cardBg: string; // predvolená farba karty v sekcii
|
||||
validity: string; // platnosť zobrazená na karte ('' = skrytá)
|
||||
sectionTitle?: string;
|
||||
// Farby predvolenej (negenerovanej) cenovky bežných produktov
|
||||
defaultBadge: { hdrBg: string; hdrColor: string; prcBg: string; prcColor: string; customText: string | null };
|
||||
}
|
||||
|
||||
export interface StoresOffset {
|
||||
x?: number; // + doprava, − doľava
|
||||
y?: number; // + vyššie nad cenovku, − nižšie (cez horný okraj cenovky)
|
||||
}
|
||||
export const DEFAULT_STORES_OFFSET_Y = 2;
|
||||
|
||||
// Výška vizuálu: pomer ako v šablóne (294 × 245), pri 1 produkte na riadok max 300 px
|
||||
export function cardVisualHeight(cardWidth: number): number {
|
||||
return cardWidth <= 300 ? Math.round((cardWidth * 245) / 294) : 300;
|
||||
}
|
||||
|
||||
// Kontext karty z nastavení sekcie
|
||||
export function cardContextForModule(m: NewsletterModule, mode: RenderMode): CardContext {
|
||||
const variant: CardVariant = m.type === 'loyalty_club' ? 'loyalty' : 'standard';
|
||||
const showValidity = m.data.showCardValidity === true;
|
||||
return {
|
||||
variant,
|
||||
mode,
|
||||
cardWidth: variant === 'standard' && m.data.columns === 1 ? 600 : 294,
|
||||
cardBg: m.data.cardBgColor || (variant === 'loyalty' ? '#e6e1d9' : '#feeaea'),
|
||||
validity: showValidity ? m.data.globalValidity || '' : '',
|
||||
sectionTitle: m.data.sectionTitle,
|
||||
defaultBadge: {
|
||||
hdrBg: m.data.badgeHeaderBg || '#fefefe',
|
||||
hdrColor: m.data.badgeHeaderColor || '#000000',
|
||||
prcBg: m.data.badgePriceBg || '#e2001a',
|
||||
prcColor: m.data.badgePriceColor || '#fefefe',
|
||||
customText: m.data.badgeHeaderCustomText || null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Trieda karty pre ochranu farby v tmavom režime (podľa šablóny)
|
||||
const CARD_COLOR_CLASSES: Record<string, string> = {
|
||||
'#feeaea': 'card-fantasticky',
|
||||
'#fef0c7': 'card-domace',
|
||||
'#dcb085': 'card-grill',
|
||||
'#e3f0e4': 'card-zelena',
|
||||
'#e6e1d9': 'card-vernostna',
|
||||
'#fefefe': 'card-white-bg'
|
||||
};
|
||||
|
||||
// V náhľade idú cudzie obrázky cez /api/img (rovnaký pôvod), aby sa vizuál dal pri exporte uložiť ako obrázok
|
||||
function assetUrl(url: string | undefined, mode: RenderMode): string {
|
||||
if (!url) return '';
|
||||
if (mode === 'preview' && /^https?:\/\//i.test(url)) {
|
||||
return `/api/img?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// Údaje pre premenné v šablóne cenovky
|
||||
function badgeData(p: CardProduct, variant: CardVariant): BadgeProductData {
|
||||
if (variant === 'loyalty') {
|
||||
return {
|
||||
priceMain: p.cardPriceMain || p.priceMain || '0',
|
||||
priceSub: p.cardPriceSub || p.priceSub || '00',
|
||||
discountText: p.cardDiscount || p.discountText || '',
|
||||
unitPrice: p.cardUnitPrice || p.unitPrice || '',
|
||||
cardPriceMain: p.cardPriceMain,
|
||||
cardPriceSub: p.cardPriceSub,
|
||||
cardDiscount: p.cardDiscount
|
||||
};
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
export function badgeWidthFor(p: CardProduct, variant: CardVariant): number {
|
||||
if (p.badgeWidth) return p.badgeWidth;
|
||||
if (p.badgeCustomTemplate) return p.badgeCustomTemplate.width || 110;
|
||||
if (p.customBadgePngUrl) return 110;
|
||||
return variant === 'loyalty' ? 140 : 95;
|
||||
}
|
||||
|
||||
// Samotná cenovka: vlastná PNG > šablóna > predvolená cenovka
|
||||
function renderBadge(p: CardProduct, ctx: CardContext, width: number): string {
|
||||
if (p.customBadgePngUrl) {
|
||||
return `<img src="${escapeHtml(assetUrl(p.customBadgePngUrl, ctx.mode))}" width="${width}" alt="" style="display: block; width: ${width}px; height: auto; border: 0;">`;
|
||||
}
|
||||
if (p.badgeCustomTemplate) {
|
||||
return renderCleanBadgeSvg(p.badgeCustomTemplate, badgeData(p, ctx.variant), width);
|
||||
}
|
||||
if (ctx.variant === 'loyalty') {
|
||||
return renderDualSticker(p);
|
||||
}
|
||||
const b = ctx.defaultBadge;
|
||||
const mainFontSize = Math.max(28, Math.round(width * 0.54));
|
||||
const subFontSize = Math.max(14, Math.round(width * 0.27));
|
||||
return `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="background-color: ${b.hdrBg}; border: 0; border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.18); width: ${width}px; overflow: hidden;">
|
||||
${b.customText ? `
|
||||
<tr>
|
||||
<td bgcolor="${b.hdrBg}" style="padding: 4px 4px; text-align: center; background-color: ${b.hdrBg};">
|
||||
<span style="font-family: 'Arial Narrow', Arial, sans-serif; font-size: 10px; color: ${b.hdrColor}; font-weight: 800; text-transform: uppercase;">${escapeHtml(b.customText)}</span>
|
||||
</td>
|
||||
</tr>` : p.discountText ? `
|
||||
<tr>
|
||||
<td style="padding: 2px 4px; text-align: right; background-color: ${b.hdrBg};">
|
||||
<span style="font-family: 'Arial Narrow', Arial, sans-serif; font-size: 9px; color: ${b.hdrColor}; font-weight: 700; text-transform: lowercase;">${escapeHtml(p.badgeText || 'zľava')}</span>
|
||||
<span style="font-family: 'Arial Narrow', Arial, sans-serif; font-size: 20px; color: ${b.hdrColor}; font-weight: 900; line-height: 1; margin-left: 2px;">${escapeHtml(p.discountText)}</span>
|
||||
</td>
|
||||
</tr>` : ''}
|
||||
<tr>
|
||||
<td class="force-text-white" bgcolor="${b.prcBg}" style="background-color: ${b.prcBg}; padding: 4px 2px 2px 2px; font-family: 'Arial Narrow', Arial, sans-serif; color: ${b.prcColor}; font-weight: 900; line-height: 1; text-align: center; border-radius: 0 0 12px 12px;">
|
||||
<span style="font-size: ${mainFontSize}px;">${escapeHtml(p.priceMain)}</span><sup style="font-size: ${subFontSize}px; vertical-align: top; line-height: 1.2;">${escapeHtml(p.priceSub)}</sup>
|
||||
</td>
|
||||
</tr>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// Predvolená vernostná cenovka (Bez karty / S kartou)
|
||||
function renderDualSticker(p: CardProduct): string {
|
||||
return `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="width: 140px;">
|
||||
<tr>
|
||||
<td width="42%" valign="bottom">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fefefe; border-radius: 10px 0 0 10px; box-shadow: -2px 4px 8px rgba(0,0,0,0.12); overflow: hidden;">
|
||||
<tr>
|
||||
<td 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>
|
||||
<td width="58%" valign="top">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fefefe; 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 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>
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 4px; font-size: 8px; color: #666666; font-family: Arial, sans-serif;">${escapeHtml(p.noCardUnitPrice)}</td>
|
||||
<td align="center" style="padding-top: 4px; font-size: 9px; color: #e2001a; font-weight: 800; font-family: Arial, sans-serif;">${escapeHtml(p.cardUnitPrice)}</td>
|
||||
</tr>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// Štítky predajní (Potraviny / Supermarket / Tempo) pod sebou; výška podľa veľkosti cenovky
|
||||
function renderStores(p: CardProduct, mode: RenderMode, tagHeight: number): string {
|
||||
const tag = (color: string, file: string, alt: string) =>
|
||||
`<div style="text-align: right; line-height: 0;"><img src="${escapeHtml(assetUrl(SUPABASE_ASSETS_BASE + file, mode))}" height="${tagHeight}" alt="${alt}" style="display: inline-block; height: ${tagHeight}px; width: auto; border: 0; background-color: ${color};"></div>`;
|
||||
return [
|
||||
p.stores?.potraviny ? tag('#e2001a', 'stit_potraviny.png', 'Potraviny') : '',
|
||||
p.stores?.supermarket ? tag('#f9890e', 'stit_supermarket.png', 'Supermarket') : '',
|
||||
p.stores?.tempo ? tag('#005239', 'stit_tempo.png', 'Tempo') : ''
|
||||
].join('');
|
||||
}
|
||||
|
||||
// Jednoznačný kľúč vizuálu – pri zmene čohokoľvek, čo ovplyvní obrázok, sa pri exporte vytvorí nový
|
||||
export function cardVisualKey(p: CardProduct, ctx: CardContext): string {
|
||||
const t = p.badgeCustomTemplate;
|
||||
const relevant = [
|
||||
ctx.variant, ctx.cardWidth, p.cardBgColor || ctx.cardBg, ctx.variant === 'standard' ? ctx.defaultBadge : null,
|
||||
p.imageUrl, p.imageScale ?? 1, p.imageOffsetX || 0, p.imageOffsetY || 0,
|
||||
p.customBadgePngUrl, badgeWidthFor(p, ctx.variant),
|
||||
t ? [t.svgRaw, t.layersConfig, t.width] : null,
|
||||
p.priceMain, p.priceSub, p.discountText, p.badgeText, p.unitPrice,
|
||||
p.cardPriceMain, p.cardPriceSub, p.cardDiscount, p.cardUnitPrice,
|
||||
p.noCardPriceMain, p.noCardPriceSub, p.noCardDiscount, p.noCardUnitPrice,
|
||||
p.stores, p.storesOffsetX || 0, p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y
|
||||
];
|
||||
const str = JSON.stringify(relevant);
|
||||
let h1 = 5381;
|
||||
let h2 = 52711;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const c = str.charCodeAt(i);
|
||||
h1 = (h1 * 33) ^ c;
|
||||
h2 = (h2 * 33) ^ c;
|
||||
}
|
||||
return `cv-${(h1 >>> 0).toString(36)}${(h2 >>> 0).toString(36)}`;
|
||||
}
|
||||
|
||||
// Vizuál karty v náhľade: fotka (zoom/posun), cenovka vpravo dole, štítky nad cenovkou
|
||||
function renderVisualPreview(p: CardProduct, ctx: CardContext, key: string): string {
|
||||
const height = cardVisualHeight(ctx.cardWidth);
|
||||
const bg = p.cardBgColor || ctx.cardBg;
|
||||
const bw = badgeWidthFor(p, ctx.variant);
|
||||
const scale = p.imageScale ?? 1;
|
||||
const ox = p.imageOffsetX || 0;
|
||||
const oy = p.imageOffsetY || 0;
|
||||
const sx = Math.round(p.storesOffsetX || 0);
|
||||
const sy = Math.round(p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y);
|
||||
const stores = renderStores(p, ctx.mode, Math.max(10, Math.round(bw * 0.12)));
|
||||
return `
|
||||
<div data-card-visual="${key}" style="position: relative; width: 100%; height: ${height}px; overflow: hidden; background-color: ${bg}; border-radius: 16px 16px 0 0; line-height: normal; font-size: 12px;">
|
||||
${p.imageUrl ? `<img src="${escapeHtml(assetUrl(p.imageUrl, ctx.mode))}" alt="" style="position: absolute; left: 44%; top: 47%; max-width: 86%; max-height: 86%; transform: translate(-50%, -50%) translate(${ox}px, ${oy}px) scale(${scale}); border: 0;">` : ''}
|
||||
<div style="position: absolute; right: 8px; bottom: 8px; width: ${bw}px;">
|
||||
${stores ? `<div style="position: absolute; right: ${-sx}px; bottom: calc(100% + ${sy}px);">${stores}</div>` : ''}
|
||||
${renderBadge(p, ctx, bw)}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Alt text obrázka karty: názov, gramáž a cena (ako v šablóne); ručne zadaný alt má prednosť
|
||||
export function productCardAlt(p: CardProduct, variant: CardVariant): string {
|
||||
const name = [pickAlt(p.title), p.quantity].filter(Boolean).join(' ');
|
||||
const price = (main?: string, sub?: string) => (main ? `${main}${sub ? `,${sub}` : ''} €` : '');
|
||||
const pct = (d?: string) => (d ? d.replace('-', '').replace(/\s*%/, ' %') : '');
|
||||
let priceText = '';
|
||||
if (variant === 'loyalty') {
|
||||
const card = price(p.cardPriceMain, p.cardPriceSub);
|
||||
const noCard = price(p.noCardPriceMain, p.noCardPriceSub);
|
||||
priceText = [
|
||||
card ? `s vernostnou kartou ${card}${p.cardDiscount ? ` (zľava ${pct(p.cardDiscount)})` : ''}` : '',
|
||||
noCard ? `bez vernostnej karty ${noCard}${p.noCardDiscount ? ` (zľava ${pct(p.noCardDiscount)})` : ''}` : ''
|
||||
].filter(Boolean).join(', ');
|
||||
} else {
|
||||
const main = price(p.priceMain, p.priceSub);
|
||||
const card = price(p.cardPriceMain, p.cardPriceSub);
|
||||
priceText = [
|
||||
main ? `${main}${p.discountText ? `, zľava ${pct(p.discountText)}` : ''}` : '',
|
||||
card ? `s kartou ${card}${p.cardDiscount ? ` (zľava ${pct(p.cardDiscount)})` : ''}` : ''
|
||||
].filter(Boolean).join(', ');
|
||||
}
|
||||
return pickAlt(p.imageAlt, [name, priceText].filter(Boolean).join(' – '), 'Produkt');
|
||||
}
|
||||
|
||||
// Vizuál karty v e-maile: uložený obrázok z exportu, inak jednoduchá záloha (fotka + cenovka pod ňou)
|
||||
function renderVisualExport(p: CardProduct, ctx: CardContext, key: string): string {
|
||||
const alt = productCardAlt(p, ctx.variant);
|
||||
const w = ctx.cardWidth;
|
||||
if (p.cardImage?.key === key && p.cardImage?.url) {
|
||||
return `<img src="${escapeHtml(p.cardImage.url)}" width="${w}" alt="${escapeHtml(alt)}" style="display: block; width: 100%; max-width: ${w}px; height: auto; margin: 0 auto; border: 0; border-top-left-radius: 16px; border-top-right-radius: 16px;">`;
|
||||
}
|
||||
const bw = badgeWidthFor(p, ctx.variant);
|
||||
return `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr><td style="padding: 12px 12px 0 12px; text-align: center; line-height: 0;"><img src="${escapeHtml(p.imageUrl)}" width="${Math.min(220, w - 24)}" alt="${escapeHtml(alt)}" style="display: block; width: 100%; max-width: ${Math.min(220, w - 24)}px; height: auto; margin: 0 auto; border: 0;"></td></tr>
|
||||
<tr><td align="right" style="padding: 6px 12px 0 12px; line-height: normal; font-size: 12px;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right" style="width: ${bw}px;"><tr><td>${renderBadge(p, ctx, bw)}</td></tr></table></td></tr>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// Texty pod vizuálom: názov 15px/700 + gramáž 15px/400, jednotková cena 11px #555, poznámka a platnosť 10px #888
|
||||
const TEXT_FONT = "font-family: 'Poppins', Helvetica, Arial, sans-serif;";
|
||||
function renderTexts(p: CardProduct, ctx: CardContext): string {
|
||||
// Pri predvolenej vernostnej cenovke sú jednotkové ceny priamo na cenovke
|
||||
const unitPrice =
|
||||
ctx.variant === 'loyalty'
|
||||
? p.badgeCustomTemplate || p.customBadgePngUrl
|
||||
? p.cardUnitPrice || p.unitPrice
|
||||
: ''
|
||||
: p.unitPrice;
|
||||
const validity = p.validityOverride || ctx.validity;
|
||||
const small = (text: string) =>
|
||||
`<div class="prod-unit force-text-dark" style="margin-top: 2px; ${TEXT_FONT} font-size: 10px; line-height: 1.3; color: #888888;">${escapeHtml(text)}</div>`;
|
||||
return `<h3 class="prod-name force-text-dark" style="margin: 0; ${TEXT_FONT} font-size: 15px; line-height: 1.25; font-weight: 700; color: #1a1a1a;">${escapeWithBreaks(p.title)}${p.quantity ? ` <span style="font-weight: 400;">${escapeHtml(p.quantity)}</span>` : ''}</h3>${
|
||||
unitPrice ? `
|
||||
<div class="prod-unit force-text-dark" style="margin-top: 3px; ${TEXT_FONT} font-size: 11px; line-height: 1.3; color: #555555;">${escapeHtml(unitPrice)}</div>` : ''}${
|
||||
p.note ? small(p.note) : ''}${validity ? small(validity) : ''}`;
|
||||
}
|
||||
|
||||
// Celá karta produktu
|
||||
export function renderProductCard(p: CardProduct, ctx: CardContext): string {
|
||||
const bg = p.cardBgColor || ctx.cardBg;
|
||||
const colorClass = CARD_COLOR_CLASSES[(bg || '').toLowerCase()] || '';
|
||||
const key = cardVisualKey(p, ctx);
|
||||
const visual = ctx.mode === 'preview' ? renderVisualPreview(p, ctx, key) : renderVisualExport(p, ctx, key);
|
||||
return `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%" class="glass-product ${colorClass}" bgcolor="${bg}" style="background-color: ${bg}; border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); overflow: hidden; height: 100%;">
|
||||
<tr>
|
||||
<td style="padding: 0; line-height: 0; font-size: 0; border-top-left-radius: 16px; border-top-right-radius: 16px;">
|
||||
${visual}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="prod-pad" valign="top" style="padding: 2px 14px 12px 14px; text-align: left; height: 100%;">
|
||||
${renderTexts(p, ctx)}
|
||||
</td>
|
||||
</tr>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// Mriežka kariet (1 alebo 2 na riadok) s rovnakou výškou kariet v riadku
|
||||
export function renderProductGrid(products: CardProduct[], ctx: CardContext): string {
|
||||
const cols = ctx.cardWidth > 300 ? 1 : 2;
|
||||
const rows: CardProduct[][] = [];
|
||||
for (let i = 0; i < products.length; i += cols) rows.push(products.slice(i, i + cols));
|
||||
return rows
|
||||
.map((row, rIdx) => {
|
||||
const spacer = rIdx > 0 ? '<tr><td height="12" style="font-size: 12px; line-height: 12px;"> </td></tr>' : '';
|
||||
const cells = row
|
||||
.map(
|
||||
(p, idx) => `
|
||||
<td width="${cols === 1 ? 100 : 50}%" valign="top" height="1" style="${cols === 1 ? '' : idx === 0 ? 'padding-right: 6px;' : 'padding-left: 6px;'} height: 100%;">
|
||||
${renderProductCard(p, ctx)}
|
||||
</td>`
|
||||
)
|
||||
.join('');
|
||||
const filler = cols === 2 && row.length === 1 ? '<td width="50%" style="padding-left: 6px;"></td>' : '';
|
||||
return `${spacer}
|
||||
<tr>
|
||||
<td>
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="1">
|
||||
<tr>${cells}${filler}
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
Reference in new issue
Block a user