feat: viac fotiek v jednej produktovej karte (bundle)

- pri produkte + Pridat dalsiu fotku: obrazok, poloha X/Y, velkost a volba
  pred / za hlavnou fotkou; lubovolny pocet fotiek
- fotky su sucastou obrazka karty pri exporte

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:12:42 +02:00
1 parent 23f23c8903
commit 533a856303
3 files changed
+72 -3

No files matched your search

+10 -2
View File
@@ -5,7 +5,7 @@
// 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 { BadgeTemplate, CardProduct, NewsletterModule, ProductLabel } from '@/types/newsletter';
import { BadgeTemplate, CardProduct, NewsletterModule, ProductLabel, ProductPhoto } from '@/types/newsletter';
import { renderCleanBadgeSvg, BadgeProductData } from './badgeUtils';
import { SUPABASE_ASSETS_BASE } from './defaultCampaign';
import { pickAlt } from './altText';
@@ -208,7 +208,7 @@ export function cardVisualKey(p: CardProduct, ctx: CardContext): string {
p.cardPriceMain, p.cardPriceSub, p.cardDiscount, p.cardUnitPrice,
p.noCardPriceMain, p.noCardPriceSub, p.noCardDiscount, p.noCardUnitPrice,
p.stores, p.storesOffsetX ?? DEFAULT_STORES_OFFSET_X, p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y, p.storesSize ?? DEFAULT_STORES_SIZE,
p.labels || []
p.labels || [], p.extraPhotos || []
];
const str = JSON.stringify(relevant);
let h1 = 5381;
@@ -221,6 +221,12 @@ export function cardVisualKey(p: CardProduct, ctx: CardContext): string {
return `cv-${(h1 >>> 0).toString(36)}${(h2 >>> 0).toString(36)}`;
}
// Ďalšia fotka v karte (bundle): poloha v % vizuálu, veľkosť voči hlavnej fotke
function renderExtraPhoto(f: ProductPhoto, mode: RenderMode): string {
if (!f.url) return '';
return `<img src="${escapeHtml(assetUrl(f.url, mode))}" alt="" style="position: absolute; left: ${f.x}%; top: ${f.y}%; max-width: 86%; max-height: 86%; transform: translate(-50%, -50%) scale(${f.scale || 1}); border: 0;">`;
}
// Vlastný štítok na fotke (nad fotkou, pod cenovkou) – štýl ako „PODIEL MÄSA 92 %“ z letáku, písmo Aptly Bold (Adobe Fonts)
function renderLabel(l: ProductLabel): string {
if (!l.text) return '';
@@ -284,7 +290,9 @@ function renderVisualPreview(p: CardProduct, ctx: CardContext, key: string): str
const insetRight = bgBox ? Math.round(bgBox.right * bw) : 0;
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.extraPhotos || []).filter((f) => f.behind).map((f) => renderExtraPhoto(f, ctx.mode)).join('')}
${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;">` : ''}
${(p.extraPhotos || []).filter((f) => !f.behind).map((f) => renderExtraPhoto(f, ctx.mode)).join('')}
${(p.labels || []).map(renderLabel).join('')}
<div style="position: absolute; right: 8px; bottom: 8px; width: ${bw}px;">
${stores ? `<div style="position: absolute; right: ${insetRight - sx}px; bottom: calc(100% - ${insetTop}px + ${sy}px);">${stores}</div>` : ''}