diff --git a/scripts/badges/convert-illustrator-svg.py b/scripts/badges/convert-illustrator-svg.py new file mode 100644 index 0000000..f71b6ba --- /dev/null +++ b/scripts/badges/convert-illustrator-svg.py @@ -0,0 +1,137 @@ +# Prevod testovacích SVG cenoviek z Illustratora na pozadia šablón: +# - ceny, zľava % a „zľava“ sa odstránia (budú z nich vrstvy editora), +# - statické nápisy (SUPER / CENA / VÝHODNÉ, písmo Gilroy-Heavy) sa prevedú na krivky, +# - triedy a id dostanú jedinečnú predponu (inak by sa viaceré cenovky na stránke prebíjali), +# - obsah sa posunie tak, aby telo cenovky bolo vo všetkých šablónach na rovnakom mieste. +# +# Použitie: pip install fonttools; python convert-illustrator-svg.py +# (Gilroy-Heavy musí byť nainštalovaný; priradenie súborov k typu/farbe/pôvodu je v ASSETS nižšie.) +# Výstup obsahuje vyčistené SVG a namerané polohy textov – z nich sa vyrobili šablóny v tabuľke badges. +import json +import re +import sys + +from fontTools.pens.svgPathPen import SVGPathPen +from fontTools.pens.transformPen import TransformPen +from fontTools.ttLib import TTFont + +SRC = sys.argv[1] +OUT = sys.argv[2] +GILROY = r'C:\Users\jakub\AppData\Local\Microsoft\Windows\Fonts\Gilroy-Heavy.ttf' + +font = TTFont(GILROY) +glyphs = font.getGlyphSet() +cmap = font.getBestCmap() +upem = font['head'].unitsPerEm + +# referenčná poloha tela cenovky (zhora ľavý roh rámika s cenou) +REF_X = 27.98 + +ASSETS = { + 'Asset 2.svg': ('SUPER CENA', 'cervena', 'SK'), + 'Asset 3.svg': ('SUPER VÝHODNÉ', 'cervena', 'SK'), + 'Asset 4.svg': ('SUPER CENA', 'oranzova', 'SK'), + 'Asset 5.svg': ('SUPER CENA', 'zelena', 'SK'), + 'Asset 6.svg': ('SUPER CENA', 'cervena', ''), + 'Asset 7.svg': ('SUPER VÝHODNÉ', 'cervena', ''), + 'Asset 8.svg': ('SUPER CENA', 'oranzova', ''), + 'Asset 9.svg': ('SUPER VÝHODNÉ', 'zelena', ''), +} + + +def parse_css(svg): + style = re.search(r'', svg, re.S).group(1) + props = {} + for sel, body in re.findall(r'([^{}]+)\{([^}]*)\}', style): + decl = dict( + (k.strip(), v.strip()) for k, v in (d.split(':', 1) for d in body.split(';') if ':' in d) + ) + for cls in sel.split(','): + props.setdefault(cls.strip().lstrip('.'), {}).update(decl) + return props + + +def text_to_path(transform, runs, size, fill): + """runs: [(x, text, letter_spacing_em)] → s tou istou transformáciou ako pôvodný .""" + s = size / upem + pen = SVGPathPen(glyphs) + for x, txt, ls in runs: + cursor = x + for ch in txt: + name = cmap.get(ord(ch)) + if not name: + continue + glyphs[name].draw(TransformPen(pen, (s, 0, 0, -s, cursor, 0))) + cursor += glyphs[name].width * s + ls * size + d = pen.getCommands() + return f'' if d else '' + + +result = [] +for fname, (btype, color, origin) in ASSETS.items(): + svg = open(f'{SRC}/{fname}', encoding='utf-8').read() + key = 'b' + re.sub(r'\D', '', fname) + css = parse_css(svg) + + texts = {} # rola → údaje + def replace_text(m): + classes = m.group(1).split() + transform = m.group(2) + body = m.group(3) + p = {} + for c in classes: + p.update(css.get(c, {})) + size = float(p.get('font-size', '0').replace('px', '')) + family = p.get('font-family', '') + fill = p.get('fill', '#000') + runs = [] + for tcls, x, txt in re.findall(r'([^<]*)', body): + ls = css.get(tcls, {}).get('letter-spacing', '0em').replace('em', '') if tcls else '0' + runs.append((float(x), txt, float(ls or 0))) + content = ''.join(r[1] for r in runs) + tr = re.match(r'translate\(([\d.\-]+) ([\d.\-]+)\)', transform) + tx, ty = float(tr.group(1)), float(tr.group(2)) + if not content.strip(): + return '' + if 'Gilroy' in family: + return text_to_path(transform, runs, size, fill) + # Aptly: ceny, zľava % a „zľava“ → vrstvy editora + if '%' in content: + role = 'discount' + elif content.isdigit() and size >= 25: + role = 'main' + elif content.isdigit(): + role = 'cents' + else: + role = 'label' + texts[role] = {'text': content, 'x': tx, 'y': ty, 'size': size, 'fill': fill, 'runs': runs} + return '' + + svg = re.sub(r'(.*?)', replace_text, svg, flags=re.S) + + # posun obsahu: rámik s cenou vždy na REF_X a jeho horná hrana na rovnakej výške v rámci druhu cenovky + rects = re.findall(r'', svg) + box_x, box_y = float(rects[0][1]), float(rects[0][2]) + dx = REF_X - box_x + dy = (92.67 if btype == 'SUPER CENA' else 44.98) - box_y + + # jedinečné triedy a id + svg = re.sub(r'\bcls-(\d+)\b', rf'{key}-c\1', svg) + for ident in set(re.findall(r'id="([^"]+)"', svg)): + svg = re.sub(rf'(id="|url\(#|xlink:href="#){re.escape(ident)}(["\)])', rf'\1{key}-{ident}\2', svg) + + inner = re.sub(r'^.*?]*>', '', svg, flags=re.S) + inner = re.sub(r'\s*$', '', inner) + defs = re.search(r'.*?', inner, re.S).group(0) + body = inner.replace(defs, '') + vb = [float(v) for v in re.search(r'viewBox="([^"]+)"', svg).group(1).split()] + + result.append({ + 'file': fname, 'key': key, 'type': btype, 'color': color, 'origin': origin, + 'dx': dx, 'dy': dy, 'boxY': box_y, 'vb': vb, 'texts': texts, + 'svg': f'{defs}{body.strip()}', + }) + +json.dump(result, open(OUT, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) +for r in result: + print(r['file'], r['type'], r['color'], r['origin'], 'dx', round(r['dx'], 2), 'dy', round(r['dy'], 2), 'boxY', r['boxY'], 'texts', {k: v['text'] for k, v in r['texts'].items()}, len(r['svg'])) diff --git a/src/app/page.tsx b/src/app/page.tsx index 4ab5d1c..fc1f5c7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -349,7 +349,7 @@ export default function Home() { setSelectedModuleId(newMod.id); }; - const handleImportProducts = (imported: ProductItem[]) => { + const handleImportProducts = (imported: ProductItem[], options?: { replace?: boolean }) => { if (!selectedModuleId) return; const target = campaign.modules.find((m) => m.id === selectedModuleId); if (!target || target.type !== 'products_grid') { @@ -374,7 +374,7 @@ export default function Home() { ...target, data: { ...target.data, - products: [...(target.data.products || []), ...withLayout] + products: options?.replace ? withLayout : [...(target.data.products || []), ...withLayout] } }; handleUpdateModule(updated); diff --git a/src/components/editor/BadgeEditorModal.tsx b/src/components/editor/BadgeEditorModal.tsx index 47f434c..d8b74e8 100644 --- a/src/components/editor/BadgeEditorModal.tsx +++ b/src/components/editor/BadgeEditorModal.tsx @@ -991,6 +991,14 @@ export const BadgeEditorModal: React.FC = ({ updateLayer(layer.id, { indexFontSize: v })} /> updateLayer(layer.id, { indexOffsetY: v })} /> updateLayer(layer.id, { indexOffsetX: v })} /> + updateLayer(layer.id, { longPriceScale: v >= 100 ? undefined : v / 100 })} + /> )} diff --git a/src/components/editor/ProductImportModal.tsx b/src/components/editor/ProductImportModal.tsx index 769d872..13a01c0 100644 --- a/src/components/editor/ProductImportModal.tsx +++ b/src/components/editor/ProductImportModal.tsx @@ -14,7 +14,7 @@ import { shrinkImage, uploadFile } from '@/lib/upload'; interface ImportModalProps { isOpen: boolean; onClose: () => void; - onImportProducts: (products: ProductItem[]) => void; + onImportProducts: (products: ProductItem[], options?: { replace?: boolean }) => void; availableBadges?: BadgeTemplate[]; // šablóny cenoviek – stĺpec s kódom určí, ktorú použiť } @@ -56,6 +56,7 @@ export const ProductImportModal: React.FC = ({ // ZIP s podkladmi z Databázy produktov (tabuľka + fotky) – pevný formát, bez mapovania stĺpcov const [zip, setZip] = useState<{ products: (PodkladyProduct & { thumb: string })[]; info: string } | null>(null); const [zipProgress, setZipProgress] = useState(null); + const [replaceExisting, setReplaceExisting] = useState(false); // vymazať doterajšie produkty sekcie if (!isOpen) return null; @@ -179,7 +180,7 @@ export const ProductImportModal: React.FC = ({ zip.products.forEach((p) => p.thumb && URL.revokeObjectURL(p.thumb)); setZip(null); setFile(null); - onImportProducts(items); + onImportProducts(items, { replace: replaceExisting }); onClose(); }; @@ -340,6 +341,10 @@ export const ProductImportModal: React.FC = ({ ); })} +

Fotky sa pri importe zmenšia na najviac 1000 px, pozadie karty sa nastaví podľa farby cenovky. Rozloženie karty (veľkosť a poloha cenovky, štítky predajní) sa prevezme z prvého produktu v sekcii. diff --git a/src/lib/badgeUtils.ts b/src/lib/badgeUtils.ts index 10a2310..ab81d67 100644 --- a/src/lib/badgeUtils.ts +++ b/src/lib/badgeUtils.ts @@ -97,6 +97,8 @@ export function badgePriceParts(token: string, product: BadgeProductData): [stri 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. @@ -155,11 +157,21 @@ export function renderCleanBadgeSvg( // Zostavenie textových vrstiev const textLayersSvg = (template.layersConfig || []).map((layer, layerIndex) => { 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 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'; const fontFamily = layer.fontFamily @@ -172,7 +184,6 @@ export function renderCleanBadgeSvg( // Formátovanie textu: ak obsahuje {cena} alebo {centy}, centy vložíme do ako horný index let contentSvg = ''; - const priceMatch = rawText.match(BADGE_PRICE_TOKEN_RE); if (priceMatch) { const [tokMain, tokSub] = badgePriceParts(priceMatch[1], product); const idx = priceMatch.index || 0; @@ -189,7 +200,7 @@ export function renderCleanBadgeSvg( } // data-layer-index: editor cenoviek podľa neho nájde a odmeria vrstvu na plátne - return `${contentSvg}`; + return `${contentSvg}`; }).join('\n '); return ` diff --git a/src/types/newsletter.ts b/src/types/newsletter.ts index bd5172d..3980a93 100644 --- a/src/types/newsletter.ts +++ b/src/types/newsletter.ts @@ -229,6 +229,7 @@ export interface BadgeLayer { indexFontSize?: number; // Veľkosť horného indexu v px indexOffsetY?: number; // Vertikálny posun horného indexu v px (posun hore / dole) indexOffsetX?: number; // Horizontálny posun horného indexu v px (medzera) + longPriceScale?: number; // zmenšenie ceny od 10 € (dvojciferné eurá), napr. 0.8; účiaria ostáva } export interface BadgeTemplate {