From 98fdad535da4f677900c80483c5fe05b10beef7d Mon Sep 17 00:00:00 2001 From: Jandl-hostacny Date: Mon, 19 Oct 2026 16:42:59 +0200 Subject: [PATCH] feat: lista platnosti akcie nad produktmi, platnost na kartach predvolene skryta - novy blok Lista platnosti akcie (vzhlad ako oznamovacia lista, bez badge a CTA): text + globalna platnost, da sa skryt a posuvat, nie zmazat; do existujucich kampani sa vlozi automaticky nad prvu produktovu sekciu - platnost na kartach produktov sa da zobrazit prepinacom (predvolene skryta), produkty s vlastnou platnostou ju zobrazia vzdy Co-Authored-By: Claude Opus 5.5 --- src/app/page.tsx | 106 +++++++++++++++++++------- src/components/editor/ConfigPanel.tsx | 84 +++++++++++++++++--- src/lib/defaultCampaign.ts | 23 +++++- src/lib/emailRenderer.ts | 36 ++++++++- src/types/newsletter.ts | 8 ++ 5 files changed, 213 insertions(+), 44 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index a13d593..4962213 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,8 +2,8 @@ import React, { useState, useEffect } from 'react'; import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter'; -import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign'; -import { renderEmailHtml, renderSingleModuleHtml } from '@/lib/emailRenderer'; +import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE, ensureValidityBar } from '@/lib/defaultCampaign'; +import { renderEmailHtml, renderSingleModuleHtml, getCampaignValidity } from '@/lib/emailRenderer'; import { supabase } from '@/lib/supabase'; import { DEFAULT_BADGE_TEMPLATES } from '@/lib/badgeUtils'; import { getBrevoCampaignUrl } from '@/lib/brevo'; @@ -142,7 +142,7 @@ export default function Home() { preheader: dbItem.data.preheader || DEFAULT_CAMPAIGN.preheader, status: dbItem.status, brevoCampaignId: dbItem.brevo_campaign_id, - modules: enforceAnchors(dbItem.data.modules) + modules: enforceAnchors(ensureValidityBar(dbItem.data.modules)) }); if (dbItem.brevo_campaign_id) { setBrevoUrl(getBrevoCampaignUrl(dbItem.brevo_campaign_id)); @@ -234,7 +234,7 @@ export default function Home() { // Duplikácia: Ukotvené moduly sa nedajú duplikovať const duplicateModule = (index: number) => { const original = campaign.modules[index]; - if (isAnchoredModule(original)) return; + if (isAnchoredModule(original) || original.type === 'validity_bar') return; const clone: NewsletterModule = { ...original, @@ -256,6 +256,10 @@ export default function Home() { alert('Hlavičku, letáky, články/recepty ani pätičku nie je možné vymazať.'); return; } + if (mod.type === 'validity_bar') { + alert('Lištu platnosti akcie nie je možné vymazať, dá sa len skryť.'); + return; + } if (campaign.modules.length <= 1) return; const newModules = campaign.modules.filter((_, i) => i !== index); @@ -591,14 +595,15 @@ export default function Home() { })); }; - // Globálna platnosť akcie: prepíše sa do všetkých produktových sekcií aj vernostných zliav + // Spoločné nastavenia (globálna platnosť akcie, zobrazenie platnosti na kartách) sa prepíšu + // do všetkých produktových sekcií aj vernostných zliav // (produkty s vlastnou platnosťou validityOverride si ju ponechajú) - const handleSetGlobalValidity = (validity: string) => { + const handleUpdateAllProductSections = (patch: Record) => { setCampaign((prev) => ({ ...prev, modules: prev.modules.map((m) => m.type === 'products_grid' || m.type === 'loyalty_club' - ? { ...m, data: { ...m.data, globalValidity: validity } } + ? { ...m, data: { ...m.data, ...patch } } : m ) })); @@ -704,6 +709,26 @@ export default function Home() { ) : ( <> + {mod.type === 'validity_bar' && ( + + )} - - + {mod.type !== 'validity_bar' && ( + <> + + + + )} )} @@ -770,6 +799,24 @@ export default function Home() { Aktivovať vernostné zľavy + ) : mod.type === 'validity_bar' && mod.data.enabled === false ? ( +
+

+ Lišta platnosti akcie (Skrytá) +

+

+ Táto lišta je v e-maile skrytá. Kliknutím sem ju môžete aktivovať. +

+ +
) : mod.type === 'announcement' && mod.data.enabled === false ? (

@@ -808,7 +855,8 @@ export default function Home() { module={selectedModule} onUpdateModule={handleUpdateModule} onApplyBadgeToAll={handleApplyBadgeToAll} - onSetGlobalValidity={handleSetGlobalValidity} + onUpdateAllProductSections={handleUpdateAllProductSections} + campaignValidity={getCampaignValidity(campaign)} availableBadges={availableBadges} onOpenBadgeManager={(pId) => { setTargetProductIdForBadge(pId); @@ -846,7 +894,7 @@ export default function Home() { onClose={() => setIsSavedCampaignsOpen(false)} currentCampaign={campaign} onLoadCampaign={(selected: NewsletterCampaign) => { - setCampaign({ ...selected, modules: enforceAnchors(selected.modules) }); + setCampaign({ ...selected, modules: enforceAnchors(ensureValidityBar(selected.modules)) }); setSelectedModuleId(selected.modules[1]?.id || selected.modules[0]?.id || null); if (selected.brevoCampaignId) { setBrevoUrl(getBrevoCampaignUrl(selected.brevoCampaignId)); diff --git a/src/components/editor/ConfigPanel.tsx b/src/components/editor/ConfigPanel.tsx index 3c550f2..2ce3ab5 100644 --- a/src/components/editor/ConfigPanel.tsx +++ b/src/components/editor/ConfigPanel.tsx @@ -21,7 +21,9 @@ interface ConfigPanelProps { module: NewsletterModule | null; onUpdateModule: (updated: NewsletterModule) => void; onApplyBadgeToAll?: (badge: BadgeTemplate, badgeLayout?: 'below' | 'side') => void; - onSetGlobalValidity?: (validity: string) => void; // nastaví platnosť vo všetkých produktových sekciách + // 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) onOpenBadgeManager?: (productId: string) => void; onOpenImport?: () => void; availableBadges?: BadgeTemplate[]; @@ -183,7 +185,8 @@ export const ConfigPanel: React.FC = ({ module, onUpdateModule, onApplyBadgeToAll, - onSetGlobalValidity, + onUpdateAllProductSections, + campaignValidity = '', onOpenBadgeManager, onOpenImport, availableBadges = [] @@ -196,6 +199,9 @@ export const ConfigPanel: React.FC = ({ ); } + const updateAllProductSections = (patch: Record) => + onUpdateAllProductSections ? onUpdateAllProductSections(patch) : updateData(patch); + const updateData = (newData: any) => { onUpdateModule({ ...module, @@ -280,6 +286,50 @@ export const ConfigPanel: React.FC = ({

)} + {/* LIŠTA PLATNOSTI AKCIE */} + {module.type === 'validity_bar' && ( +
+
+
+ Zobraziť v e-maile + Lišta platnosti akcie +
+ +
+ +
+ + updateData({ text: e.target.value })} + placeholder="napr. Akcia platí" + className="w-full text-sm border border-gray-300 rounded-lg p-2" + /> +
+ +
+ + updateAllProductSections({ globalValidity: e.target.value })} + placeholder="napr. 2. 5. – 15. 5. 2026" + className="w-full text-sm border border-gray-300 rounded-lg p-2" + /> +

Rovnaká platnosť ako v produktových sekciách – zmena sa prejaví všade.

+
+
+ )} + {/* Vlastné HTML pre hlavičku a pätičku */} {CUSTOM_HTML_MODULE_TYPES.includes(module.type) && ( @@ -503,15 +553,20 @@ export const ConfigPanel: React.FC = ({ - onSetGlobalValidity - ? onSetGlobalValidity(e.target.value) - : updateData({ globalValidity: e.target.value }) - } + onChange={(e) => updateAllProductSections({ globalValidity: e.target.value })} placeholder="napr. 2. 5. – 15. 5. 2026" className="w-full text-sm border border-gray-300 rounded-lg p-2" />

Platí pre všetky produktové sekcie aj vernostné zľavy. Produkty s vlastnou platnosťou si ju ponechajú.

+ {/* Rozloženie produktov (Počet produktov na riadok) */} @@ -1212,15 +1267,20 @@ export const ConfigPanel: React.FC = ({ - onSetGlobalValidity - ? onSetGlobalValidity(e.target.value) - : updateData({ globalValidity: e.target.value }) - } + onChange={(e) => updateAllProductSections({ globalValidity: e.target.value })} placeholder="napr. 2. 5. – 15. 5. 2026" className="w-full text-sm border border-gray-300 rounded-lg p-2" />

Platí pre všetky produktové sekcie aj vernostné zľavy. Produkty s vlastnou platnosťou si ju ponechajú.

+ {/* Farba pozadia vernostných kariet */} diff --git a/src/lib/defaultCampaign.ts b/src/lib/defaultCampaign.ts index b42abb2..4c72a11 100644 --- a/src/lib/defaultCampaign.ts +++ b/src/lib/defaultCampaign.ts @@ -1,9 +1,29 @@ -import { NewsletterCampaign, ProductItem } from '@/types/newsletter'; +import { NewsletterCampaign, NewsletterModule, ProductItem } from '@/types/newsletter'; // Verejné úložisko statických obrázkov (logá, ikony, štítky predajní...) v Supabase projekte z .env. // Súbory sú v supabase/assets/ a nahrajú sa príkazom: npm run db:upload-assets export const SUPABASE_ASSETS_BASE = `${(process.env.NEXT_PUBLIC_SUPABASE_URL || '').replace(/\/$/, '')}/storage/v1/object/public/newsletter-assets/`; +// Lišta platnosti akcie nad produktmi – dá sa skryť a posúvať, nie zmazať +export const VALIDITY_BAR_MODULE: NewsletterModule = { + id: 'mod-validity-bar', + type: 'validity_bar', + title: 'Lišta platnosti akcie', + data: { + text: 'Akcia platí', + enabled: true + } +}; + +// Staršie kampane bez lišty platnosti: vloží ju nad prvú produktovú sekciu +export function ensureValidityBar(modules: NewsletterModule[]): NewsletterModule[] { + if (modules.some((m) => m.type === 'validity_bar')) return modules; + const idx = modules.findIndex((m) => m.type === 'products_grid' || m.type === 'loyalty_club'); + const bar = { ...VALIDITY_BAR_MODULE, data: { ...VALIDITY_BAR_MODULE.data } }; + if (idx === -1) return modules; + return [...modules.slice(0, idx), bar, ...modules.slice(idx)]; +} + export const DEFAULT_CAMPAIGN: NewsletterCampaign = { id: '', title: 'COOP Jednota Newsletter', @@ -67,6 +87,7 @@ export const DEFAULT_CAMPAIGN: NewsletterCampaign = { buttonBgColor: '#003399' } }, + VALIDITY_BAR_MODULE, { id: 'mod-products-fantasticky', type: 'products_grid', diff --git a/src/lib/emailRenderer.ts b/src/lib/emailRenderer.ts index 1842358..ff9a02f 100644 --- a/src/lib/emailRenderer.ts +++ b/src/lib/emailRenderer.ts @@ -203,6 +203,14 @@ export function renderSingleModuleHtml(m: any, campaign: NewsletterCampaign): st return renderModule(m, campaign); } +// Globálna platnosť akcie kampane (zdieľaná všetkými produktovými sekciami) +export function getCampaignValidity(campaign: NewsletterCampaign): string { + const mod = (campaign.modules || []).find( + (m) => (m.type === 'products_grid' || m.type === 'loyalty_club') && m.data?.globalValidity + ); + return mod?.data.globalValidity || ''; +} + // Moduly, pri ktorých sa dá vygenerované HTML nahradiť vlastným (HTML editor) export const CUSTOM_HTML_MODULE_TYPES = ['header', 'footer']; @@ -250,6 +258,28 @@ function renderModule(m: any, campaign: NewsletterCampaign): string { `; + case 'validity_bar': { + if (m.data?.enabled === false) { + return ''; + } + const validityText = [m.data.text, getCampaignValidity(campaign)].filter(Boolean).join(' '); + return ` + + + + + +
+ + + + + +
`; + } + case 'header': const rawNav = m.data.navLinks && m.data.navLinks.length > 0 ? m.data.navLinks : [ { label: 'Letáky', url: 'https://www.coop.sk/sk/letaky', variant: 'outline' }, @@ -314,7 +344,9 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
 
`; case 'products_grid': - const globalValidity = m.data.globalValidity || ''; + // 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 products: ProductItem[] = m.data.products || []; const cardBg = m.data.cardBgColor || '#feeaea'; const cardBorder = m.data.cardBorderColor || '#f5d4d4'; @@ -406,7 +438,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string { ${lProducts.map((p: any, idx: number) => ` - ${renderExactLoyaltyCard(p, m.data.globalValidity || '2. 5. – 15. 5. 2026')} + ${renderExactLoyaltyCard(p, m.data.showCardValidity === true ? (m.data.globalValidity || '2. 5. – 15. 5. 2026') : '')} `).join('\n')} diff --git a/src/types/newsletter.ts b/src/types/newsletter.ts index 4f2dc6f..dd04159 100644 --- a/src/types/newsletter.ts +++ b/src/types/newsletter.ts @@ -7,6 +7,7 @@ export type ModuleType = | 'products_grid' | 'loyalty_club' | 'articles_row' + | 'validity_bar' | 'footer'; export interface StoreTag { @@ -60,6 +61,12 @@ export interface AnnouncementModuleData { enabled?: boolean; } +// Lišta platnosti akcie nad produktmi (vzhľad ako oznamovacia lišta, bez badge a CTA) +export interface ValidityBarModuleData { + text: string; // text pred dátumom, napr. "Akcia platí" + enabled?: boolean; // false = skrytá v e-maile +} + export interface HeaderModuleData { logoUrl: string; logoLink: string; @@ -104,6 +111,7 @@ export interface ProductsGridModuleData { cardBgColor?: string; cardBorderColor?: string; columns?: 1 | 2; + showCardValidity?: boolean; // platnosť na kartách produktov (predvolene skrytá) } export interface LoyaltyClubModuleData {