feat: vernostne zlavy - pri neparnom pocte je prvy produkt cez cely riadok

Parny pocet: vsetky produkty po 2 v riadku. Neparny: prvy produkt sam cez
celu sirku (vacsi vizual), ostatne po 2. Export obrazkov kariet pouziva
rovnaku sirku karty ako nahlad.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-21 13:03:35 +02:00
1 parent b13a447c64
commit 23f23c8903
3 files changed
+38 -15

No files matched your search

+2 -2
View File
@@ -3,7 +3,7 @@
// a jeho adresa sa zapamätá pri produkte (cardImage). Pri ďalšom exporte sa nezmenené karty nenahrávajú znova. // a jeho adresa sa zapamätá pri produkte (cardImage). Pri ďalšom exporte sa nezmenené karty nenahrávajú znova.
import { toJpeg } from 'html-to-image'; import { toJpeg } from 'html-to-image';
import { CardProduct, NewsletterCampaign } from '@/types/newsletter'; import { CardProduct, NewsletterCampaign } from '@/types/newsletter';
import { cardContextForModule, cardVisualKey, cardVisualHeight } from './productCard'; import { cardContextFor, cardVisualKey, cardVisualHeight } from './productCard';
async function uploadJpeg(dataUrl: string, fileName: string): Promise<string> { async function uploadJpeg(dataUrl: string, fileName: string): Promise<string> {
const blob = await (await fetch(dataUrl)).blob(); const blob = await (await fetch(dataUrl)).blob();
@@ -26,8 +26,8 @@ export async function prepareCardImages(
campaign.modules.forEach((m, mIdx) => { campaign.modules.forEach((m, mIdx) => {
const isCards = m.type === 'products_grid' || (m.type === 'loyalty_club' && m.data?.enabled !== false); const isCards = m.type === 'products_grid' || (m.type === 'loyalty_club' && m.data?.enabled !== false);
if (!isCards) return; if (!isCards) return;
const ctx = cardContextForModule(m, 'preview');
(m.data.products || []).forEach((p: CardProduct, pIdx: number) => { (m.data.products || []).forEach((p: CardProduct, pIdx: number) => {
const ctx = cardContextFor(m, pIdx, 'preview');
const key = cardVisualKey(p, ctx); const key = cardVisualKey(p, ctx);
if (p.cardImage?.key !== key) { if (p.cardImage?.key !== key) {
jobs.push({ mIdx, pIdx, key, bg: p.cardBgColor || ctx.cardBg, width: ctx.cardWidth }); jobs.push({ mIdx, pIdx, key, bg: p.cardBgColor || ctx.cardBg, width: ctx.cardWidth });
+3 -3
View File
@@ -2,7 +2,7 @@ import { NewsletterCampaign } from '@/types/newsletter';
import { SUPABASE_ASSETS_BASE } from './defaultCampaign'; import { SUPABASE_ASSETS_BASE } from './defaultCampaign';
import { pickAlt } from './altText'; import { pickAlt } from './altText';
import { escapeHtml, escapeWithBreaks } from './html'; import { escapeHtml, escapeWithBreaks } from './html';
import { RenderMode, cardContextForModule, renderProductGrid } from './productCard'; import { RenderMode, renderProductGrid } from './productCard';
// E-mail na export (ZIP/Brevo): produktové karty použijú obrázky pripravené cez prepareCardImages() // E-mail na export (ZIP/Brevo): produktové karty použijú obrázky pripravené cez prepareCardImages()
export function renderEmailHtml(campaign: NewsletterCampaign): string { export function renderEmailHtml(campaign: NewsletterCampaign): string {
@@ -392,7 +392,7 @@ function renderModule(m: any, campaign: NewsletterCampaign, mode: RenderMode = '
<h2 class="force-text-white" style="margin: 0; font-size: 24px; color: #fefefe; font-weight: 900; text-transform: uppercase; text-shadow: 0px 2px 5px rgba(0,0,0,0.5);">${escapeWithBreaks(m.data.sectionTitle)}</h2> <h2 class="force-text-white" style="margin: 0; font-size: 24px; color: #fefefe; font-weight: 900; text-transform: uppercase; text-shadow: 0px 2px 5px rgba(0,0,0,0.5);">${escapeWithBreaks(m.data.sectionTitle)}</h2>
</td> </td>
</tr>` : ''} </tr>` : ''}
${renderProductGrid(m.data.products || [], cardContextForModule(m, mode))} ${renderProductGrid(m, mode)}
</table> </table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"><tr><td height="12" style="font-size: 12px; line-height: 12px;">&nbsp;</td></tr></table>`; <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"><tr><td height="12" style="font-size: 12px; line-height: 12px;">&nbsp;</td></tr></table>`;
@@ -423,7 +423,7 @@ function renderModule(m: any, campaign: NewsletterCampaign, mode: RenderMode = '
</td> </td>
</tr> </tr>
<!-- Produktové karty --> <!-- Produktové karty -->
${renderProductGrid(m.data.products || [], cardContextForModule(m, mode))} ${renderProductGrid(m, mode)}
</table> </table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"><tr><td height="25" style="font-size: 25px; line-height: 25px;">&nbsp;</td></tr></table>`; <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"><tr><td height="25" style="font-size: 25px; line-height: 25px;">&nbsp;</td></tr></table>`;
+33 -10
View File
@@ -372,23 +372,46 @@ export function renderProductCard(p: CardProduct, ctx: CardContext): string {
</table>`; </table>`;
} }
// Mriežka kariet (1 alebo 2 na riadok) s rovnakou výškou kariet v riadku // Kontext konkrétnej karty: vo vernostných zľavách pri nepárnom počte je prvý produkt cez celý riadok
export function renderProductGrid(products: CardProduct[], ctx: CardContext): string { export function cardContextFor(m: NewsletterModule, index: number, mode: RenderMode): CardContext {
const cols = ctx.cardWidth > 300 ? 1 : 2; const base = cardContextForModule(m, mode);
const rows: CardProduct[][] = []; const count = (m.data.products || []).length;
for (let i = 0; i < products.length; i += cols) rows.push(products.slice(i, i + cols)); if (base.variant === 'loyalty' && count % 2 === 1 && index === 0) return { ...base, cardWidth: 600 };
return rows return base;
}
// Riadky mriežky (indexy produktov): 1 na riadok, alebo po 2;
// vernostné zľavy s nepárnym počtom: prvý produkt sám cez celý riadok, ostatné po 2
function gridRows(m: NewsletterModule, count: number, oneColumn: boolean): number[][] {
const idx = Array.from({ length: count }, (_, i) => i);
if (oneColumn) return idx.map((i) => [i]);
const rows: number[][] = [];
let start = 0;
if (m.type === 'loyalty_club' && count % 2 === 1) {
rows.push([0]);
start = 1;
}
for (let i = start; i < count; i += 2) rows.push(idx.slice(i, i + 2));
return rows;
}
// Mriežka kariet s rovnakou výškou kariet v riadku
export function renderProductGrid(m: NewsletterModule, mode: RenderMode): string {
const products: CardProduct[] = m.data.products || [];
const oneColumn = cardContextForModule(m, mode).cardWidth > 300;
return gridRows(m, products.length, oneColumn)
.map((row, rIdx) => { .map((row, rIdx) => {
const spacer = rIdx > 0 ? '<tr><td height="12" style="font-size: 12px; line-height: 12px;">&nbsp;</td></tr>' : ''; const spacer = rIdx > 0 ? '<tr><td height="12" style="font-size: 12px; line-height: 12px;">&nbsp;</td></tr>' : '';
const cells = row const cells = row
.map( .map(
(p, idx) => ` (i, idx) => `
<td width="${cols === 1 ? 100 : 50}%" valign="top" height="1" style="${cols === 1 ? '' : idx === 0 ? 'padding-right: 6px;' : 'padding-left: 6px;'} height: 100%;"> <td width="${row.length === 1 ? 100 : 50}%" valign="top" height="1" style="${row.length === 1 ? '' : idx === 0 ? 'padding-right: 6px;' : 'padding-left: 6px;'} height: 100%;">
${renderProductCard(p, ctx)} ${renderProductCard(products[i], cardContextFor(m, i, mode))}
</td>` </td>`
) )
.join(''); .join('');
const filler = cols === 2 && row.length === 1 ? '<td width="50%" style="padding-left: 6px;"></td>' : ''; // posledný riadok s 1 produktom v 2-stĺpcovej mriežke (bežné sekcie) – produkt ostane v ľavej polovici
const filler = !oneColumn && row.length === 1 && cardContextFor(m, row[0], mode).cardWidth <= 300 ? '<td width="50%" style="padding-left: 6px;"></td>' : '';
return `${spacer} return `${spacer}
<tr> <tr>
<td> <td>