diff --git a/src/app/page.tsx b/src/app/page.tsx index cb2dd41..9821606 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -15,6 +15,7 @@ import { SavedCampaignsModal } from '@/components/editor/SavedCampaignsModal'; import { NameDialog } from '@/components/editor/NameDialog'; import JSZip from 'jszip'; import { prepareCardImages } from '@/lib/cardImages'; +import { cardLayout } from '@/lib/productCard'; import { normalizeUnitPrice } from '@/lib/unitPrice'; import { useCampaignReview } from '@/components/review/useCampaignReview'; import { ReviewModal } from '@/components/review/ReviewModal'; @@ -404,6 +405,7 @@ export default function Home() { const inherited: Partial = {}; for (const k of layoutKeys) { if (k === 'cardBgColor' && target.type === 'loyalty_club') continue; // vernostné zľavy: farba pozadia ostáva + if (p.badgeCustomTemplate?.layout && k !== 'cardBgColor') continue; // šablóna má vlastné predvolené rozloženie if (p[k] === undefined && first[k] !== undefined) Object.assign(inherited, { [k]: first[k] }); } return { ...inherited, ...p }; @@ -648,6 +650,54 @@ export default function Home() { }; // Poloha štítkov predajní pre všetky produkty v celej kampani (všetky sekcie + vernostné zľavy) + // Rozloženie produktu (poloha a veľkosť cenovky, poloha a veľkosť štítkov) ako predvolené pre všetky šablóny + // rovnakého druhu (napr. všetky „Základná“); produkty s týmito šablónami v newsletteri ho prevezmú + const handleSaveLayoutAsDefault = async (p: CardProduct) => { + const t = p.badgeCustomTemplate; + if (!t?.importTags?.type) return; + const kind = (b: BadgeTemplate) => + b.importTags?.type === t.importTags?.type && !!b.importTags?.app === !!t.importTags?.app && !!b.importTags?.loyalty === !!t.importTags?.loyalty; + const group = availableBadges.filter(kind); + const L = cardLayout(p); + const layout = { offsetX: L.offsetX, offsetY: L.offsetY, storesOffsetX: L.storesX, storesOffsetY: L.storesY, storesSize: L.storesSize }; + const width = p.badgeWidth || t.width; + const label = t.name.split(' | ')[0]; + if (!confirm(`Uložiť polohu cenovky (${layout.offsetX} / ${layout.offsetY}), veľkosť (${width} px) a štítky (${layout.storesOffsetX} / ${layout.storesOffsetY}, ${layout.storesSize} px) ako predvolené pre ${group.length} šablón „${label}“?\n\nProdukty s týmito cenovkami v newsletteri ho prevezmú.`)) return; + try { + for (const b of group) { + const { data, error } = await supabase.from('badges').select('layers_config').eq('id', b.id).single(); + if (error) throw error; + const cfg = data.layers_config && !Array.isArray(data.layers_config) ? data.layers_config : { layers: data.layers_config || [] }; + const res = await supabase.from('badges').update({ layers_config: { ...cfg, layout, width } }).eq('id', b.id); + if (res.error) throw res.error; + } + } catch (err) { + alert(`Predvolené rozloženie sa nepodarilo uložiť: ${(err as Error).message}`); + return; + } + const updated = new Map(group.map((b) => [b.id, { ...b, layout, width }])); + setAvailableBadges((prev) => prev.map((b) => updated.get(b.id) || b)); + setCampaign((prev) => ({ + ...prev, + modules: prev.modules.map((m) => + m.type === 'products_grid' || m.type === 'loyalty_club' + ? { + ...m, + data: { + ...m.data, + products: (m.data.products || []).map((pp: ProductItem) => { + const u = pp.badgeCustomTemplate && !pp.customBadgePngUrl ? updated.get(pp.badgeCustomTemplate.id) : undefined; + return u + ? { ...pp, badgeCustomTemplate: u, badgeWidth: undefined, badgeOffsetX: undefined, badgeOffsetY: undefined, storesOffsetX: undefined, storesOffsetY: undefined, storesSize: undefined } + : pp; + }) + } + } + : m + ) + })); + }; + const handleApplyStoresOffsetToAll = (x: number, y: number, size: number) => { if (!confirm(`Použiť polohu (${x} / ${y}) a veľkosť (${size} px) štítkov pre všetky produkty v newsletteri?`)) return; setCampaign((prev) => ({ @@ -1007,6 +1057,7 @@ export default function Home() { onUpdateModule={handleUpdateModule} onApplyBadgeToAll={handleApplyBadgeToAll} onApplyStoresOffsetToAll={handleApplyStoresOffsetToAll} + onSaveLayoutAsDefault={handleSaveLayoutAsDefault} onUpdateAllProductSections={handleUpdateAllProductSections} campaignValidity={getCampaignValidity(campaign)} availableBadges={availableBadges} diff --git a/src/components/editor/BadgeEditorModal.tsx b/src/components/editor/BadgeEditorModal.tsx index b6388d9..57781d7 100644 --- a/src/components/editor/BadgeEditorModal.tsx +++ b/src/components/editor/BadgeEditorModal.tsx @@ -50,7 +50,8 @@ function toBadgeDbPayload(badge: BadgeTemplate) { paddingTop: badge.paddingTop ?? 4, paddingBottom: badge.paddingBottom ?? 4, importTags: badge.importTags, - isDefault: badge.isDefault || undefined + isDefault: badge.isDefault || undefined, + layout: badge.layout } }; } diff --git a/src/components/editor/ConfigPanel.tsx b/src/components/editor/ConfigPanel.tsx index 76fa460..a99980a 100644 --- a/src/components/editor/ConfigPanel.tsx +++ b/src/components/editor/ConfigPanel.tsx @@ -16,6 +16,7 @@ interface ConfigPanelProps { onUpdateModule: (updated: NewsletterModule) => void; onApplyBadgeToAll?: (badge: BadgeTemplate, badgeLayout?: 'below' | 'side') => void; onApplyStoresOffsetToAll?: (x: number, y: number, size: number) => void; + onSaveLayoutAsDefault?: (p: CardProduct) => void; // Zmena nastavení spoločných pre všetky produktové sekcie a vernostné zľavy (platnosť akcie...) onUpdateAllProductSections?: (patch: Record) => void; campaignValidity?: string; // globálna platnosť akcie (pre Lištu platnosti) @@ -95,6 +96,7 @@ export const ConfigPanel: React.FC = ({ onUpdateModule, onApplyBadgeToAll, onApplyStoresOffsetToAll, + onSaveLayoutAsDefault, onUpdateAllProductSections, campaignValidity = '', onOpenBadgeManager, @@ -703,6 +705,7 @@ export const ConfigPanel: React.FC = ({ onApplyBadgeToSection={() => applyBadgeToSection(p)} onApplyBadgeToAll={onApplyBadgeToAll ? () => applyBadgeToAll(p, 'standard') : undefined} onApplyStoresOffsetToAll={onApplyStoresOffsetToAll} + onSaveLayoutAsDefault={onSaveLayoutAsDefault} /> ))} @@ -928,6 +931,7 @@ export const ConfigPanel: React.FC = ({ onApplyBadgeToSection={() => applyBadgeToSection(p)} onApplyBadgeToAll={onApplyBadgeToAll ? () => applyBadgeToAll(p, 'loyalty') : undefined} onApplyStoresOffsetToAll={onApplyStoresOffsetToAll} + onSaveLayoutAsDefault={onSaveLayoutAsDefault} /> ))} diff --git a/src/components/editor/ProductEditor.tsx b/src/components/editor/ProductEditor.tsx index 94bd1b7..263ad24 100644 --- a/src/components/editor/ProductEditor.tsx +++ b/src/components/editor/ProductEditor.tsx @@ -7,7 +7,7 @@ import { BADGE_IMPORT_TYPES, BadgeTemplate, CardProduct, ProductLabel, ProductPh import { ChevronDown, ChevronUp, Trash2, Pencil, Globe, Layers } from 'lucide-react'; import { ImageUploadInput } from './ImageUploadInput'; import { pickAlt } from '@/lib/altText'; -import { DEFAULT_IMAGE_SCALE, DEFAULT_STORES_OFFSET_X, DEFAULT_STORES_OFFSET_Y, DEFAULT_STORES_SIZE, MAIN_PHOTO, PRODUCT_BG_PRESETS, badgeWidthFor, photoStack, productCardAlt, usesAppPrice } from '@/lib/productCard'; +import { DEFAULT_IMAGE_SCALE, DEFAULT_STORES_OFFSET_X, DEFAULT_STORES_OFFSET_Y, DEFAULT_STORES_SIZE, MAIN_PHOTO, PRODUCT_BG_PRESETS, badgeWidthFor, cardLayout, photoStack, productCardAlt, usesAppPrice } from '@/lib/productCard'; export { PRODUCT_BG_PRESETS }; @@ -33,6 +33,7 @@ interface ProductEditorProps { onApplyBadgeToSection?: () => void; onApplyBadgeToAll?: () => void; onApplyStoresOffsetToAll?: (x: number, y: number, size: number) => void; // poloha a veľkosť štítkov pre všetky produkty + onSaveLayoutAsDefault?: (p: CardProduct) => void; // rozloženie produktu ako predvolené pre šablóny rovnakého druhu } const inputCls = 'w-full text-xs border border-gray-300 rounded-lg px-2 py-1.5 bg-white'; @@ -321,7 +322,8 @@ export const ProductEditor: React.FC = ({ onOpenBadgeEditor, onApplyBadgeToSection, onApplyBadgeToAll, - onApplyStoresOffsetToAll + onApplyStoresOffsetToAll, + onSaveLayoutAsDefault }) => { // Po otvorení (aj klikom na produkt v náhľade) posunieme panel na tento produkt const rootRef = useRef(null); @@ -347,9 +349,10 @@ export const ProductEditor: React.FC = ({ const scale = p.imageScale ?? DEFAULT_IMAGE_SCALE; const offsetX = p.imageOffsetX || 0; const offsetY = p.imageOffsetY || 0; - const storesX = p.storesOffsetX ?? DEFAULT_STORES_OFFSET_X; - const storesY = p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y; - const storesSize = p.storesSize ?? DEFAULT_STORES_SIZE; + const lay = cardLayout(p); + const storesX = lay.storesX; + const storesY = lay.storesY; + const storesSize = lay.storesSize; const hasStores = !!(p.stores?.potraviny || p.stores?.supermarket || p.stores?.tempo); const badgeWidth = badgeWidthFor(p, variant); @@ -544,8 +547,8 @@ export const ProductEditor: React.FC = ({ - - + + {isLoyalty ? ( <> @@ -582,6 +585,17 @@ export const ProductEditor: React.FC = ({ )} )} + + {p.badgeCustomTemplate?.importTags?.type && onSaveLayoutAsDefault && ( + + )} {/* DOSTUPNOSŤ */} diff --git a/src/lib/badgeUtils.ts b/src/lib/badgeUtils.ts index 5a56587..2dd2e17 100644 --- a/src/lib/badgeUtils.ts +++ b/src/lib/badgeUtils.ts @@ -1,4 +1,4 @@ -import { BadgeTemplate, BadgeLayer, BadgeImportTags } from '@/types/newsletter'; +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[] = []; @@ -21,6 +21,7 @@ export interface BadgeRow { paddingBottom?: number; importTags?: BadgeImportTags; isDefault?: boolean; + layout?: BadgeLayout; } | null; } @@ -38,7 +39,8 @@ export function badgeFromRow(d: BadgeRow): BadgeTemplate { paddingTop: cfg?.paddingTop ?? 4, paddingBottom: cfg?.paddingBottom ?? 4, importTags: cfg?.importTags, - isDefault: cfg?.isDefault || undefined + isDefault: cfg?.isDefault || undefined, + layout: cfg?.layout }; } diff --git a/src/lib/productCard.ts b/src/lib/productCard.ts index 3980dca..2f9d638 100644 --- a/src/lib/productCard.ts +++ b/src/lib/productCard.ts @@ -103,6 +103,18 @@ function badgeData(p: CardProduct, variant: CardVariant): BadgeProductData { return p; } +// Poloha cenovky a štítkov predajní: vlastné nastavenie produktu, inak predvolené zo šablóny, inak pevné +export function cardLayout(p: CardProduct) { + const l = p.customBadgePngUrl ? undefined : p.badgeCustomTemplate?.layout; + return { + offsetX: p.badgeOffsetX ?? l?.offsetX ?? 0, + offsetY: p.badgeOffsetY ?? l?.offsetY ?? 0, + storesX: p.storesOffsetX ?? l?.storesOffsetX ?? DEFAULT_STORES_OFFSET_X, + storesY: p.storesOffsetY ?? l?.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y, + storesSize: p.storesSize ?? l?.storesSize ?? DEFAULT_STORES_SIZE + }; +} + export function badgeWidthFor(p: CardProduct, variant: CardVariant): number { if (p.badgeWidth) return p.badgeWidth; if (p.badgeCustomTemplate) return p.badgeCustomTemplate.width || 110; @@ -225,12 +237,12 @@ export function cardVisualKey(p: CardProduct, ctx: CardContext): string { 'v2', // verzia vykreslenia – zvýšiť, keď sa zmení spôsob prípravy obrázkov (v2: písma z Adobe Fonts) ctx.variant, ctx.cardWidth, p.cardBgColor || ctx.cardBg, ctx.variant === 'standard' ? ctx.defaultBadge : null, p.imageUrl, p.imageScale ?? DEFAULT_IMAGE_SCALE, p.imageOffsetX || 0, p.imageOffsetY || 0, - p.customBadgePngUrl, badgeWidthFor(p, ctx.variant), p.badgeOffsetX || 0, p.badgeOffsetY || 0, + p.customBadgePngUrl, badgeWidthFor(p, ctx.variant), cardLayout(p), t ? [t.svgRaw, t.layersConfig, t.width] : null, p.priceMain, p.priceSub, p.discountText, p.badgeText, p.unitPrice, p.cardPriceMain, p.cardPriceSub, p.cardDiscount, p.cardUnitPrice, p.appPriceMain, p.appPriceSub, p.appDiscount, p.appUnitPrice, p.noCardPriceMain, p.noCardPriceSub, p.noCardDiscount, p.noCardUnitPrice, - p.stores, p.storesOffsetX ?? DEFAULT_STORES_OFFSET_X, p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y, p.storesSize ?? DEFAULT_STORES_SIZE, + p.stores, p.labels || [], p.extraPhotos || [], photoStack(p) ]; const str = JSON.stringify(relevant); @@ -336,9 +348,10 @@ function renderVisualPreview(p: CardProduct, ctx: CardContext, key: string): str const scale = p.imageScale ?? DEFAULT_IMAGE_SCALE; const ox = p.imageOffsetX || 0; const oy = p.imageOffsetY || 0; - const sx = Math.round(p.storesOffsetX ?? DEFAULT_STORES_OFFSET_X); - const sy = Math.round(p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y); - const stores = renderStores(p, ctx.mode, p.storesSize ?? DEFAULT_STORES_SIZE, bw); + const lay = cardLayout(p); + const sx = Math.round(lay.storesX); + const sy = Math.round(lay.storesY); + const stores = renderStores(p, ctx.mode, lay.storesSize, bw); // 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; @@ -355,7 +368,7 @@ function renderVisualPreview(p: CardProduct, ctx: CardContext, key: string): str }) .join('')} ${(p.labels || []).map(renderLabel).join('')} -
+
${stores ? `
${stores}
` : ''} ${renderBadge(p, ctx, bw)}
diff --git a/src/types/newsletter.ts b/src/types/newsletter.ts index 3add029..7806e74 100644 --- a/src/types/newsletter.ts +++ b/src/types/newsletter.ts @@ -252,6 +252,16 @@ export interface BadgeTemplate { paddingBottom?: number; importTags?: BadgeImportTags; // podľa čoho sa šablóna vyberie pri importe podkladov (ZIP) isDefault?: boolean; // predvolená cenovka produktov – nedá sa zmazať + layout?: BadgeLayout; // predvolené rozloženie pre produkty s touto cenovkou (ak ho produkt nemá vlastné) +} + +// Predvolená poloha cenovky a štítkov predajní pre šablónu (napr. všetky „Základná“) +export interface BadgeLayout { + offsetX?: number; // posun cenovky vodorovne, + doprava + offsetY?: number; // posun cenovky zvisle, + nadol + storesOffsetX?: number; // štítky predajní ↔ + storesOffsetY?: number; // štítky predajní ↕ + storesSize?: number; // výška štítka predajne v px } // Druhy cenoviek v podkladoch: „Zľava grafická“, a keď je prázdna, základná cenovka s výkričníkom alebo bez