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 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-19 16:42:59 +02:00
1 parent 5f6a39debe
commit 98fdad535d
5 files changed
+213 -44

No files matched your search

+77 -29
View File
@@ -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<string, unknown>) => {
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() {
</div>
) : (
<>
{mod.type === 'validity_bar' && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleUpdateModule({
...mod,
data: { ...mod.data, enabled: mod.data.enabled === false }
});
}}
className={`text-[9px] font-bold px-2 py-0.5 rounded-full border transition-all mr-1 ${
mod.data.enabled === false
? 'bg-red-500/20 text-red-300 border-red-400/30 hover:bg-red-500/30'
: 'bg-emerald-500/20 text-emerald-300 border-emerald-400/30 hover:bg-emerald-500/30'
}`}
title={mod.data.enabled === false ? 'Zobraziť lištu v e-maile' : 'Skryť lištu v e-maile'}
>
{mod.data.enabled === false ? 'Skrytá (zapnúť)' : 'Zobrazená'}
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
@@ -726,26 +751,30 @@ export default function Home() {
>
<ChevronDown className="w-3.5 h-3.5" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
duplicateModule(idx);
}}
className="hover:text-blue-400 p-1 ml-1"
title="Duplikovať sekciu"
>
<Copy className="w-3.5 h-3.5" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
deleteModule(idx);
}}
className="hover:text-red-400 p-1 ml-1"
title="Zmazať sekciu"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
{mod.type !== 'validity_bar' && (
<>
<button
onClick={(e) => {
e.stopPropagation();
duplicateModule(idx);
}}
className="hover:text-blue-400 p-1 ml-1"
title="Duplikovať sekciu"
>
<Copy className="w-3.5 h-3.5" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
deleteModule(idx);
}}
className="hover:text-red-400 p-1 ml-1"
title="Zmazať sekciu"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</>
)}
</>
)}
</div>
@@ -770,6 +799,24 @@ export default function Home() {
Aktivovať vernostné zľavy
</button>
</div>
) : mod.type === 'validity_bar' && mod.data.enabled === false ? (
<div className="p-4 my-2 border-2 border-dashed border-red-500/40 bg-red-950/30 rounded-xl text-center">
<p className="text-xs font-bold text-red-300 uppercase tracking-wider">
Lišta platnosti akcie (Skrytá)
</p>
<p className="text-[11px] text-gray-300 mt-0.5">
Táto lišta je v e-maile skrytá. Kliknutím sem ju môžete aktivovať.
</p>
<button
onClick={(e) => {
e.stopPropagation();
handleUpdateModule({ ...mod, data: { ...mod.data, enabled: true } });
}}
className="mt-2 px-3 py-1 bg-red-600 hover:bg-red-700 text-white text-xs font-bold rounded-lg transition-colors shadow-sm"
>
Aktivovať lištu platnosti
</button>
</div>
) : mod.type === 'announcement' && mod.data.enabled === false ? (
<div className="p-4 my-2 border-2 border-dashed border-red-500/40 bg-red-950/30 rounded-xl text-center">
<p className="text-xs font-bold text-red-300 uppercase tracking-wider">
@@ -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));