feat: uprava produktovej fotky - zoom, posun a orezanie

- posuvniky Velkost / Vodorovne / Zvisle pri produkte aj vernostnom produkte
- upravena fotka sa vykresli ako pozadie bunky (background-size/position,
  funguje aj v Gmaile), co presahuje ramik sa oreze
- pri cenovke vedla obrazka je fotka pozadim celeho riadku a da sa
  zasunut pod cenovku; Outlook (mso) dostane povodny obrazok
- neupravene produkty sa vykresluju presne ako doteraz

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-19 10:36:30 +02:00
1 parent d72293a2e2
commit 9c2fb60fd6
3 files changed
+188 -13

No files matched your search

+98 -13
View File
@@ -337,7 +337,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 12px;">
<tr>
<td width="100%" valign="top">
${renderExactProductCard(p, globalValidity, cardBg, cardBorder, badgeHdrBg, badgeHdrColor, badgePrcBg, badgePrcColor, badgeCustomTxt, m.data.sectionTitle)}
${renderExactProductCard(p, globalValidity, cardBg, cardBorder, badgeHdrBg, badgeHdrColor, badgePrcBg, badgePrcColor, badgeCustomTxt, m.data.sectionTitle, 600)}
</td>
</tr>
</table>`;
@@ -630,6 +630,56 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
}
}
// ---------------------------------------------------------------------------
// Úprava produktovej fotky (zoom / posun / orezanie)
// Upravená fotka sa vykreslí ako pozadie bunky (background-size / background-position
// fungujú aj v Gmaile, ktorý transform/position odstraňuje); čo presahuje rámik, sa oreže.
// Outlook pre Windows (mso) pozadia nevie, dostane pôvodný <img>.
// ---------------------------------------------------------------------------
function hasImageAdjust(p: any): boolean {
return (p.imageScale ?? 1) !== 1 || (p.imageOffsetX || 0) !== 0 || (p.imageOffsetY || 0) !== 0;
}
// Obsah vkladaný do podmieneného komentára pre Outlook nesmie obsahovať vlastné komentáre (napr. zo SVG cenovky)
function stripHtmlComments(html: string): string {
return html.replace(/<!--[\s\S]*?-->/g, '');
}
// Štýl pozadia: fotka so základnou šírkou baseW, vycentrovaná na (centerX, centerY) rámika, plus zoom a posun
function adjustedImageBgStyle(p: any, baseW: number, centerX: number, centerY: number): string {
const scale = p.imageScale ?? 1;
const aspect = p.imageAspect || 1;
const w = Math.round(baseW * scale);
const h = Math.round(w * aspect);
const x = Math.round(centerX - w / 2 + (p.imageOffsetX || 0));
const y = Math.round(centerY - h / 2 + (p.imageOffsetY || 0));
return `background-image: url('${escapeHtml(p.imageUrl)}'); background-repeat: no-repeat; background-size: ${w}px ${h}px; background-position: ${x}px ${y}px;`;
}
// Riadok s fotkou pod sebou (rozloženie "pod obrázkom" a vernostné karty)
function renderStackedImageRow(p: any, imgHtml: string, cellStyle: string, cardWidth: number, maxImgW: number, alt: string): string {
if (!hasImageAdjust(p)) {
return `
<tr height="160">
<td height="160" style="${cellStyle}" valign="middle">
${imgHtml}
</td>
</tr>`;
}
const baseW = Math.min(maxImgW, cardWidth - 16);
const baseH = Math.round(baseW * (p.imageAspect || 1));
const frameH = Math.max(160, baseH + 15);
return `
<!--[if mso]>
<tr><td height="160" style="${cellStyle}" valign="middle">${imgHtml}</td></tr>
<![endif]-->
<!--[if !mso]><!-->
<tr height="${frameH}">
<td height="${frameH}" role="img" aria-label="${escapeHtml(alt)}" style="height: ${frameH}px; line-height: ${frameH}px; font-size: 0; ${adjustedImageBgStyle(p, baseW, cardWidth / 2, baseH / 2)}">&nbsp;</td>
</tr>
<!--<![endif]-->`;
}
function renderExactProductCard(
p: ProductItem,
globalValidity: string,
@@ -640,7 +690,8 @@ function renderExactProductCard(
badgePrcBg: string,
badgePrcColor: string,
badgeCustomTxt: string | null,
sectionTitle?: string
sectionTitle?: string,
cardWidth: number = 294 // nominálna šírka karty v px (2 stĺpce = 294, 1 stĺpec = 600)
): string {
const validity = p.validityOverride || globalValidity;
const effectiveCardBg = p.cardBgColor || cardBg || '#feeaea';
@@ -726,15 +777,36 @@ function renderExactProductCard(
</table>
</td>
</tr>
<tr>
<td width="58%" height="160" style="padding: 8px 4px 8px 8px; text-align: center; height: 160px;" valign="middle">
<img src="${escapeHtml(p.imageUrl)}" width="180" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt'))}" style="display: block; width: 100%; max-width: 180px; margin: 0 auto; border-radius: 8px;">
</td>
<td width="42%" style="padding: 8px 8px 8px 4px; text-align: center;" valign="middle">
${(() => {
const alt = pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt');
const imgCell = `<td width="58%" height="160" style="padding: 8px 4px 8px 8px; text-align: center; height: 160px;" valign="middle">
<img src="${escapeHtml(p.imageUrl)}" width="180" alt="${escapeHtml(alt)}" style="display: block; width: 100%; max-width: 180px; margin: 0 auto; border-radius: 8px;">
</td>`;
const badgeCell = `<td width="42%" style="padding: 8px 8px 8px 4px; text-align: center;" valign="middle">
${renderBadgeContent(bWidth, bAlign, padTop, padBottom, marginStyle)}
${p.unitPrice ? `<div style="font-size: 8px; color: #666666; font-weight: 500; font-family: Arial, sans-serif; text-align: center; margin-top: 4px;">${escapeHtml(p.unitPrice)}</div>` : ''}
</td>`;
if (!hasImageAdjust(p)) {
return `<tr>${imgCell}${badgeCell}</tr>`;
}
// Upravená fotka: pozadie celého riadku, cenovka je nad ňou (produkt sa dá zasunúť pod cenovku)
const baseW = Math.min(180, Math.round(cardWidth * 0.58) - 12);
const frameH = Math.max(160, Math.round(baseW * (p.imageAspect || 1)) + 16);
return `
<!--[if mso]><tr>${imgCell}${stripHtmlComments(badgeCell)}</tr><![endif]-->
<!--[if !mso]><!-->
<tr>
<td colspan="2" height="${frameH}" role="img" aria-label="${escapeHtml(alt)}" style="height: ${frameH}px; ${adjustedImageBgStyle(p, baseW, Math.round(cardWidth * 0.29), frameH / 2)}">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="${frameH}">
<tr>
<td width="58%" height="${frameH}" style="font-size: 0; line-height: 0;">&nbsp;</td>
${badgeCell}
</tr>
</table>
</td>
</tr>
<!--<![endif]-->`;
})()}
<tr>
<td colspan="2" height="54" style="padding: 0 8px 10px 8px; text-align: center; height: 54px; min-height: 54px;" valign="top">
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>
@@ -756,11 +828,14 @@ function renderExactProductCard(
</table>
</td>
</tr>
<tr height="160">
<td height="160" style="padding: 0 8px 15px 8px; text-align: center; height: 160px;" valign="middle">
<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt'))}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">
</td>
</tr>
${renderStackedImageRow(
p,
`<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt'))}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">`,
'padding: 0 8px 15px 8px; text-align: center; height: 160px;',
cardWidth,
220,
pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt')
)}
<tr>
<td height="54" style="padding: 0 8px 10px 8px; text-align: center; height: 54px; min-height: 54px;" valign="top">
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>
@@ -813,11 +888,21 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
</table>
</td>
</tr>
${hasImageAdjust(p)
? renderStackedImageRow(
p,
`<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, 'Produkt'))}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">`,
'padding: 0 8px 15px 8px; text-align: center; height: 160px;',
294,
220,
pickAlt(p.imageAlt, p.title, 'Produkt')
)
: `
<tr height="1">
<td style="padding: 0 8px 15px 8px; text-align: center; height: 160px;" valign="middle">
<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, 'Produkt'))}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">
</td>
</tr>
</tr>`}
<tr>
<td style="padding: 0 8px 10px 8px; text-align: center; height: 100%; min-height: 44px;" valign="top">
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>