From 23f23c890399aa59eaebe010e0d55f1dd8e48780 Mon Sep 17 00:00:00 2001 From: Jandl-hostacny Date: Wed, 21 Oct 2026 13:03:35 +0200 Subject: [PATCH] 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 --- src/lib/cardImages.ts | 4 ++-- src/lib/emailRenderer.ts | 6 +++--- src/lib/productCard.ts | 43 ++++++++++++++++++++++++++++++---------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/lib/cardImages.ts b/src/lib/cardImages.ts index 51e8391..7beecf8 100644 --- a/src/lib/cardImages.ts +++ b/src/lib/cardImages.ts @@ -3,7 +3,7 @@ // 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 { 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 { const blob = await (await fetch(dataUrl)).blob(); @@ -26,8 +26,8 @@ export async function prepareCardImages( campaign.modules.forEach((m, mIdx) => { const isCards = m.type === 'products_grid' || (m.type === 'loyalty_club' && m.data?.enabled !== false); if (!isCards) return; - const ctx = cardContextForModule(m, 'preview'); (m.data.products || []).forEach((p: CardProduct, pIdx: number) => { + const ctx = cardContextFor(m, pIdx, 'preview'); const key = cardVisualKey(p, ctx); if (p.cardImage?.key !== key) { jobs.push({ mIdx, pIdx, key, bg: p.cardBgColor || ctx.cardBg, width: ctx.cardWidth }); diff --git a/src/lib/emailRenderer.ts b/src/lib/emailRenderer.ts index 2397bb8..bd680ba 100644 --- a/src/lib/emailRenderer.ts +++ b/src/lib/emailRenderer.ts @@ -2,7 +2,7 @@ import { NewsletterCampaign } from '@/types/newsletter'; import { SUPABASE_ASSETS_BASE } from './defaultCampaign'; import { pickAlt } from './altText'; 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() export function renderEmailHtml(campaign: NewsletterCampaign): string { @@ -392,7 +392,7 @@ function renderModule(m: any, campaign: NewsletterCampaign, mode: RenderMode = '

${escapeWithBreaks(m.data.sectionTitle)}

` : ''} - ${renderProductGrid(m.data.products || [], cardContextForModule(m, mode))} + ${renderProductGrid(m, mode)}
 
`; @@ -423,7 +423,7 @@ function renderModule(m: any, campaign: NewsletterCampaign, mode: RenderMode = ' - ${renderProductGrid(m.data.products || [], cardContextForModule(m, mode))} + ${renderProductGrid(m, mode)}
 
`; diff --git a/src/lib/productCard.ts b/src/lib/productCard.ts index a22e197..38137dc 100644 --- a/src/lib/productCard.ts +++ b/src/lib/productCard.ts @@ -372,23 +372,46 @@ export function renderProductCard(p: CardProduct, ctx: CardContext): string { `; } -// 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 +// Kontext konkrétnej karty: vo vernostných zľavách pri nepárnom počte je prvý produkt cez celý riadok +export function cardContextFor(m: NewsletterModule, index: number, mode: RenderMode): CardContext { + const base = cardContextForModule(m, mode); + const count = (m.data.products || []).length; + if (base.variant === 'loyalty' && count % 2 === 1 && index === 0) return { ...base, cardWidth: 600 }; + 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) => { const spacer = rIdx > 0 ? ' ' : ''; const cells = row .map( - (p, idx) => ` - - ${renderProductCard(p, ctx)} + (i, idx) => ` + + ${renderProductCard(products[i], cardContextFor(m, i, mode))} ` ) .join(''); - const filler = cols === 2 && row.length === 1 ? '' : ''; + // 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 ? '' : ''; return `${spacer}