Files
Coop-NL/src/app/page.tsx
T
Jakub_HostacnyandClaude Opus 5.5 c426145c48 feat: klik na produkt v nahlade otvori jeho editor (harmonika)
- v paneli je otvoreny vzdy len jeden produkt, ostatne su zbalene
- klik na kartu produktu v nahlade vyberie sekciu aj produkt, panel sa
  posunie na jeho editor a karta sa v nahlade zvyrazni
- odkazy v nahlade sa klikom neotvaraju

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:52:09 +02:00

982 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter';
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';
import { TopBar } from '@/components/editor/TopBar';
import { ConfigPanel } from '@/components/editor/ConfigPanel';
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';
export default function Home() {
const [campaign, setCampaign] = useState<NewsletterCampaign>(DEFAULT_CAMPAIGN);
const [selectedModuleId, setSelectedModuleId] = useState<string | null>('mod-products-fantasticky');
// Produkt otvorený v paneli (harmonika); nastaví sa aj klikom na produkt v náhľade
const [selectedProductId, setSelectedProductId] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [isExportingBrevo, setIsExportingBrevo] = useState(false);
const [brevoUrl, setBrevoUrl] = useState<string | null>(null);
// Modály
const [isImportOpen, setIsImportOpen] = useState(false);
const [isBadgeEditorOpen, setIsBadgeEditorOpen] = useState(false);
const [isSavedCampaignsOpen, setIsSavedCampaignsOpen] = useState(false);
const [targetProductIdForBadge, setTargetProductIdForBadge] = useState<string | null>(null);
const [availableBadges, setAvailableBadges] = useState<BadgeTemplate[]>(DEFAULT_BADGE_TEMPLATES);
// Načítanie dostupných šablón cenoviek pre opakované použitie
useEffect(() => {
const fetchBadges = async () => {
let loaded: BadgeTemplate[] = [];
try {
const { data, error } = await supabase
.from('badges')
.select('*')
.order('created_at', { ascending: false });
if (data && !error && data.length > 0) {
loaded = data.map((d: any) => {
const isObj = d.layers_config && !Array.isArray(d.layers_config);
return {
id: d.id,
name: d.name,
svgRaw: d.svg_raw || '',
layersConfig: isObj ? (d.layers_config.layers || []) : (d.layers_config || []),
width: isObj ? (d.layers_config.width || 110) : 110,
align: isObj ? (d.layers_config.align || 'center') : 'center',
paddingTop: isObj ? (d.layers_config.paddingTop ?? 4) : 4,
paddingBottom: isObj ? (d.layers_config.paddingBottom ?? 4) : 4
};
});
}
} catch (e) {
console.warn('Error fetching badges in page:', e);
}
try {
const local = localStorage.getItem('coop_custom_badges');
if (local) {
const parsed = JSON.parse(local);
if (Array.isArray(parsed)) {
const existingIds = new Set(loaded.map((b) => b.id));
for (const b of parsed) {
if (!existingIds.has(b.id)) {
loaded.push(b);
existingIds.add(b.id);
}
}
}
}
} catch (e) {}
const all = [...loaded];
const existingIds = new Set(all.map((b) => b.id));
for (const preset of DEFAULT_BADGE_TEMPLATES) {
if (!existingIds.has(preset.id)) {
all.push(preset);
}
}
setAvailableBadges(all);
};
fetchBadges();
}, [isBadgeEditorOpen]);
// Pomocné funkcie pre fixné ukotvenie hlavičky, spodných letákov, receptov a pätičky
const isTopModule = (m: NewsletterModule) => m.type === 'header' || m.type === 'announcement';
const isFlyersModule = (m: NewsletterModule) =>
m.id === 'mod-flyers-row' ||
(m.type === 'articles_row' && m.title?.toLowerCase().includes('leták'));
const isRecipesModule = (m: NewsletterModule) =>
m.id === 'mod-recipes-row' ||
(m.type === 'articles_row' &&
(m.title?.toLowerCase().includes('recept') || m.title?.toLowerCase().includes('článk')));
const isFooterModule = (m: NewsletterModule) => m.type === 'footer';
const isBottomModule = (m: NewsletterModule) =>
isFlyersModule(m) || isRecipesModule(m) || isFooterModule(m);
const isAnchoredModule = (m: NewsletterModule) => isTopModule(m) || isBottomModule(m);
const enforceAnchors = (mods: NewsletterModule[]): NewsletterModule[] => {
const topMods = mods.filter((m) => isTopModule(m));
const flyerMods = mods.filter((m) => isFlyersModule(m));
const recipeMods = mods.filter((m) => isRecipesModule(m));
const footerMods = mods.filter((m) => isFooterModule(m));
const middleMods = mods.filter((m) => !isAnchoredModule(m));
// announcement je pred headerom, ak existujú oba
topMods.sort((a, b) => {
if (a.type === 'announcement' && b.type === 'header') return -1;
if (a.type === 'header' && b.type === 'announcement') return 1;
return 0;
});
return [...topMods, ...middleMods, ...flyerMods, ...recipeMods, ...footerMods];
};
// Auto-save sa spustí až po načítaní kampane z databázy. Inak by sa pri pomalšom načítaní
// predvolená kampaň uložila ako nová kópia a ďalšie zmeny by sa ukladali do nej.
const [isLoaded, setIsLoaded] = useState(false);
// Prebiehajúce vloženie novej kampane – kým nemáme jej id, ďalšie vloženie by vytvorilo duplikát
const insertInFlight = useRef(false);
// Načítanie existujúceho draftu z databázy pri štarte
useEffect(() => {
async function loadInitial() {
try {
const { data } = await supabase
.from('templates')
.select('*')
.order('updated_at', { ascending: false })
.limit(1);
if (data && data.length > 0) {
const dbItem = data[0];
if (Array.isArray(dbItem.data?.modules) && dbItem.data.modules.length > 0) {
setCampaign({
id: dbItem.id,
title: dbItem.title,
preheader: dbItem.data.preheader || DEFAULT_CAMPAIGN.preheader,
status: dbItem.status,
brevoCampaignId: dbItem.brevo_campaign_id,
modules: enforceAnchors(ensureValidityBar(dbItem.data.modules))
});
if (dbItem.brevo_campaign_id) {
setBrevoUrl(getBrevoCampaignUrl(dbItem.brevo_campaign_id));
}
}
}
} catch (err) {
console.error('Error loading template from Supabase:', err);
} finally {
setIsLoaded(true);
}
}
loadInitial();
}, []);
// Auto-save s debounce (1 sekunda)
useEffect(() => {
if (!isLoaded) return;
const timer = setTimeout(async () => {
if (!campaign.id && insertInFlight.current) return;
setIsSaving(true);
try {
if (!campaign.id) {
insertInFlight.current = true;
const { data } = await supabase
.from('templates')
.insert([
{
title: campaign.title,
status: campaign.status,
data: {
preheader: campaign.preheader,
modules: campaign.modules
}
}
])
.select();
if (data && data.length > 0) {
// id nastavíme len ak medzitým nebola otvorená iná kampaň
setCampaign((prev) => (prev.id ? prev : { ...prev, id: data[0].id }));
}
} else {
await supabase
.from('templates')
.update({
title: campaign.title,
data: {
preheader: campaign.preheader,
modules: campaign.modules
},
updated_at: new Date().toISOString()
})
.eq('id', campaign.id);
}
} catch (err) {
console.error('Auto-save error:', err);
} finally {
insertInFlight.current = false;
setIsSaving(false);
}
}, 1000);
return () => clearTimeout(timer);
}, [isLoaded, campaign.title, campaign.preheader, campaign.modules, campaign.id]);
const selectedModule = campaign.modules.find((m) => m.id === selectedModuleId) || null;
const handleUpdateModule = (updated: NewsletterModule) => {
setCampaign((prev) => ({
...prev,
modules: prev.modules.map((m) => (m.id === updated.id ? updated : m))
}));
};
// Posun modulu: Hlavička, letáky, recepty a pätička sú ukotvené a bežné moduly sa nemôžu dostať mimo vyhradený stred
const moveModule = (index: number, direction: 'up' | 'down') => {
const currentMod = campaign.modules[index];
if (!currentMod || isAnchoredModule(currentMod)) return;
const topCount = campaign.modules.filter((m) => isTopModule(m)).length;
const firstBottomIndex = campaign.modules.findIndex((m) => isBottomModule(m));
const maxAllowedIndex = firstBottomIndex !== -1 ? firstBottomIndex - 1 : campaign.modules.length - 1;
const targetIndex = direction === 'up' ? index - 1 : index + 1;
if (targetIndex < topCount || targetIndex > maxAllowedIndex) return;
const newModules = [...campaign.modules];
const temp = newModules[index];
newModules[index] = newModules[targetIndex];
newModules[targetIndex] = temp;
setCampaign((prev) => ({ ...prev, modules: enforceAnchors(newModules) }));
};
// Duplikácia: Ukotvené moduly sa nedajú duplikovať
const duplicateModule = (index: number) => {
const original = campaign.modules[index];
if (isAnchoredModule(original) || original.type === 'validity_bar') return;
const clone: NewsletterModule = {
...original,
id: `mod-${Date.now()}`,
title: `${original.title} (kópia)`,
data: JSON.parse(JSON.stringify(original.data))
};
const newModules = [...campaign.modules];
newModules.splice(index + 1, 0, clone);
setCampaign((prev) => ({ ...prev, modules: enforceAnchors(newModules) }));
setSelectedModuleId(clone.id);
};
// Zmazanie: Ukotvené moduly (hlavička, letáky, články/recepty, pätička) sa nedajú vymazať
const deleteModule = (index: number) => {
const mod = campaign.modules[index];
if (isAnchoredModule(mod)) {
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);
setCampaign((prev) => ({ ...prev, modules: enforceAnchors(newModules) }));
setSelectedModuleId(newModules[0]?.id || null);
};
// Pridanie modulu: Vloží sa vždy PRED spodné bloky (letáky, recepty a pätičku)
const handleAddModule = (type: any) => {
let newMod: NewsletterModule;
if (type === 'banner') {
newMod = {
id: `mod-banner-${Date.now()}`,
type: 'banner',
title: 'Nový Banner',
data: {
imageUrl: `${SUPABASE_ASSETS_BASE}aktualne_letaky.jpg`,
linkUrl: '#',
altText: 'Aktuálne letáky',
title: 'Prelistujte si naše najnovšie letáky online',
description: 'Všetky akcie a zľavy na jednom mieste priamo vo vašom mobile.',
buttonText: 'Pozrieť letáky',
buttonUrl: '#',
buttonBgColor: '#e2001a'
}
};
} else {
newMod = {
id: `mod-products-${Date.now()}`,
type: 'products_grid',
title: 'Nová sekcia produktov',
data: {
sectionTitle: 'AKČNÁ PONUKA',
globalValidity: '2. 5. – 15. 5. 2026',
cardBgColor: '#feeaea',
cardBorderColor: '#f5d4d4',
badgeHeaderBg: '#fefefe',
badgeHeaderColor: '#000000',
badgePriceBg: '#e2001a',
badgePriceColor: '#fefefe',
products: [
{
id: `p-${Date.now()}-1`,
title: 'Nový produkt 1',
imageUrl: `${SUPABASE_ASSETS_BASE}produkt_5.png`,
priceMain: '2',
priceSub: '49',
unitPrice: '1kg = 9,90 €',
discountText: '15%',
badgeText: 'zľava',
cardBgColor: '#feeaea',
stores: { potraviny: true, supermarket: true, tempo: true }
}
]
}
};
}
setCampaign((prev) => {
const firstBottomIdx = prev.modules.findIndex((m) => isBottomModule(m));
const newModules = [...prev.modules];
if (firstBottomIdx !== -1) {
newModules.splice(firstBottomIdx, 0, newMod);
} else {
newModules.push(newMod);
}
return { ...prev, modules: enforceAnchors(newModules) };
});
setSelectedModuleId(newMod.id);
};
const handleImportProducts = (imported: ProductItem[]) => {
if (!selectedModuleId) return;
const target = campaign.modules.find((m) => m.id === selectedModuleId);
if (!target || target.type !== 'products_grid') {
alert('Pre import prosím najskôr označte produktovú sekciu.');
return;
}
const updated = {
...target,
data: {
...target.data,
products: [...(target.data.products || []), ...imported]
}
};
handleUpdateModule(updated);
};
// Manuálne uloženie kampane
const handleManualSave = async () => {
setIsSaving(true);
try {
if (!campaign.id) {
const { data, error } = await supabase
.from('templates')
.insert([
{
title: campaign.title,
status: campaign.status,
data: {
preheader: campaign.preheader,
modules: campaign.modules
}
}
])
.select();
if (error) throw error;
if (data && data.length > 0) {
setCampaign((prev) => ({ ...prev, id: data[0].id }));
}
} else {
const { error } = await supabase
.from('templates')
.update({
title: campaign.title,
data: {
preheader: campaign.preheader,
modules: campaign.modules
},
updated_at: new Date().toISOString()
})
.eq('id', campaign.id);
if (error) throw error;
}
alert('Kampaň bola úspešne uložená do databázy!');
} catch (err: any) {
alert(`Chyba pri ukladaní: ${err.message}`);
} finally {
setIsSaving(false);
}
};
// Duplikácia aktuálnej kampane v Supabase
const handleDuplicateCurrentCampaign = async () => {
setIsSaving(true);
try {
const newTitle = `${campaign.title} (kópia)`;
const { data, error } = await supabase
.from('templates')
.insert([
{
title: newTitle,
status: 'draft',
data: {
preheader: campaign.preheader,
modules: campaign.modules
}
}
])
.select();
if (error) throw error;
if (data && data.length > 0) {
setCampaign({
id: data[0].id,
title: newTitle,
preheader: campaign.preheader,
status: 'draft',
modules: campaign.modules
});
alert(`Kampaň bola zduplikovaná ako "${newTitle}"!`);
}
} catch (err: any) {
alert(`Chyba pri duplikovaní: ${err.message}`);
} finally {
setIsSaving(false);
}
};
// Pred exportom sa vizuály produktových kariet uložia ako obrázky (nezmenené sa nenahrávajú znova)
const prepareForExport = async (): Promise<NewsletterCampaign> => {
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 prepared = await prepareForExport();
const rawHtml = renderEmailHtml(prepared);
let finalHtml = rawHtml;
try {
const res = await fetch('/api/inline-css', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: rawHtml })
});
const data = await res.json();
if (data.html) finalHtml = data.html;
} catch (e) {
console.warn('Inliner fallback:', e);
}
const zip = new JSZip();
zip.file('index.html', finalHtml);
const blob = await zip.generateAsync({ type: 'blob' });
saveAs(blob, `coop-newsletter-${new Date().toISOString().split('T')[0]}.zip`);
};
const handleExportBrevo = async (updateExisting = false) => {
setIsExportingBrevo(true);
try {
const prepared = await prepareForExport();
const rawHtml = renderEmailHtml(prepared);
let finalHtml = rawHtml;
try {
const res = await fetch('/api/inline-css', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: rawHtml })
});
const data = await res.json();
if (data.html) finalHtml = data.html;
} catch (e) {}
const response = await fetch('/api/brevo/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
campaignName: `${campaign.title} - ${new Date().toLocaleDateString('sk-SK')}`,
subject: campaign.title,
htmlContent: finalHtml,
updateExisting: updateExisting,
existingCampaignId: campaign.brevoCampaignId
})
});
const resData = await response.json();
if (resData.success && resData.campaignId) {
setCampaign((prev) => ({
...prev,
brevoCampaignId: resData.campaignId,
status: 'exported'
}));
setBrevoUrl(resData.brevoUrl);
if (campaign.id) {
await supabase
.from('templates')
.update({
status: 'exported',
brevo_campaign_id: resData.campaignId
})
.eq('id', campaign.id);
}
alert(
resData.message ||
(updateExisting
? 'Kampaň bola v Brevo úspešne aktualizovaná!'
: 'Kampaň bola úspešne odoslaná do Brevo!')
);
} else {
alert(`Chyba pri exporte do Brevo: ${resData.error || 'Neznáma chyba'}`);
}
} catch (err: any) {
alert(`Chyba: ${err.message}`);
} finally {
setIsExportingBrevo(false);
}
};
const handleClearBrevoId = async () => {
setCampaign((prev) => ({ ...prev, brevoCampaignId: undefined }));
setBrevoUrl(null);
if (campaign.id) {
await supabase
.from('templates')
.update({ brevo_campaign_id: null })
.eq('id', campaign.id);
}
};
// Vybraný produkt pre editor cenoviek
let targetProduct: any = null;
if (targetProductIdForBadge && selectedModule?.data?.products) {
targetProduct = selectedModule.data.products.find((p: any) => p.id === targetProductIdForBadge) || null;
}
const handleApplyBadgeToProduct = (badge: BadgeTemplate, renderedPngUrl?: string) => {
if (!targetProductIdForBadge || !selectedModule) return;
const updatedProducts = (selectedModule.data.products || []).map((p: any) => {
if (p.id === targetProductIdForBadge) {
return {
...p,
badgeId: badge.id,
badgeCustomTemplate: badge,
badgeWidth: badge.width ?? p.badgeWidth ?? 110,
badgeAlign: badge.align ?? p.badgeAlign ?? 'center',
badgePaddingTop: badge.paddingTop ?? p.badgePaddingTop ?? 4,
badgePaddingBottom: badge.paddingBottom ?? p.badgePaddingBottom ?? 4,
customBadgePngUrl: undefined // Používame živý dynamický SVG vektor, žiadna statická fotka
};
}
return p;
});
handleUpdateModule({
...selectedModule,
data: {
...selectedModule.data,
products: updatedProducts
}
});
setAvailableBadges((prev) => {
if (prev.some((b) => b.id === badge.id)) {
return prev.map((b) => (b.id === badge.id ? badge : b));
}
return [badge, ...prev];
});
};
// Hromadná aplikácia cenovky na všetky produkty v celej kampani (všetky sekcie + vernostné zľavy)
const handleApplyBadgeToAll = (badge: BadgeTemplate, badgeLayout?: 'below' | 'side') => {
setCampaign((prev) => ({
...prev,
modules: prev.modules.map((m) => {
if (m.type === 'products_grid' || m.type === 'loyalty_club') {
return {
...m,
data: {
...m.data,
// Produkty s vlastnou PNG cenovkou (výnimky) sa neprepisujú
products: (m.data.products || []).map((prod: any) => prod.customBadgePngUrl ? prod : ({
...prod,
badgeId: badge.id,
badgeCustomTemplate: badge,
badgeWidth: badge.width ?? prod.badgeWidth ?? 110,
badgeAlign: badge.align ?? prod.badgeAlign ?? 'center',
badgePaddingTop: badge.paddingTop ?? prod.badgePaddingTop ?? 4,
badgePaddingBottom: badge.paddingBottom ?? prod.badgePaddingBottom ?? 4,
badgeLayout: badgeLayout ?? prod.badgeLayout
}))
}
};
}
return m;
})
}));
};
// 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 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, ...patch } }
: m
)
}));
};
const topCount = campaign.modules.filter((m) => isTopModule(m)).length;
const firstBottomIdx = campaign.modules.findIndex((m) => isBottomModule(m));
return (
<div className="flex flex-col h-screen overflow-hidden bg-slate-950 font-sans">
{/* 1. Horná lišta */}
<TopBar
campaign={campaign}
isSaving={isSaving}
onSave={handleManualSave}
onUpdateTitle={(title) => setCampaign((prev) => ({ ...prev, title }))}
onOpenSavedCampaigns={() => setIsSavedCampaignsOpen(true)}
onDuplicateCampaign={handleDuplicateCurrentCampaign}
onExportZip={handleExportZip}
onExportBrevo={handleExportBrevo}
isExportingBrevo={isExportingBrevo}
brevoCampaignUrl={brevoUrl}
brevoListingUrl="https://app.brevo.com/campaigns/listing"
onClearBrevoId={handleClearBrevoId}
onAddModule={handleAddModule}
/>
{/* 2. Hlavný pracovný priestor */}
<div className="flex flex-1 overflow-hidden">
{/* Stredné plátno - Presný originálny gradient a čisté rozloženie */}
<main
className="flex-1 overflow-y-auto p-4 md:p-8 flex items-start justify-center"
style={{
backgroundColor: '#001a33',
backgroundImage: 'linear-gradient(135deg, #005bb5 0%, #000d1a 100%)'
}}
>
{/* Zvýraznenie produktu otvoreného v paneli */}
{selectedProductId && (
<style>{`main [data-product-id="${selectedProductId.replace(/["\\]/g, '')}"] { outline: 3px solid #ef4444; outline-offset: 3px; }`}</style>
)}
{/* E-mail Container 1:1 k originálu */}
<div className="w-full max-w-[600px] my-4">
{campaign.modules.map((mod, idx) => {
const isSelected = mod.id === selectedModuleId;
const isAnchored = isAnchoredModule(mod);
const isTop = isTopModule(mod);
const isFlyers = isFlyersModule(mod);
const isRecipes = isRecipesModule(mod);
const isFooter = isFooterModule(mod);
const canMoveUp = !isAnchored && idx > topCount;
const canMoveDown = !isAnchored && idx < (firstBottomIdx !== -1 ? firstBottomIdx - 1 : campaign.modules.length - 1);
return (
<div
key={mod.id}
onClick={(e) => {
// Klik na kartu produktu otvorí v paneli práve tento produkt
const card = (e.target as HTMLElement).closest('[data-product-id]');
if (card) {
setSelectedProductId(card.getAttribute('data-product-id'));
} else if (mod.id !== selectedModuleId) {
setSelectedProductId(null);
}
setSelectedModuleId(mod.id);
}}
className={`relative cursor-pointer transition-all duration-150 rounded-2xl ${
isSelected
? 'ring-2 ring-offset-4 ring-offset-[#001a33] ring-red-500 shadow-2xl'
: 'hover:ring-1 hover:ring-offset-2 hover:ring-offset-[#001a33] hover:ring-white/30'
}`}
>
{/* Lišta s nástrojmi pre označený modul */}
{isSelected && (
<div className="absolute -top-3 right-4 z-40 flex items-center gap-1.5 bg-[#001a33] text-white px-3 py-1.5 rounded-full text-xs shadow-2xl border border-white/20 backdrop-blur-md">
<span className="text-[10px] font-bold text-gray-300 uppercase tracking-wider">
{mod.title}
</span>
{/* Ak je modul ukotvený (Hlavička / Letáky / Recepty / Pätička / Oznamovacia lišta) */}
{isAnchored ? (
<div className="flex items-center gap-1.5">
{mod.type === 'announcement' && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleUpdateModule({
...mod,
data: { ...mod.data, enabled: mod.data.enabled === false ? true : false }
});
}}
className={`text-[9px] font-bold px-2 py-0.5 rounded-full border transition-all flex items-center gap-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 ? 'Aktivovať lištu v e-maile' : 'Skryť lištu v e-maile'}
>
{mod.data.enabled === false ? 'Skrytá (zapnúť)' : 'Zobrazená'}
</button>
)}
<span className="text-[9px] font-bold bg-white/10 text-amber-300 px-2 py-0.5 rounded-full border border-white/10 uppercase flex items-center gap-1">
<Lock className="w-2.5 h-2.5" />
{isFooter
? 'Ukotvená pätička'
: isFlyers
? 'Ukotvené letáky'
: isRecipes
? 'Ukotvené recepty'
: mod.type === 'announcement'
? 'Oznamovacia lišta'
: 'Ukotvená hlavička'}
</span>
</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();
moveModule(idx, 'up');
}}
disabled={!canMoveUp}
className="hover:text-red-400 disabled:opacity-30 p-1"
title={canMoveUp ? 'Posunúť vyššie' : 'Nemožno posunúť nad hlavičku'}
>
<ChevronUp className="w-3.5 h-3.5" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
moveModule(idx, 'down');
}}
disabled={!canMoveDown}
className="hover:text-red-400 disabled:opacity-30 p-1"
title={canMoveDown ? 'Posunúť nižšie' : 'Nemožno posunúť pod pätičku'}
>
<ChevronDown 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>
)}
{/* Vykreslenie modulu: ak je loyalty_club alebo announcement vypnutý, zobrazíme prívetivý placeholder */}
{mod.type === 'loyalty_club' && mod.data.enabled === false ? (
<div className="p-8 my-4 border-2 border-dashed border-red-500/40 bg-red-950/30 rounded-2xl text-center">
<p className="text-xs font-bold text-red-300 uppercase tracking-wider">
Sekcia Vernostné zľavy (Vypnutá)
</p>
<p className="text-[11px] text-gray-300 mt-1">
Táto sekcia je v odosielanom 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-3 px-4 py-1.5 bg-red-600 hover:bg-red-700 text-white text-xs font-bold rounded-lg transition-colors shadow-sm"
>
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">
Top oznamovacia lišta (Vypnutá / 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ť oznamovaciu lištu
</button>
</div>
) : (
<div
dangerouslySetInnerHTML={{
__html: renderSingleModuleHtml(mod, campaign)
}}
onClick={(e) => {
// odkazy v náhľade neotvárame, klik slúži na výber modulu / produktu
if ((e.target as HTMLElement).closest('a')) e.preventDefault();
}}
className="[&_[data-product-id]]:cursor-pointer"
/>
)}
</div>
);
})}
</div>
</main>
{/* Pravý konfiguračný panel */}
<aside className="w-96 flex-shrink-0 h-full bg-white shadow-2xl z-20">
<ConfigPanel
module={selectedModule}
selectedProductId={selectedProductId}
onSelectProduct={setSelectedProductId}
onUpdateModule={handleUpdateModule}
onApplyBadgeToAll={handleApplyBadgeToAll}
onUpdateAllProductSections={handleUpdateAllProductSections}
campaignValidity={getCampaignValidity(campaign)}
availableBadges={availableBadges}
onOpenBadgeManager={(pId) => {
setTargetProductIdForBadge(pId);
setIsBadgeEditorOpen(true);
}}
onOpenImport={() => setIsImportOpen(true)}
/>
</aside>
</div>
{/* Modál Hromadného importu */}
<ProductImportModal
isOpen={isImportOpen}
onClose={() => setIsImportOpen(false)}
onImportProducts={handleImportProducts}
/>
{/* Modál Editora cenoviek (SVG -> PNG) */}
<BadgeEditorModal
isOpen={isBadgeEditorOpen}
onClose={() => setIsBadgeEditorOpen(false)}
onSelectBadge={handleApplyBadgeToProduct}
currentDiscount={targetProduct?.discountText || targetProduct?.cardDiscount || '25%'}
currentPriceMain={targetProduct?.priceMain || targetProduct?.cardPriceMain || '1'}
currentPriceSub={targetProduct?.priceSub || targetProduct?.cardPriceSub || '99'}
currentUnitPrice={targetProduct?.unitPrice || targetProduct?.cardUnitPrice || '1kg = 4,91 €'}
currentCardPriceMain={targetProduct?.cardPriceMain || undefined}
currentCardPriceSub={targetProduct?.cardPriceSub || undefined}
currentCardDiscount={targetProduct?.cardDiscount || undefined}
/>
{/* Modál Moje newslettre (Ukladanie, duplikácia, vytváranie a prepínanie) */}
<SavedCampaignsModal
isOpen={isSavedCampaignsOpen}
onClose={() => setIsSavedCampaignsOpen(false)}
currentCampaign={campaign}
onLoadCampaign={(selected: NewsletterCampaign) => {
setCampaign({ ...selected, modules: enforceAnchors(ensureValidityBar(selected.modules)) });
setSelectedModuleId(selected.modules[1]?.id || selected.modules[0]?.id || null);
if (selected.brevoCampaignId) {
setBrevoUrl(getBrevoCampaignUrl(selected.brevoCampaignId));
} else {
setBrevoUrl(null);
}
}}
onSaveCurrentAsCopy={async (newTitle: string) => {
const { data, error } = await supabase
.from('templates')
.insert([
{
title: newTitle,
status: 'draft',
data: {
preheader: campaign.preheader,
modules: campaign.modules
}
}
])
.select();
if (error) throw error;
if (data && data[0]) {
setCampaign({
id: data[0].id,
title: newTitle,
preheader: campaign.preheader,
status: 'draft',
modules: campaign.modules
});
}
}}
/>
</div>
);
}