diff --git a/src/app/api/img/route.ts b/src/app/api/img/route.ts new file mode 100644 index 0000000..7892097 --- /dev/null +++ b/src/app/api/img/route.ts @@ -0,0 +1,31 @@ +// Proxy obrázkov pre náhľad produktových kariet: obrázky z iných domén idú cez rovnaký pôvod, +// aby sa vizuál karty dal pri exporte uložiť ako obrázok (canvas nesmie byť "tainted" cez CORS). +const MAX_BYTES = 15 * 1024 * 1024; + +export async function GET(request: Request) { + const target = new URL(request.url).searchParams.get('url'); + if (!target || !/^https?:\/\//i.test(target)) { + return Response.json({ error: 'Neplatná adresa obrázka.' }, { status: 400 }); + } + + try { + const res = await fetch(target, { redirect: 'follow' }); + const contentType = res.headers.get('content-type') || ''; + if (!res.ok || !contentType.startsWith('image/')) { + return Response.json({ error: `Obrázok sa nepodarilo načítať (${res.status}).` }, { status: 502 }); + } + const body = await res.arrayBuffer(); + if (body.byteLength > MAX_BYTES) { + return Response.json({ error: 'Obrázok je príliš veľký.' }, { status: 413 }); + } + return new Response(body, { + headers: { + 'content-type': contentType, + 'cache-control': 'public, max-age=86400' + } + }); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'Chyba pri načítaní obrázka'; + return Response.json({ error: message }, { status: 502 }); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 4962213..4caa847 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -13,6 +13,7 @@ import { ProductImportModal } from '@/components/editor/ProductImportModal'; import { BadgeEditorModal } from '@/components/editor/BadgeEditorModal'; import { SavedCampaignsModal } from '@/components/editor/SavedCampaignsModal'; import JSZip from 'jszip'; +import { prepareCardImages } from '@/lib/cardImages'; import { saveAs } from 'file-saver'; import { ChevronUp, ChevronDown, Copy, Trash2, Anchor, Lock } from 'lucide-react'; @@ -432,8 +433,19 @@ export default function Home() { } }; + // Pred exportom sa vizuály produktových kariet uložia ako obrázky (nezmenené sa nenahrávajú znova) + const prepareForExport = async (): Promise => { + const { campaign: prepared, failed } = await prepareCardImages(campaign); + setCampaign((prev) => ({ ...prev, modules: prepared.modules })); + if (failed > 0) { + alert(`${failed} kariet produktov sa nepodarilo uložiť ako obrázok – v e-maile budú v jednoduchšej podobe.`); + } + return prepared; + }; + const handleExportZip = async () => { - const rawHtml = renderEmailHtml(campaign); + const prepared = await prepareForExport(); + const rawHtml = renderEmailHtml(prepared); let finalHtml = rawHtml; try { const res = await fetch('/api/inline-css', { @@ -456,7 +468,8 @@ export default function Home() { const handleExportBrevo = async (updateExisting = false) => { setIsExportingBrevo(true); try { - const rawHtml = renderEmailHtml(campaign); + const prepared = await prepareForExport(); + const rawHtml = renderEmailHtml(prepared); let finalHtml = rawHtml; try { const res = await fetch('/api/inline-css', { diff --git a/src/components/editor/ConfigPanel.tsx b/src/components/editor/ConfigPanel.tsx index c096531..d583e40 100644 --- a/src/components/editor/ConfigPanel.tsx +++ b/src/components/editor/ConfigPanel.tsx @@ -1,21 +1,15 @@ 'use client'; import React from 'react'; -import { NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter'; +import { NewsletterModule, ProductItem, BadgeTemplate, CardProduct } from '@/types/newsletter'; import { Plus, Trash2, Image as ImageIcon, Tag, Store, Globe, Palette, Check, Code } from 'lucide-react'; import { SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign'; import { ImageUploadInput } from './ImageUploadInput'; import { pickAlt } from '@/lib/altText'; -import { renderGeneratedModuleHtml, CUSTOM_HTML_MODULE_TYPES, DEFAULT_STORES_OFFSET_Y } from '@/lib/emailRenderer'; +import { renderGeneratedModuleHtml, CUSTOM_HTML_MODULE_TYPES } from '@/lib/emailRenderer'; +import { badgeWidthFor } from '@/lib/productCard'; +import { ProductEditor, PRODUCT_BG_PRESETS } from './ProductEditor'; -const PRODUCT_BG_PRESETS = [ - { label: 'Červená', sub: 'Fantasticky', color: '#feeaea', border: '#f5d4d4' }, - { label: 'Žltá', sub: 'Domáce', color: '#fef0c7', border: '#e6d49a' }, - { label: 'Hnedá', sub: 'Gril', color: '#dcb085', border: '#c49a6c' }, - { label: 'Sivá', sub: 'Neutrálna', color: '#e6e1d9', border: '#d5cfc5' }, - { label: 'Zelená', sub: 'Bio', color: '#e8f5e9', border: '#c8e6c9' }, - { label: 'Biela', sub: 'Čistá', color: '#ffffff', border: '#e5e7eb' }, -]; interface ConfigPanelProps { module: NewsletterModule | null; @@ -29,115 +23,6 @@ interface ConfigPanelProps { availableBadges?: BadgeTemplate[]; } -// Posun štítkov predajní voči hornej hrane cenovky (pre jeden produkt) -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.

-
- ); -}; - -// Pomer strán obrázka (výška / šírka); pri chybe načítania 1 -const loadImageAspect = (url: string): Promise => - new Promise((resolve) => { - if (!url) return resolve(1); - const img = new window.Image(); - img.onload = () => resolve(img.naturalWidth ? img.naturalHeight / img.naturalWidth : 1); - img.onerror = () => resolve(1); - img.src = url; - }); - -// Úprava produktovej fotky: zoom a posun (čo presahuje rámik, sa oreže) -const ProductImageAdjust: React.FC<{ - product: ProductItem; - onChange: (patch: Partial) => void; -}> = ({ product, onChange }) => { - const scale = product.imageScale ?? 1; - const offsetX = product.imageOffsetX || 0; - const offsetY = product.imageOffsetY || 0; - const isAdjusted = scale !== 1 || offsetX !== 0 || offsetY !== 0; - - const update = async (patch: Partial) => { - // Pomer strán fotky potrebujeme pre presný výpočet pozadia; zistíme ho pri prvej úprave - if (!product.imageAspect && product.imageUrl) { - patch = { ...patch, imageAspect: await loadImageAspect(product.imageUrl) }; - } - onChange(patch); - }; - - const slider = (label: string, value: number, display: string, min: number, max: number, step: number, key: keyof ProductItem) => ( -
- {label} - update({ [key]: Number(e.target.value) } as Partial)} - className="w-full h-1 accent-red-600 cursor-pointer" - /> - {display} -
- ); - - return ( -
-
- Úprava fotky - {isAdjusted && ( - - )} -
- {slider('Veľkosť', scale, `${Math.round(scale * 100)} %`, 0.5, 2.5, 0.05, 'imageScale')} - {slider('Vodorovne', offsetX, `${offsetX} px`, -150, 150, 1, 'imageOffsetX')} - {slider('Zvisle', offsetY, `${offsetY} px`, -120, 120, 1, 'imageOffsetY')} -

Čo presahuje rámik, sa oreže. Pri polohe cenovky „vedľa obrázka“ sa dá produkt zasunúť pod cenovku.

-
- ); -}; - // Jednoduchý HTML editor: vlastné HTML nahradí modul vygenerovaný z formulára const CustomHtmlEditor: React.FC<{ module: NewsletterModule; @@ -201,29 +86,6 @@ const CustomHtmlEditor: React.FC<{ ); }; -// Nahratie celej cenovky ako PNG pre výnimočný produkt (prekryje šablónu) -const CustomBadgePngInput: React.FC<{ value?: string; onChange: (url: string) => void }> = ({ value, onChange }) => ( -
- - {value ? ( - - ) : ( -

Nahraná PNG nahradí generovanú cenovku len pri tomto produkte.

- )} -
-); - export const ConfigPanel: React.FC = ({ module, onUpdateModule, @@ -252,6 +114,33 @@ export const ConfigPanel: React.FC = ({ }); }; + // Zmena jedného produktu v sekcii + const updateProduct = (pIdx: number, patch: Record) => { + const products = [...(module.data.products || [])]; + products[pIdx] = { ...products[pIdx], ...patch }; + updateData({ products }); + }; + + // Cenovka produktu pre všetky produkty sekcie (produkty s vlastnou PNG cenovkou sa neprepisujú) + const applyBadgeToSection = (p: CardProduct) => { + const template = p.badgeCustomTemplate; + if (!template) return; + if (!confirm(`Použiť cenovku "${template.name}" pre všetky produkty v tejto sekcii?`)) return; + const products = (module.data.products || []).map((prod: CardProduct) => + prod.customBadgePngUrl + ? prod + : { ...prod, badgeCustomTemplate: template, badgeId: template.id, badgeWidth: p.badgeWidth } + ); + updateData({ products }); + }; + + // Cenovka produktu v celom newsletteri (všetky produktové sekcie aj vernostné zľavy) + const applyBadgeToAll = (p: CardProduct, variant: 'standard' | 'loyalty') => { + if (!p.badgeCustomTemplate || !onApplyBadgeToAll) return; + if (!confirm(`Použiť cenovku "${p.badgeCustomTemplate.name}" pre VŠETKY produkty v celom newsletteri?`)) return; + onApplyBadgeToAll({ ...p.badgeCustomTemplate, width: badgeWidthFor(p, variant) }); + }; + return (
@@ -737,8 +626,8 @@ export const ConfigPanel: React.FC = ({
- {/* Zoznam produktov */} -
+ {/* Produkty */} +
Produkty ({module.data.products?.length || 0}) @@ -751,7 +640,7 @@ export const ConfigPanel: React.FC = ({ imageUrl: `${SUPABASE_ASSETS_BASE}produkt_1.png`, priceMain: '1', priceSub: '99', - unitPrice: '1kg = 4,99 €', + unitPrice: '', discountText: '20%', badgeText: 'zľava', cardBgColor: module.data.cardBgColor || '#feeaea', @@ -766,535 +655,19 @@ export const ConfigPanel: React.FC = ({
{(module.data.products || []).map((p: ProductItem, pIdx: number) => ( -
-
- - {p.title || `Produkt ${pIdx + 1}`} - - -
- -
- { - const products = [...module.data.products]; - products[pIdx].title = e.target.value; - updateData({ products }); - }} - placeholder="Názov produktu" - className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white font-medium" - /> -
- -
-
- - { - const products = [...module.data.products]; - products[pIdx].quantity = e.target.value; - updateData({ products }); - }} - placeholder="napr. 35 g" - className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].note = e.target.value; - updateData({ products }); - }} - placeholder="napr. Ilustračné foto" - className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].imageUrl = url; - products[pIdx].imageAspect = undefined; // nová fotka = nový pomer strán - updateData({ products }); - }} - altValue={p.imageAlt} - onAltChange={(alt) => { - const products = [...module.data.products]; - products[pIdx].imageAlt = alt; - updateData({ products }); - }} - altPlaceholder={pickAlt(p.title, module.data.sectionTitle, 'Produkt')} - placeholder="https://...produkt.png alebo nahrajte súbor" - /> - - { - const products = [...module.data.products]; - products[pIdx] = { ...products[pIdx], ...patch }; - updateData({ products }); - }} - /> - -
- - { - const products = [...module.data.products]; - products[pIdx].validityOverride = e.target.value; - updateData({ products }); - }} - placeholder={`Predvolená: ${module.data.globalValidity || '2. 5. – 15. 5. 2026'}`} - className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white placeholder:text-gray-400" - /> -
- -
-
- - { - const products = [...module.data.products]; - products[pIdx].priceMain = e.target.value; - updateData({ products }); - }} - className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].priceSub = e.target.value; - updateData({ products }); - }} - className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold" - /> -
-
- -
-
- - { - const products = [...module.data.products]; - products[pIdx].discountText = e.target.value; - updateData({ products }); - }} - className="w-full text-xs border border-gray-300 rounded p-1 bg-white" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].unitPrice = e.target.value; - updateData({ products }); - }} - placeholder="jednotková cena 9,43 EUR/kg" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white text-[10px]" - /> -
-
- - {/* Cena s kartou (voliteľné) – premenné {cena_s_kartou} a {zlava_s_kartou} v cenovke */} -
-
- - { - const products = [...module.data.products]; - products[pIdx].cardPriceMain = e.target.value; - updateData({ products }); - }} - placeholder="—" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].cardPriceSub = e.target.value; - updateData({ products }); - }} - placeholder="—" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].cardDiscount = e.target.value; - updateData({ products }); - }} - placeholder="—" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white" - /> -
-
- - {/* Predajne */} -
- -
- - - -
-
- - {/* Grafická cenovka a šablóna */} -
-
-
- - Cenovka -
- -
- - {/* Výber opakovane použiteľnej šablóny */} -
- - -
- - {/* Vlastná PNG cenovka (výnimka): prekryje šablónu, po odstránení sa vráti generovaná */} - { - const products = [...module.data.products]; - products[pIdx].customBadgePngUrl = url || undefined; - updateData({ products }); - }} - /> - - {/* Štítky predajní (Potraviny / Supermarket / Tempo) pri hornej hrane cenovky */} - { - const products = [...module.data.products]; - products[pIdx] = { ...products[pIdx], ...patch }; - updateData({ products }); - }} - /> - - {/* Tlačidlá aplikácie cenovky */} - {p.badgeCustomTemplate && ( -
- - {onApplyBadgeToAll && ( - - )} -
- )} - - {/* Poloha cenovky: pod / vedľa */} -
- Poloha cenovky -
- - -
-
- - {/* Rýchle ovládanie veľkosti (Width), zarovnania a paddingu */} -
- {/* Šírka */} -
-
- Veľkosť cenovky (Šírka) - - {p.badgeWidth || (p.badgeCustomTemplate ? 110 : 95)} px - -
- { - const products = [...module.data.products]; - products[pIdx].badgeWidth = Number(e.target.value); - updateData({ products }); - }} - className="w-full accent-red-600" - /> -
- - {/* Zarovnanie */} -
- Zarovnanie -
- - - -
-
- - {/* Padding zhora a zdola */} -
-
- - { - const products = [...module.data.products]; - products[pIdx].badgePaddingTop = Number(e.target.value); - updateData({ products }); - }} - className="w-full text-xs border border-gray-300 rounded-lg p-1 bg-white font-medium" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].badgePaddingBottom = Number(e.target.value); - updateData({ products }); - }} - className="w-full text-xs border border-gray-300 rounded-lg p-1 bg-white font-medium" - /> -
-
-
-
-
+ product={p} + index={pIdx} + variant="standard" + defaultOpen={pIdx === 0} + availableBadges={availableBadges || []} + onChange={(patch) => updateProduct(pIdx, patch)} + onDelete={() => updateData({ products: (module.data.products || []).filter((_: ProductItem, i: number) => i !== pIdx) })} + onOpenBadgeEditor={onOpenBadgeManager ? () => onOpenBadgeManager(p.id) : undefined} + onApplyBadgeToSection={() => applyBadgeToSection(p)} + onApplyBadgeToAll={onApplyBadgeToAll ? () => applyBadgeToAll(p, 'standard') : undefined} + /> ))}
@@ -1461,555 +834,24 @@ export const ConfigPanel: React.FC = ({ - {/* 2 Vernostné produkty */} -
- + {/* Vernostné produkty */} +
+ Vernostné produkty ({module.data.products?.length || 0}) - - {(module.data.products || []).map((p: any, pIdx: number) => ( -
- - {p.title || `Vernostný produkt ${pIdx + 1}`} - - -
- - { - const products = [...module.data.products]; - products[pIdx].title = e.target.value; - updateData({ products }); - }} - className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white font-medium" - /> -
- -
-
- - { - const products = [...module.data.products]; - products[pIdx].quantity = e.target.value; - updateData({ products }); - }} - placeholder="napr. 35 g" - className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].note = e.target.value; - updateData({ products }); - }} - placeholder="napr. Ilustračné foto" - className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].imageUrl = url; - products[pIdx].imageAspect = undefined; // nová fotka = nový pomer strán - updateData({ products }); - }} - altValue={p.imageAlt} - onAltChange={(alt) => { - const products = [...module.data.products]; - products[pIdx].imageAlt = alt; - updateData({ products }); - }} - altPlaceholder={pickAlt(p.title, module.data.sectionTitle, 'Produkt')} - placeholder="https://...produkt.png alebo nahrajte súbor" - /> - - { - const products = [...module.data.products]; - products[pIdx] = { ...products[pIdx], ...patch }; - updateData({ products }); - }} - /> - -
- - { - const products = [...module.data.products]; - products[pIdx].validityOverride = e.target.value; - updateData({ products }); - }} - placeholder={`Predvolená: ${module.data.globalValidity || '2. 5. – 15. 5. 2026'}`} - className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white placeholder:text-gray-400" - /> -
- - {/* Sekcia Bez karty */} -
- Bez karty -
-
- - { - const products = [...module.data.products]; - products[pIdx].noCardDiscount = e.target.value; - updateData({ products }); - }} - placeholder="21%" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].noCardPriceMain = e.target.value; - updateData({ products }); - }} - placeholder="0" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].noCardPriceSub = e.target.value; - updateData({ products }); - }} - placeholder="79" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold" - /> -
-
-
- - { - const products = [...module.data.products]; - products[pIdx].noCardUnitPrice = e.target.value; - updateData({ products }); - }} - placeholder="1kg = 9,88 €" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white" - /> -
-
- - {/* Sekcia S kartou */} -
- S kartou (Klub) -
-
- - { - const products = [...module.data.products]; - products[pIdx].cardDiscount = e.target.value; - updateData({ products }); - }} - placeholder="38%" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].cardPriceMain = e.target.value; - updateData({ products }); - }} - placeholder="0" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].cardPriceSub = e.target.value; - updateData({ products }); - }} - placeholder="62" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold" - /> -
-
-
- - { - const products = [...module.data.products]; - products[pIdx].cardUnitPrice = e.target.value; - updateData({ products }); - }} - placeholder="1kg = 7,75 €" - className="w-full text-xs border border-gray-300 rounded p-1 bg-white" - /> -
-
- - {/* Predajne */} -
- -
- - - -
-
- - {/* Grafická cenovka a šablóna pre vernostné produkty */} -
-
-
- - Cenovka -
- -
- - {/* Výber šablóny */} -
- - -
- - {/* Vlastná PNG cenovka (výnimka): prekryje šablónu, po odstránení sa vráti generovaná */} - { - const products = [...module.data.products]; - products[pIdx].customBadgePngUrl = url || undefined; - updateData({ products }); - }} - /> - - {/* Štítky predajní (Potraviny / Supermarket / Tempo) pri hornej hrane cenovky */} - { - const products = [...module.data.products]; - products[pIdx] = { ...products[pIdx], ...patch }; - updateData({ products }); - }} - /> - - {/* Tlačidlá aplikácie cenovky */} - {p.badgeCustomTemplate && ( -
- - {onApplyBadgeToAll && ( - - )} -
- )} - - {/* Poloha cenovky: pod / vedľa */} - {p.badgeCustomTemplate && ( -
- Poloha cenovky -
- - -
-
- )} - - {/* Rýchle ovládanie veľkosti (Width), zarovnania a paddingu pre vernostnú cenovku */} - {(p.badgeCustomTemplate || p.customBadgePngUrl) && ( -
- {/* Šírka */} -
-
- Veľkosť cenovky (Šírka) - - {p.badgeWidth || (p.badgeCustomTemplate ? 120 : 140)} px - -
- { - const products = [...module.data.products]; - products[pIdx].badgeWidth = Number(e.target.value); - updateData({ products }); - }} - className="w-full accent-red-600 cursor-pointer" - /> -
- - {/* Zarovnanie */} -
- - Zarovnanie cenovky - -
- - - -
-
- - {/* Padding zhora a zdola */} -
-
- - { - const products = [...module.data.products]; - products[pIdx].badgePaddingTop = Number(e.target.value); - updateData({ products }); - }} - className="w-full text-xs border border-gray-300 rounded-lg p-1 bg-white font-medium" - /> -
-
- - { - const products = [...module.data.products]; - products[pIdx].badgePaddingBottom = Number(e.target.value); - updateData({ products }); - }} - className="w-full text-xs border border-gray-300 rounded-lg p-1 bg-white font-medium" - /> -
-
-
- )} -
-
+ {(module.data.products || []).map((p: CardProduct, pIdx: number) => ( + updateProduct(pIdx, patch)} + onOpenBadgeEditor={onOpenBadgeManager ? () => onOpenBadgeManager(p.id) : undefined} + onApplyBadgeToSection={() => applyBadgeToSection(p)} + onApplyBadgeToAll={onApplyBadgeToAll ? () => applyBadgeToAll(p, 'loyalty') : undefined} + /> ))}
diff --git a/src/components/editor/ProductEditor.tsx b/src/components/editor/ProductEditor.tsx new file mode 100644 index 0000000..730e274 --- /dev/null +++ b/src/components/editor/ProductEditor.tsx @@ -0,0 +1,410 @@ +'use client'; + +// 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, { useState } from 'react'; +import { BadgeTemplate, CardProduct } from '@/types/newsletter'; +import { ChevronDown, Trash2, Pencil, Globe, Layers } from 'lucide-react'; +import { ImageUploadInput } from './ImageUploadInput'; +import { pickAlt } from '@/lib/altText'; +import { DEFAULT_STORES_OFFSET_Y, badgeWidthFor, productCardAlt } from '@/lib/productCard'; + +export const PRODUCT_BG_PRESETS = [ + { label: 'Červená', sub: 'Fantasticky', color: '#feeaea', border: '#f5d4d4' }, + { label: 'Žltá', sub: 'Domáce', color: '#fef0c7', border: '#e6d49a' }, + { label: 'Hnedá', sub: 'Gril', color: '#dcb085', border: '#c49a6c' }, + { label: 'Sivá', sub: 'Neutrálna', color: '#e6e1d9', border: '#d5cfc5' }, + { label: 'Zelená', sub: 'Bio', color: '#e3f0e4', border: '#c8e6c9' }, + { label: 'Biela', sub: 'Čistá', color: '#ffffff', border: '#e5e7eb' } +]; + +type Variant = 'standard' | 'loyalty'; +// Textové polia cien produktu (bežné aj vernostné) +type TextKey = + | 'priceMain' | 'priceSub' | 'discountText' + | 'cardPriceMain' | 'cardPriceSub' | 'cardDiscount' | 'cardUnitPrice' + | 'noCardPriceMain' | 'noCardPriceSub' | 'noCardDiscount' | 'noCardUnitPrice'; +type Patch = Record; + +interface ProductEditorProps { + product: CardProduct; + index: number; + variant: Variant; + availableBadges: BadgeTemplate[]; + defaultOpen?: boolean; + onChange: (patch: Patch) => void; + onDelete?: () => void; + onOpenBadgeEditor?: () => void; + onApplyBadgeToSection?: () => void; + onApplyBadgeToAll?: () => void; +} + +const inputCls = 'w-full text-xs border border-gray-300 rounded-lg px-2 py-1.5 bg-white'; + +const Field: React.FC<{ label: string; children: React.ReactNode; className?: string }> = ({ label, children, className }) => ( + +); + +const TextInput: React.FC<{ value?: string; onChange: (v: string) => void; placeholder?: string; bold?: boolean }> = ({ + value, + onChange, + placeholder, + bold +}) => ( + onChange(e.target.value)} + placeholder={placeholder} + className={`${inputCls} ${bold ? 'font-bold' : ''}`} + /> +); + +const Slider: React.FC<{ + label: string; + value: number; + display: string; + min: number; + max: number; + step: number; + onChange: (v: number) => void; +}> = ({ label, value, display, min, max, step, onChange }) => ( +
+ {label} + onChange(Number(e.target.value))} + className="w-full h-1 accent-red-600 cursor-pointer" + /> + {display} +
+); + +const Block: React.FC<{ title: string; action?: React.ReactNode; children: React.ReactNode }> = ({ title, action, children }) => ( +
+
+ {title} + {action} +
+ {children} +
+); + +const ResetLink: React.FC<{ onClick: () => void }> = ({ onClick }) => ( + +); + +// Šablóna používa ceny s kartou → zobrazíme aj polia s kartou +const usesCardPrice = (t?: BadgeTemplate) => !!t && JSON.stringify(t.layersConfig || []).includes('_s_kartou'); + +export const ProductEditor: React.FC = ({ + product: p, + index, + variant, + availableBadges, + defaultOpen = false, + onChange, + onDelete, + onOpenBadgeEditor, + onApplyBadgeToSection, + onApplyBadgeToAll +}) => { + const [open, setOpen] = useState(defaultOpen); + const isLoyalty = variant === 'loyalty'; + const set = (key: string) => (v: unknown) => onChange({ [key]: v }); + + const scale = p.imageScale ?? 1; + const offsetX = p.imageOffsetX || 0; + const offsetY = p.imageOffsetY || 0; + const storesX = p.storesOffsetX || 0; + const storesY = p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y; + const hasStores = !!(p.stores?.potraviny || p.stores?.supermarket || p.stores?.tempo); + const badgeWidth = badgeWidthFor(p, variant); + + const selectBadge = (id: string) => { + if (id === 'default') { + onChange({ badgeCustomTemplate: undefined, badgeId: undefined, customBadgePngUrl: undefined, badgeWidth: undefined }); + return; + } + const found = availableBadges.find((b) => b.id === id); + if (found) { + onChange({ badgeCustomTemplate: found, badgeId: found.id, badgeWidth: found.width ?? 110, customBadgePngUrl: undefined }); + } + }; + + const text = (key: TextKey) => p[key] as string | undefined; + + const priceRow = (label: string, mainKey: TextKey, subKey: TextKey, discountKey: TextKey, unitKey?: TextKey) => ( +
+ {label} +
+ + + + + + + + + +
+ {unitKey && ( + + + + )} +
+ ); + + return ( +
+ {/* Hlavička produktu – klik rozbalí / zbalí */} +
+ + {onDelete && ( + + )} +
+ + {open && ( +
+ {/* TEXTY */} + + +