import { BadgeTemplate, BadgeLayer, BadgeImportTags, BadgeLayout } 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[] = []; // --------------------------------------------------------------------------- // Šablóny v databáze (tabuľka badges) a kód šablóny pre hromadný import // --------------------------------------------------------------------------- export interface BadgeRow { id: string; name: string; svg_raw: string | null; layers_config: | BadgeLayer[] | { layers?: BadgeLayer[]; width?: number; align?: BadgeTemplate['align']; paddingTop?: number; paddingBottom?: number; importTags?: BadgeImportTags; isDefault?: boolean; layout?: BadgeLayout; } | null; } // Riadok tabuľky badges → šablóna (staršie riadky majú v layers_config len pole vrstiev) export function badgeFromRow(d: BadgeRow): BadgeTemplate { const cfg = d.layers_config && !Array.isArray(d.layers_config) ? d.layers_config : null; return { id: d.id, name: d.name, svgRaw: d.svg_raw || '', layersConfig: cfg ? cfg.layers || [] : (d.layers_config as BadgeLayer[]) || [], width: cfg?.width || 110, align: cfg?.align || 'center', paddingTop: cfg?.paddingTop ?? 4, paddingBottom: cfg?.paddingBottom ?? 4, importTags: cfg?.importTags, isDefault: cfg?.isDefault || undefined, layout: cfg?.layout }; } // Kód šablóny pre hromadný import = názov šablóny upravený na kód: // bez diakritiky, veľké písmená, medzery a interpunkcia → _ (napr. „Super cena – 1+1“ → SUPER_CENA_1_1) export function normalizeBadgeCode(value: string | null | undefined): string { return String(value || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toUpperCase() .trim() .replace(/[^A-Z0-9]+/g, '_') .replace(/^_+|_+$/g, ''); } export const badgeCode = (b: Pick) => normalizeBadgeCode(b.name); // Šablóna podľa hodnoty zo súboru (kód alebo názov – oboje sa porovná po úprave na kód) export function findBadgeByCode(badges: BadgeTemplate[], value: string | null | undefined): BadgeTemplate | undefined { const wanted = normalizeBadgeCode(value); if (!wanted) return undefined; return badges.find((b) => badgeCode(b) === wanted); } export interface BadgeProductData { priceMain: string; priceSub: string; discountText?: string; unitPrice?: string; cardPriceMain?: string; // cena s kartou → {cena_s_kartou} cardPriceSub?: string; cardDiscount?: string; // zľava s kartou → {zlava_s_kartou} appPriceMain?: string; // cena pre appkáčov → {cena_app} appPriceSub?: string; appDiscount?: string; // → {zlava_app} appUnitPrice?: string; // → {jednotkova_cena_app} cardUnitPrice?: string; // → {jednotkova_cena_s_kartou} noCardPriceMain?: string; // cena bez vernostnej karty → {cena_bez_karty} noCardPriceSub?: string; noCardDiscount?: string; // → {zlava_bez_karty} noCardUnitPrice?: string; // → {jednotkova_cena_bez_karty} } // Premenné ceny s centami v hornom indexe: {cena}, {cena_s_kartou}, {cena_bez_karty} a {cena_app} export const BADGE_PRICE_TOKEN_RE = /\{(cena|cena_s_kartou|cena_bez_karty|cena_app)\}/; // Nahradenie jednoduchých textových premenných (bez horného indexu) export function replaceBadgeTextVars(text: string, product: BadgeProductData): string { return text .replace(/{cela_suma}/g, product.priceMain || '0') .replace(/{zlava_s_kartou}/g, (product.cardDiscount || '').replace('-', '')) .replace(/{zlava_bez_karty}/g, (product.noCardDiscount || '').replace('-', '')) .replace(/{zlava_app}/g, (product.appDiscount || '').replace('-', '')) .replace(/{jednotkova_cena_s_kartou}/g, product.cardUnitPrice || product.unitPrice || '') .replace(/{jednotkova_cena_bez_karty}/g, product.noCardUnitPrice || '') .replace(/{jednotkova_cena_app}/g, product.appUnitPrice || '') .replace(/{zlava}/g, (product.discountText || '').replace('-', '')) .replace(/{jednotkova_cena}/g, product.unitPrice || '') .replace(/{mena}/g, '€'); } // Celé eurá a centy pre danú cenovú premennú export function badgePriceParts(token: string, product: BadgeProductData): [string, string] { if (token === 'cena_s_kartou') { return [product.cardPriceMain || '', product.cardPriceSub || '']; } if (token === 'cena_app') { return [product.appPriceMain || '', product.appPriceSub || '']; } if (token === 'cena_bez_karty') { return [product.noCardPriceMain || '', product.noCardPriceSub || '']; } return [product.priceMain || '0', product.priceSub || '00']; } const round2 = (n: number) => Math.round(n * 100) / 100; /** * 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 pSub = product.priceSub || '00'; // 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 a ) let innerBg = ''; if (template.svgRaw) { const inside = template.svgRaw.replace(/<\/?svg[^>]*>/gi, '').trim(); innerBg = inside; } else { innerBg = ``; } // Ší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, layerIndex) => { // napr. nápis „zľava“ len vtedy, keď má produkt zľavu if (layer.showIfFilled && !replaceBadgeTextVars(layer.showIfFilled, product).trim()) return ''; const rawText = layer.text || ''; const priceMatch = rawText.match(BADGE_PRICE_TOKEN_RE); // Cena od 10 € (dvojciferné eurá): menšie písmo, účiaria eur aj centov ostáva na mieste // (0.43 ≈ vzdialenosť stredu písma od účiarie v em – vrstvy sa kreslia so strednou účiariou) const baseSize = layer.fontSize || 16; const baseSub = layer.indexFontSize || Math.max(10, Math.round(baseSize * 0.55)); const long = !!priceMatch && !!layer.longPriceScale && layer.longPriceScale < 1 && badgePriceParts(priceMatch[1], product)[0].length >= 2; const k = long ? layer.longPriceScale! : 1; const fontSize = round2(baseSize * k); const subFontSize = round2(baseSub * k); const layerY = long ? round2(layer.y + 0.43 * baseSize * (1 - k)) : layer.y; const offY = layer.indexOffsetY !== undefined ? round2(layer.indexOffsetY - (long ? 0.43 * (1 - k) * (baseSize - baseSub) : 0)) : undefined; const offX = layer.indexOffsetX !== undefined ? round2(layer.indexOffsetX * k) : undefined; const subDy = offY !== undefined ? `${offY}px` : '-0.45em'; const subDx = offX !== undefined ? `${offX}px` : '1px'; const revertDy = offY !== undefined ? `${-offY}px` : '0.45em'; const fontWeight = layer.fontWeight || '700'; const fill = layer.fill || '#ffffff'; // písmo môže mať viac názvov oddelených čiarkou (napr. názov v Adobe Fonts a názov nainštalovaného písma) const fontFamily = layer.fontFamily ? `${layer.fontFamily .split(',') .map((f) => `'${f.trim().replace(/'/g, '')}'`) .join(', ')}, -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 ako horný index let contentSvg = ''; let caronDx = ''; // posuny písmen za ľ/ť/ď (atribút dx na , bez vnorených tspan) if (priceMatch) { const [tokMain, tokSub] = badgePriceParts(priceMatch[1], product); const idx = priceMatch.index || 0; const before = escapeXml(replaceBadgeTextVars(rawText.slice(0, idx), product)); const after = escapeXml(replaceBadgeTextVars(rawText.slice(idx + priceMatch[0].length), product)); contentSvg = `${before}${escapeXml(tokMain)}${escapeXml(tokSub)}${after ? `${after}` : ''}`; } else if (rawText.includes('{centy}')) { const parts = rawText.split('{centy}'); const before = escapeXml(parts[0] || ''); const after = escapeXml(parts[1] || ''); contentSvg = `${before}${escapeXml(pSub)}${after ? `${after}` : ''}`; } else { const plain = replaceBadgeTextVars(rawText, product); contentSvg = escapeXml(plain); caronDx = caronOffsets(plain, fontSize); } // data-layer-index: editor cenoviek podľa neho nájde a odmeria vrstvu na plátne return `${contentSvg}`; }).join('\n '); return ` ${innerBg} ${textLayersSvg} `; } /** * 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 { 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)}`); } }); } // Mäkčeň pri ľ, ť, ď je v písme široký – písmeno za ním trochu pritiahneme (ako v návrhoch z Illustratora). // Posun ide do zoznamu dx priamo na (po znakoch), nie do vnoreného – ten pri prevode karty // na obrázok strácal zarovnanie na strednú účiaru a písmeno vyskočilo z riadku. function caronOffsets(text: string, fontSize: number): string { const chars = [...text]; const shift = -Math.round(fontSize * 0.05 * 100) / 100; const dx = chars.map((c, i) => (i > 0 && /[ľťď]/.test(chars[i - 1]) && !/\s/.test(c) ? shift : 0)); return dx.some(Boolean) ? dx.join(' ') : ''; } function escapeXml(unsafe: string): string { if (!unsafe) return ''; return unsafe .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }