From 533a85630369ddee3ea54fd0788e82eae5ec8008 Mon Sep 17 00:00:00 2001 From: Jandl-hostacny Date: Wed, 21 Oct 2026 13:12:42 +0200 Subject: [PATCH] feat: viac fotiek v jednej produktovej karte (bundle) - pri produkte + Pridat dalsiu fotku: obrazok, poloha X/Y, velkost a volba pred / za hlavnou fotkou; lubovolny pocet fotiek - fotky su sucastou obrazka karty pri exporte Co-Authored-By: Claude Opus 5.5 --- src/components/editor/ProductEditor.tsx | 52 ++++++++++++++++++++++++- src/lib/productCard.ts | 12 +++++- src/types/newsletter.ts | 11 ++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/components/editor/ProductEditor.tsx b/src/components/editor/ProductEditor.tsx index ff4c922..6abc51c 100644 --- a/src/components/editor/ProductEditor.tsx +++ b/src/components/editor/ProductEditor.tsx @@ -3,7 +3,7 @@ // Editor jedného produktu (bežné aj vernostné produkty) – zbalený po produktoch, len to, čo treba: // texty, fotka (zoom/posun), cenovka (šablóna, veľkosť, ceny), dostupnosť a ďalšie možnosti. import React, { useEffect, useRef } from 'react'; -import { BadgeTemplate, CardProduct, ProductLabel } from '@/types/newsletter'; +import { BadgeTemplate, CardProduct, ProductLabel, ProductPhoto } from '@/types/newsletter'; import { ChevronDown, Trash2, Pencil, Globe, Layers } from 'lucide-react'; import { ImageUploadInput } from './ImageUploadInput'; import { pickAlt } from '@/lib/altText'; @@ -105,6 +105,31 @@ const ResetLink: React.FC<{ onClick: () => void }> = ({ onClick }) => ( ); +// Ďalšia fotka v karte (bundle): obrázok, poloha, veľkosť, pred/za hlavnou fotkou +const ExtraPhotoEditor: React.FC<{ + index: number; + photo: ProductPhoto; + onChange: (patch: Partial) => void; + onRemove: () => void; +}> = ({ index, photo, onChange, onRemove }) => ( +
+
+ Fotka {index + 2} + +
+ onChange({ url })} /> + onChange({ x: v })} /> + onChange({ y: v })} /> + onChange({ scale: v })} /> + +
+); + // Nastavenie jedného vlastného štítka na fotke const LABEL_COLORS: Array<[string, string]> = [ ['#e2001a', 'Červená'], @@ -322,6 +347,31 @@ export const ProductEditor: React.FC = ({ + + {/* Ďalšie fotky (bundle) */} + {(p.extraPhotos || []).map((f, i) => ( + onChange({ extraPhotos: (p.extraPhotos || []).map((x, j) => (j === i ? { ...x, ...patch } : x)) })} + onRemove={() => onChange({ extraPhotos: (p.extraPhotos || []).filter((_, j) => j !== i) })} + /> + ))} + {/* CENOVKA */} diff --git a/src/lib/productCard.ts b/src/lib/productCard.ts index 38137dc..b6923c3 100644 --- a/src/lib/productCard.ts +++ b/src/lib/productCard.ts @@ -5,7 +5,7 @@ // V editore (mode 'preview') sa vizuál skladá živo z HTML/CSS. Pri exporte (ZIP/Brevo) sa vizuál z náhľadu // uloží ako JPG (src/lib/cardImages.ts) a do e-mailu ide ako jeden obrázok – rovnako ako karta_produkt_*.jpg // v šablóne. Vďaka tomu karta vyzerá rovnako vo všetkých klientoch (Gmail, Outlook, mobil). -import { BadgeTemplate, CardProduct, NewsletterModule, ProductLabel } from '@/types/newsletter'; +import { BadgeTemplate, CardProduct, NewsletterModule, ProductLabel, ProductPhoto } from '@/types/newsletter'; import { renderCleanBadgeSvg, BadgeProductData } from './badgeUtils'; import { SUPABASE_ASSETS_BASE } from './defaultCampaign'; import { pickAlt } from './altText'; @@ -208,7 +208,7 @@ export function cardVisualKey(p: CardProduct, ctx: CardContext): string { p.cardPriceMain, p.cardPriceSub, p.cardDiscount, p.cardUnitPrice, 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.labels || [] + p.labels || [], p.extraPhotos || [] ]; const str = JSON.stringify(relevant); let h1 = 5381; @@ -221,6 +221,12 @@ export function cardVisualKey(p: CardProduct, ctx: CardContext): string { return `cv-${(h1 >>> 0).toString(36)}${(h2 >>> 0).toString(36)}`; } +// Ďalšia fotka v karte (bundle): poloha v % vizuálu, veľkosť voči hlavnej fotke +function renderExtraPhoto(f: ProductPhoto, mode: RenderMode): string { + if (!f.url) return ''; + return ``; +} + // Vlastný štítok na fotke (nad fotkou, pod cenovkou) – štýl ako „PODIEL MÄSA 92 %“ z letáku, písmo Aptly Bold (Adobe Fonts) function renderLabel(l: ProductLabel): string { if (!l.text) return ''; @@ -284,7 +290,9 @@ function renderVisualPreview(p: CardProduct, ctx: CardContext, key: string): str const insetRight = bgBox ? Math.round(bgBox.right * bw) : 0; return `
+ ${(p.extraPhotos || []).filter((f) => f.behind).map((f) => renderExtraPhoto(f, ctx.mode)).join('')} ${p.imageUrl ? `` : ''} + ${(p.extraPhotos || []).filter((f) => !f.behind).map((f) => renderExtraPhoto(f, ctx.mode)).join('')} ${(p.labels || []).map(renderLabel).join('')}
${stores ? `
${stores}
` : ''} diff --git a/src/types/newsletter.ts b/src/types/newsletter.ts index e668475..6450206 100644 --- a/src/types/newsletter.ts +++ b/src/types/newsletter.ts @@ -51,6 +51,7 @@ export interface ProductItem { storesOffsetY?: number; // posun štítkov predajní voči hornej hrane cenovky (+ vyššie) storesSize?: number; // výška jedného štítka predajne v px (nezávislá od veľkosti cenovky) labels?: ProductLabel[]; // vlastné štítky na fotke (napr. „PODIEL MÄSA 92 %“) + extraPhotos?: ProductPhoto[]; // ďalšie fotky v karte (bundle) popri hlavnej fotke imageScale?: number; // Zoom fotky (1 = 100 %) imageOffsetX?: number; // Posun fotky vodorovne v px (+ doprava) imageOffsetY?: number; // Posun fotky zvisle v px (+ nadol) @@ -68,6 +69,16 @@ export interface CardProduct extends ProductItem { noCardUnitPrice?: string; } +// Ďalšia fotka v karte produktu (napr. bundle 2 produktov) +export interface ProductPhoto { + id: string; + url: string; + x: number; // stred fotky, % šírky vizuálu (0–100) + y: number; // stred fotky, % výšky vizuálu (0–100) + scale: number; // 1 = rovnaká veľkosť ako hlavná fotka bez zoomu + behind?: boolean; // za hlavnou fotkou (inak pred ňou) +} + // Vlastný štítok na fotke produktu export interface ProductLabel { id: string;