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
|
// 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
|
// 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).
|
// 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 { renderCleanBadgeSvg, BadgeProductData } from './badgeUtils';
|
||||||
import { SUPABASE_ASSETS_BASE } from './defaultCampaign';
|
import { SUPABASE_ASSETS_BASE } from './defaultCampaign';
|
||||||
import { pickAlt } from './altText';
|
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
|
// 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 {
|
function renderVisualPreview(p: CardProduct, ctx: CardContext, key: string): string {
|
||||||
const height = cardVisualHeight(ctx.cardWidth);
|
const height = cardVisualHeight(ctx.cardWidth);
|
||||||
const bg = p.cardBgColor || ctx.cardBg;
|
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 sx = Math.round(p.storesOffsetX || 0);
|
||||||
const sy = Math.round(p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y);
|
const sy = Math.round(p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y);
|
||||||
const stores = renderStores(p, ctx.mode, Math.max(10, Math.round(bw * 0.12)));
|
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 `
|
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;">
|
<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;">` : ''}
|
${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;">
|
<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)}
|
${renderBadge(p, ctx, bw)}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
Reference in new issue
Block a user