From ea2a9d85d0615845107381445f6fd27ae0260d08 Mon Sep 17 00:00:00 2001 From: Jandl-hostacny Date: Mon, 19 Oct 2026 16:46:20 +0200 Subject: [PATCH] feat: stitky predajni pri hornej hrane cenovky s nastavitelnym posunom - stitky Potraviny/Supermarket/Tempo su nad cenovkou, zarovnane k jej pravemu okraju a posuvaju sa spolu s nou (pod/vedla obrazka, zarovnanie, sirka) - posuvniky Vodorovne / Zvisle pre celu sekciu (aj vernostne zlavy); zaporny zvisly posun prekryje horny okraj cenovky - spolocny helper na vykreslenie stitkov namiesto dvoch kopii Co-Authored-By: Claude Opus 5.5 --- src/components/editor/ConfigPanel.tsx | 59 ++++++++++++++++- src/lib/emailRenderer.ts | 95 +++++++++++++++++++-------- src/types/newsletter.ts | 2 + 3 files changed, 129 insertions(+), 27 deletions(-) diff --git a/src/components/editor/ConfigPanel.tsx b/src/components/editor/ConfigPanel.tsx index 2ce3ab5..f0a515e 100644 --- a/src/components/editor/ConfigPanel.tsx +++ b/src/components/editor/ConfigPanel.tsx @@ -6,7 +6,7 @@ import { Plus, Trash2, Image as ImageIcon, Tag, Store, Globe, Palette, Check, Co import { SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign'; import { ImageUploadInput } from './ImageUploadInput'; import { pickAlt } from '@/lib/altText'; -import { renderGeneratedModuleHtml, CUSTOM_HTML_MODULE_TYPES } from '@/lib/emailRenderer'; +import { renderGeneratedModuleHtml, CUSTOM_HTML_MODULE_TYPES, DEFAULT_STORES_OFFSET_Y } from '@/lib/emailRenderer'; const PRODUCT_BG_PRESETS = [ { label: 'Červená', sub: 'Fantasticky', color: '#feeaea', border: '#f5d4d4' }, @@ -29,6 +29,49 @@ interface ConfigPanelProps { availableBadges?: BadgeTemplate[]; } +// Posun štítkov predajní voči hornej hrane cenovky (pre celú sekciu) +const StoresOffsetControl: React.FC<{ + x: number; + y: number; + onChange: (patch: { storesOffsetX?: number; storesOffsetY?: number }) => void; +}> = ({ x, y, onChange }) => { + const row = (label: string, value: number, min: number, max: number, key: 'storesOffsetX' | 'storesOffsetY', hint: string) => ( +
+ {label} + onChange({ [key]: Number(e.target.value) })} + className="w-full h-1 accent-red-600 cursor-pointer" + /> + {value} px +
+ ); + const isChanged = x !== 0 || y !== DEFAULT_STORES_OFFSET_Y; + return ( +
+
+ Štítky predajní pri cenovke + {isChanged && ( + + )} +
+ {row('Vodorovne', x, -80, 80, 'storesOffsetX', '+ doprava, − doľava')} + {row('Zvisle', y, -40, 40, 'storesOffsetY', '+ vyššie nad cenovku, − nižšie cez horný okraj cenovky')} +

Štítky sú pri hornej hrane cenovky a posúvajú sa spolu s ňou. Platí pre celú sekciu.

+
+ ); +}; + // Pomer strán obrázka (výška / šírka); pri chybe načítania 1 const loadImageAspect = (url: string): Promise => new Promise((resolve) => { @@ -569,6 +612,13 @@ export const ConfigPanel: React.FC = ({ + {/* Štítky predajní (Potraviny / Supermarket / Tempo) nad hornou hranou cenovky */} + updateData(patch)} + /> + {/* Rozloženie produktov (Počet produktov na riadok) */}
+ {/* Štítky predajní (Potraviny / Supermarket / Tempo) nad hornou hranou cenovky */} + updateData(patch)} + /> + {/* Farba pozadia vernostných kariet */}
diff --git a/src/lib/emailRenderer.ts b/src/lib/emailRenderer.ts index ff9a02f..1c0cc47 100644 --- a/src/lib/emailRenderer.ts +++ b/src/lib/emailRenderer.ts @@ -347,6 +347,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string { // Platnosť na kartách je predvolene skrytá (zobrazí sa v Lište platnosti akcie); // produkty s vlastnou platnosťou (validityOverride) ju zobrazia vždy const globalValidity = m.data.showCardValidity === true ? (m.data.globalValidity || '') : ''; + const storesOffset: StoresOffset = { x: m.data.storesOffsetX, y: m.data.storesOffsetY }; const products: ProductItem[] = m.data.products || []; const cardBg = m.data.cardBgColor || '#feeaea'; const cardBorder = m.data.cardBorderColor || '#f5d4d4'; @@ -369,7 +370,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
- ${renderExactProductCard(p, globalValidity, cardBg, cardBorder, badgeHdrBg, badgeHdrColor, badgePrcBg, badgePrcColor, badgeCustomTxt, m.data.sectionTitle, 600)} + ${renderExactProductCard(p, globalValidity, cardBg, cardBorder, badgeHdrBg, badgeHdrColor, badgePrcBg, badgePrcColor, badgeCustomTxt, m.data.sectionTitle, 600, storesOffset)}
`; @@ -380,7 +381,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string { ${row.map((p, idx) => ` - ${renderExactProductCard(p, globalValidity, cardBg, cardBorder, badgeHdrBg, badgeHdrColor, badgePrcBg, badgePrcColor, badgeCustomTxt, m.data.sectionTitle)} + ${renderExactProductCard(p, globalValidity, cardBg, cardBorder, badgeHdrBg, badgeHdrColor, badgePrcBg, badgePrcColor, badgeCustomTxt, m.data.sectionTitle, 294, storesOffset)} `).join('\n')} ${row.length === 1 ? '' : ''} @@ -438,7 +439,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string { ${lProducts.map((p: any, idx: number) => ` - ${renderExactLoyaltyCard(p, m.data.showCardValidity === true ? (m.data.globalValidity || '2. 5. – 15. 5. 2026') : '')} + ${renderExactLoyaltyCard(p, m.data.showCardValidity === true ? (m.data.globalValidity || '2. 5. – 15. 5. 2026') : '', { x: m.data.storesOffsetX, y: m.data.storesOffsetY })} `).join('\n')} @@ -712,6 +713,56 @@ function renderStackedImageRow(p: ProductItem, imgHtml: string, cellStyle: strin `; } +// Štítky dostupnosti v predajniach (Potraviny / Supermarket / Tempo); prázdne, ak nie je zvolená žiadna +function renderStoresHtml(p: any): string { + const tag = (color: string, file: string, alt: string) => + `
${alt}
`; + const rows = [ + p.stores?.potraviny ? tag('#e2001a', 'stit_potraviny.png', 'Potraviny') : '', + p.stores?.supermarket ? tag('#f9890e', 'stit_supermarket.png', 'Supermarket') : '', + p.stores?.tempo ? tag('#005239', 'stit_tempo.png', 'Tempo') : '' + ].join(''); + return rows ? `${rows}
` : ''; +} + +// Posun štítkov predajní voči hornej hrane cenovky (nastavenie sekcie) +export interface StoresOffset { + x?: number; // + doprava, − doľava + y?: number; // + vyššie nad cenovku, − nižšie (prekryje horný okraj cenovky) +} +export const DEFAULT_STORES_OFFSET_Y = 2; + +// Štítky predajní "prilepené" k hornej hrane cenovky (zarovnané k jej pravému okraju), +// takže sa posúvajú spolu s cenovkou; padTop = odsadenie celej cenovky zhora +function attachStoresToBadge( + badgeHtml: string, + storesHtml: string, + bWidth: number, + bAlign: string, + marginStyle: string, + padTop: number, + offset: StoresOffset = {} +): string { + if (!storesHtml) { + return `
${badgeHtml}
`; + } + const x = Math.round(offset.x || 0); + const y = Math.round(offset.y ?? DEFAULT_STORES_OFFSET_Y); + const cellPadding = `padding: ${padTop}px ${x < 0 ? -x : 0}px ${y > 0 ? y : 0}px 0;`; + const storesStyle = `margin-right: ${x > 0 ? -x : 0}px; margin-bottom: ${y < 0 ? y : 0}px; position: relative; z-index: 2;`; + return ` + + + + + + + +
+
${storesHtml}
+
${badgeHtml}
`; +} + function renderExactProductCard( p: ProductItem, globalValidity: string, @@ -723,22 +774,22 @@ function renderExactProductCard( badgePrcColor: string, badgeCustomTxt: string | null, sectionTitle?: string, - cardWidth: number = 294 // nominálna šírka karty v px (2 stĺpce = 294, 1 stĺpec = 600) + cardWidth: number = 294, // nominálna šírka karty v px (2 stĺpce = 294, 1 stĺpec = 600) + storesOffset: StoresOffset = {} ): string { const validity = p.validityOverride || globalValidity; const effectiveCardBg = p.cardBgColor || cardBg || '#feeaea'; - const storesHtml = ` - - ${p.stores?.potraviny ? `` : ''} - ${p.stores?.supermarket ? `` : ''} - ${p.stores?.tempo ? `` : ''} -
Potraviny
Supermarket
Tempo
`; + const storesHtml = renderStoresHtml(p); const badgeLayout = p.badgeLayout || 'below'; + // Cenovka so štítkami predajní nad hornou hranou (bez obalového ) + const renderBadgeContent = (bWidth: number, bAlign: string, padTop: number, padBottom: number, marginStyle: string) => + attachStoresToBadge(renderBadgeOnly(bWidth, bAlign, 0, padBottom, marginStyle), storesHtml, bWidth, bAlign, marginStyle, padTop, storesOffset); + // Pomocná funkcia na render samotnej cenovky (bez obalového ) - const renderBadgeContent = (bWidth: number, bAlign: string, padTop: number, padBottom: number, marginStyle: string) => { + const renderBadgeOnly = (bWidth: number, bAlign: string, padTop: number, padBottom: number, marginStyle: string) => { // Vlastná PNG cenovka (výnimka) má prednosť pred šablónou; po jej odstránení sa vráti šablóna if (p.customBadgePngUrl) { return ` @@ -804,7 +855,6 @@ function renderExactProductCard( -
${escapeHtml(validity)}${storesHtml}
@@ -855,7 +905,6 @@ function renderExactProductCard( -
${escapeHtml(validity)}${storesHtml}
@@ -898,14 +947,9 @@ function renderExactProductCard( `; } -function renderExactLoyaltyCard(p: any, validity: string): string { +function renderExactLoyaltyCard(p: any, validity: string, storesOffset: StoresOffset = {}): string { const cardValidity = p.validityOverride || validity; - const storesHtml = ` - - ${p.stores?.potraviny ? `` : ''} - ${p.stores?.supermarket ? `` : ''} - ${p.stores?.tempo ? `` : ''} -
Potraviny
Supermarket
Tempo
`; + const storesHtml = renderStoresHtml(p); const loyaltyBg = p.cardBgColor || '#e6e1d9'; return ` @@ -915,7 +959,6 @@ function renderExactLoyaltyCard(p: any, validity: string): string { -
${escapeHtml(cardValidity)}${storesHtml}
@@ -967,8 +1010,8 @@ function renderExactLoyaltyCard(p: any, validity: string): string { cardDiscount: p.cardDiscount }; - return ` - + return attachStoresToBadge(` +
-
${p.customBadgePngUrl @@ -976,11 +1019,11 @@ function renderExactLoyaltyCard(p: any, validity: string): string { : renderCleanBadgeSvg(p.badgeCustomTemplate, loyaltyProductData, bWidth)}
`; + `, storesHtml, bWidth, bAlign, marginStyle, padTop, storesOffset); } // 2. Predvolený Dual Sticker Block (Bez karty / S kartou) 1:1 - return ` + return attachStoresToBadge(` -
@@ -1033,7 +1076,7 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
${escapeHtml(p.cardUnitPrice)}
`; + `, storesHtml, 140, 'center', 'margin: 0 auto;', 0, storesOffset); })()} diff --git a/src/types/newsletter.ts b/src/types/newsletter.ts index dd04159..0f165f1 100644 --- a/src/types/newsletter.ts +++ b/src/types/newsletter.ts @@ -112,6 +112,8 @@ export interface ProductsGridModuleData { cardBorderColor?: string; columns?: 1 | 2; showCardValidity?: boolean; // platnosť na kartách produktov (predvolene skrytá) + storesOffsetX?: number; // posun štítkov predajní voči cenovke (+ doprava) + storesOffsetY?: number; // posun štítkov predajní voči hornej hrane cenovky (+ vyššie) } export interface LoyaltyClubModuleData {