fix: stitky predajni ukotvene k pravemu hornemu rohu pozadia cenovky
Pozadie cenovky moze mat v SVG priehladny okraj; jeho skutocne hranice sa odmeraju v prehliadaci (getBBox) a stitky sa ukotvia k jeho pravemu hornemu rohu namiesto rohu celeho SVG. Posun stitkov sa pocita od tohto rohu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c426145c48
commit
b0a050bd12
1 file changed
+45
-2
+45
-2
@@ -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 { CardProduct, NewsletterModule } from '@/types/newsletter';
|
||||
import { BadgeTemplate, CardProduct, NewsletterModule } from '@/types/newsletter';
|
||||
import { renderCleanBadgeSvg, BadgeProductData } from './badgeUtils';
|
||||
import { SUPABASE_ASSETS_BASE } from './defaultCampaign';
|
||||
import { pickAlt } from './altText';
|
||||
@@ -217,6 +217,45 @@ export function cardVisualKey(p: CardProduct, ctx: CardContext): string {
|
||||
}
|
||||
|
||||
// Vizuál karty v náhľade: fotka (zoom/posun), cenovka vpravo dole, štítky nad cenovkou
|
||||
// Viditeľné pozadie cenovky v SVG môže mať okolo seba priehľadný okraj. Štítky predajní ukotvujeme
|
||||
// k pravému hornému rohu samotného pozadia, preto jeho hranice odmeriame v prehliadači (getBBox).
|
||||
// Výsledok je podiel rozmerov viewBoxu; na serveri (bez DOM) sú okraje 0.
|
||||
interface BadgeBgBox {
|
||||
top: number; // okraj zhora / výška
|
||||
right: number; // okraj sprava / šírka
|
||||
aspect: number; // výška / šírka SVG
|
||||
}
|
||||
const badgeBgCache = new Map<string, BadgeBgBox>();
|
||||
|
||||
function badgeBackgroundBox(t: BadgeTemplate): BadgeBgBox | null {
|
||||
if (!t.svgRaw || typeof document === 'undefined') return null;
|
||||
const cached = badgeBgCache.get(t.svgRaw);
|
||||
if (cached) return cached;
|
||||
let box: BadgeBgBox | null = null;
|
||||
const holder = document.createElement('div');
|
||||
holder.style.cssText = 'position: absolute; left: -10000px; top: 0; visibility: hidden;';
|
||||
try {
|
||||
holder.innerHTML = renderCleanBadgeSvg({ ...t, layersConfig: [] }, { priceMain: '', priceSub: '' }, 200);
|
||||
document.body.appendChild(holder);
|
||||
const svg = holder.querySelector('svg');
|
||||
const vb = svg?.viewBox.baseVal;
|
||||
const bb = svg?.getBBox();
|
||||
if (vb && bb && vb.width && vb.height && bb.width && bb.height) {
|
||||
box = {
|
||||
top: Math.max(0, (bb.y - vb.y) / vb.height),
|
||||
right: Math.max(0, (vb.x + vb.width - (bb.x + bb.width)) / vb.width),
|
||||
aspect: vb.height / vb.width
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
box = null;
|
||||
} finally {
|
||||
holder.remove();
|
||||
}
|
||||
if (box) badgeBgCache.set(t.svgRaw, box);
|
||||
return box;
|
||||
}
|
||||
|
||||
function renderVisualPreview(p: CardProduct, ctx: CardContext, key: string): string {
|
||||
const height = cardVisualHeight(ctx.cardWidth);
|
||||
const bg = p.cardBgColor || ctx.cardBg;
|
||||
@@ -227,11 +266,15 @@ function renderVisualPreview(p: CardProduct, ctx: CardContext, key: string): str
|
||||
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)));
|
||||
// Pravý horný roh viditeľného pozadia cenovky (pri PNG a predvolenej cenovke = roh cenovky)
|
||||
const bgBox = p.badgeCustomTemplate && !p.customBadgePngUrl ? badgeBackgroundBox(p.badgeCustomTemplate) : null;
|
||||
const insetTop = bgBox ? Math.round(bgBox.top * bw * bgBox.aspect) : 0;
|
||||
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.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>` : ''}
|
||||
${stores ? `<div style="position: absolute; right: ${insetRight - sx}px; bottom: calc(100% - ${insetTop}px + ${sy}px);">${stores}</div>` : ''}
|
||||
${renderBadge(p, ctx, bw)}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
Reference in new issue
Block a user