180 lines
6.6 KiB
TypeScript
180 lines
6.6 KiB
TypeScript
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
|
|
|
// Vymazané všetky preddefinované šablóny — v systéme sú výhradne vlastné cenovky vytvorené používateľom
|
|
export const DEFAULT_BADGE_TEMPLATES: BadgeTemplate[] = [];
|
|
|
|
export interface BadgeProductData {
|
|
priceMain: string;
|
|
priceSub: string;
|
|
discountText?: string;
|
|
unitPrice?: string;
|
|
}
|
|
|
|
/**
|
|
* Vygeneruje 100% čistý vektorový SVG reťazec cenovky bez akýchkoľvek
|
|
* editovacích rámčekov, tooltippov či ovládacích prvkov.
|
|
* Centy sú naformátované ako horný index (tspan dy="-0.45em").
|
|
* Cena je predvolene bez znaku €.
|
|
*/
|
|
export function renderCleanBadgeSvg(
|
|
template: BadgeTemplate,
|
|
product: BadgeProductData,
|
|
overrideWidth?: number
|
|
): string {
|
|
const pMain = product.priceMain || '0';
|
|
const pSub = product.priceSub || '00';
|
|
const discount = (product.discountText || '').replace('-', '');
|
|
const unitPrice = product.unitPrice || '';
|
|
|
|
// Získanie viewBox alebo predvolených rozmerov
|
|
let vb = '0 0 140 95';
|
|
let vbWidth = 140;
|
|
let vbHeight = 95;
|
|
|
|
if (template.svgRaw) {
|
|
const vbMatch = template.svgRaw.match(/viewBox=["']([^"']+)["']/i);
|
|
if (vbMatch) {
|
|
vb = vbMatch[1];
|
|
const parts = vb.trim().split(/\s+/).map(Number);
|
|
if (parts.length === 4 && parts[2] > 0 && parts[3] > 0) {
|
|
vbWidth = parts[2];
|
|
vbHeight = parts[3];
|
|
}
|
|
} else {
|
|
const wMatch = template.svgRaw.match(/width=["']([^"']+)["']/i);
|
|
const hMatch = template.svgRaw.match(/height=["']([^"']+)["']/i);
|
|
if (wMatch && hMatch) {
|
|
const w = parseFloat(wMatch[1]);
|
|
const h = parseFloat(hMatch[1]);
|
|
if (w > 0 && h > 0) {
|
|
vbWidth = w;
|
|
vbHeight = h;
|
|
vb = `0 0 ${w} ${h}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extrahovanie vnútorného obsahu SVG (medzi <svg...> a </svg>)
|
|
let innerBg = '';
|
|
if (template.svgRaw) {
|
|
const inside = template.svgRaw.replace(/<\/?svg[^>]*>/gi, '').trim();
|
|
innerBg = inside;
|
|
} else {
|
|
innerBg = `<rect width="${vbWidth}" height="${vbHeight}" rx="14" fill="#ffd100"/>`;
|
|
}
|
|
|
|
// Šírka výsledného SVG
|
|
const displayWidth = overrideWidth || template.width || 110;
|
|
const displayHeight = Math.round((displayWidth / vbWidth) * vbHeight);
|
|
|
|
// Zostavenie textových vrstiev
|
|
const textLayersSvg = (template.layersConfig || []).map((layer) => {
|
|
const rawText = layer.text || '';
|
|
const fontSize = layer.fontSize || 16;
|
|
const subFontSize = layer.indexFontSize || Math.max(10, Math.round(fontSize * 0.55));
|
|
const subDy = layer.indexOffsetY !== undefined ? `${layer.indexOffsetY}px` : '-0.45em';
|
|
const subDx = layer.indexOffsetX !== undefined ? `${layer.indexOffsetX}px` : '1px';
|
|
const revertDy = layer.indexOffsetY !== undefined ? `${-layer.indexOffsetY}px` : '0.45em';
|
|
const fontWeight = layer.fontWeight || '700';
|
|
const fill = layer.fill || '#ffffff';
|
|
const fontFamily = layer.fontFamily
|
|
? `'${layer.fontFamily}', -apple-system, BlinkMacSystemFont, Arial, sans-serif`
|
|
: `'Arial Narrow', Arial, sans-serif`;
|
|
|
|
let anchor = 'middle';
|
|
if (layer.align === 'left') anchor = 'start';
|
|
if (layer.align === 'right') anchor = 'end';
|
|
|
|
// Formátovanie textu: ak obsahuje {cena} alebo {centy}, centy vložíme do <tspan> ako horný index
|
|
let contentSvg = '';
|
|
if (rawText.includes('{cena}')) {
|
|
const parts = rawText.split('{cena}');
|
|
const before = escapeXml(parts[0] || '');
|
|
const after = escapeXml(parts[1] || '');
|
|
contentSvg = `${before}${escapeXml(pMain)}<tspan font-size="${subFontSize}px" dx="${subDx}" dy="${subDy}" font-weight="${fontWeight}">${escapeXml(pSub)}</tspan>${after ? `<tspan dy="${revertDy}">${after}</tspan>` : ''}`;
|
|
} else if (rawText.includes('{centy}')) {
|
|
const parts = rawText.split('{centy}');
|
|
const before = escapeXml(parts[0] || '');
|
|
const after = escapeXml(parts[1] || '');
|
|
contentSvg = `${before}<tspan font-size="${subFontSize}px" dx="${subDx}" dy="${subDy}" font-weight="${fontWeight}">${escapeXml(pSub)}</tspan>${after ? `<tspan dy="${revertDy}">${after}</tspan>` : ''}`;
|
|
} else {
|
|
let replaced = rawText
|
|
.replace(/{cela_suma}/g, pMain)
|
|
.replace(/{zlava}/g, discount)
|
|
.replace(/{jednotkova_cena}/g, unitPrice)
|
|
.replace(/{mena}/g, '€');
|
|
contentSvg = escapeXml(replaced);
|
|
}
|
|
|
|
return `<text x="${layer.x}" y="${layer.y}" text-anchor="${anchor}" dominant-baseline="central" font-family="${fontFamily}" font-size="${fontSize}px" font-weight="${fontWeight}" fill="${fill}">${contentSvg}</text>`;
|
|
}).join('\n ');
|
|
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="${vb}" width="${displayWidth}" height="${displayHeight}" style="display:block;max-width:100%;height:auto;">
|
|
${innerBg}
|
|
${textLayersSvg}
|
|
</svg>`;
|
|
}
|
|
|
|
/**
|
|
* Skonvertuje SVG reťazec do čistého PNG Data URL v prehliadači
|
|
* (bez akýchkoľvek DOM prvkov alebo modrých drag & drop rámčekov).
|
|
*/
|
|
export async function svgToCleanPng(
|
|
svgString: string,
|
|
width: number,
|
|
height: number
|
|
): Promise<string> {
|
|
if (typeof window === 'undefined') {
|
|
return `data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`;
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
try {
|
|
const img = new Image();
|
|
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
const url = URL.createObjectURL(svgBlob);
|
|
|
|
img.onload = () => {
|
|
try {
|
|
const canvas = document.createElement('canvas');
|
|
const scale = 2; // Vysoké rozlíšenie Retina
|
|
canvas.width = width * scale;
|
|
canvas.height = height * scale;
|
|
const ctx = canvas.getContext('2d');
|
|
if (ctx) {
|
|
ctx.scale(scale, scale);
|
|
ctx.drawImage(img, 0, 0, width, height);
|
|
URL.revokeObjectURL(url);
|
|
resolve(canvas.toDataURL('image/png'));
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.warn('Canvas export fallback to SVG data URL', e);
|
|
}
|
|
URL.revokeObjectURL(url);
|
|
resolve(`data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`);
|
|
};
|
|
|
|
img.onerror = () => {
|
|
URL.revokeObjectURL(url);
|
|
resolve(`data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`);
|
|
};
|
|
|
|
img.src = url;
|
|
} catch (err) {
|
|
resolve(`data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function escapeXml(unsafe: string): string {
|
|
if (!unsafe) return '';
|
|
return unsafe
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|