feat: anchor header/footer, editable header buttons, campaign management, image uploads, badge font customization, and brevo export fix

This commit is contained in:
Jakub_Hostacny committed 2026-09-24 11:42:45 +02:00
1 parent 3dc5778155
commit 2b4d3643cb
10 files changed
+1819 -178

No files matched your search

+230 -2
View File
@@ -25,6 +25,46 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
currentPriceSub = '99',
currentUnitPrice = '1kg = 4,91 €'
}) => {
const POPULAR_FONTS = [
{ name: 'Poppins', source: 'google' as const },
{ name: 'Montserrat', source: 'google' as const },
{ name: 'Oswald', source: 'google' as const },
{ name: 'Bebas Neue', source: 'google' as const },
{ name: 'Anton', source: 'google' as const },
{ name: 'Roboto Condensed', source: 'google' as const },
{ name: 'Barlow Semi Condensed', source: 'google' as const },
{ name: 'Inter', source: 'google' as const },
{ name: 'Open Sans', source: 'google' as const },
{ name: 'Arial Narrow', source: 'system' as const },
{ name: 'Impact', source: 'system' as const }
];
const loadFontDynamically = (fontFamily: string, source: 'system' | 'google' | 'adobe' = 'google', url?: string) => {
if (typeof document === 'undefined') return;
if (source === 'adobe' && url) {
const existing = document.querySelector(`link[data-adobe-font="${url}"]`);
if (!existing) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
link.setAttribute('data-adobe-font', url);
document.head.appendChild(link);
}
} else if (source === 'google' && fontFamily) {
const clean = fontFamily.trim().replace(/\s+/g, '+');
const href = `https://fonts.googleapis.com/css2?family=${clean}:wght@400;600;700;800;900&display=swap`;
const existing = document.querySelector(`link[data-google-font="${clean}"]`);
if (!existing) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.setAttribute('data-google-font', clean);
document.head.appendChild(link);
}
}
};
const [badges, setBadges] = useState<BadgeTemplate[]>([]);
const [activeBadge, setActiveBadge] = useState<BadgeTemplate | null>(null);
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
@@ -32,6 +72,17 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
const [isExportingPng, setIsExportingPng] = useState(false);
const [isSavingDb, setIsSavingDb] = useState(false);
// Načítanie písiem pre všetky vrstvy aktívnej cenovky
useEffect(() => {
if (activeBadge?.layersConfig) {
activeBadge.layersConfig.forEach((l) => {
if (l.fontFamily) {
loadFontDynamically(l.fontFamily, l.fontSource || 'google', l.fontUrl);
}
});
}
}, [activeBadge]);
// Načítanie existujúcich šablón zo Supabase
useEffect(() => {
if (isOpen) {
@@ -366,9 +417,11 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
top: `${layer.y}px`,
transform: 'translate(-50%, -50%)',
fontSize: `${layer.fontSize}px`,
fontWeight: layer.fontWeight as any,
fontWeight: (layer.fontWeight || '700') as any,
color: layer.fill,
fontFamily: 'Poppins, Arial, sans-serif',
fontFamily: layer.fontFamily
? `'${layer.fontFamily}', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`
: 'Poppins, Arial, sans-serif',
whiteSpace: 'nowrap',
cursor: 'pointer'
}}
@@ -530,6 +583,181 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
</div>
</div>
{/* Font nastavenie */}
<div className="space-y-2 pt-2 border-t border-gray-200">
<label className="block text-[11px] font-semibold text-gray-700">
Písmo (Font) a Hrúbka
</label>
{/* Zdroj písma */}
<div className="grid grid-cols-3 gap-1 bg-gray-200 p-0.5 rounded-lg text-[10px] font-bold text-center">
<button
type="button"
onClick={() => {
updateActiveLayer({ fontSource: 'google' });
}}
className={`py-1 rounded ${
(activeLayer.fontSource || 'google') === 'google'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Google
</button>
<button
type="button"
onClick={() => {
updateActiveLayer({ fontSource: 'adobe' });
}}
className={`py-1 rounded ${
activeLayer.fontSource === 'adobe'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Adobe
</button>
<button
type="button"
onClick={() => {
updateActiveLayer({ fontSource: 'system' });
}}
className={`py-1 rounded ${
activeLayer.fontSource === 'system'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Systém
</button>
</div>
{/* Google Fonts voľba */}
{(activeLayer.fontSource || 'google') === 'google' && (
<div className="space-y-2">
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">Výber Google Fontu</label>
<select
value={
POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins'))
? activeLayer.fontFamily || 'Poppins'
: 'custom'
}
onChange={(e) => {
if (e.target.value !== 'custom') {
loadFontDynamically(e.target.value, 'google');
updateActiveLayer({ fontFamily: e.target.value, fontSource: 'google' });
}
}}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
>
{POPULAR_FONTS.filter((f) => f.source === 'google').map((f) => (
<option key={f.name} value={f.name}>
{f.name}
</option>
))}
<option value="custom">Iný Google Font (zadať vlastný)...</option>
</select>
</div>
{(!POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins')) ||
activeLayer.fontFamily === 'custom') && (
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">Názov Google Fontu</label>
<input
type="text"
placeholder="napr. Montserrat, Oswald, Syne"
value={activeLayer.fontFamily === 'custom' ? '' : activeLayer.fontFamily || ''}
onChange={(e) => {
const val = e.target.value;
loadFontDynamically(val, 'google');
updateActiveLayer({ fontFamily: val, fontSource: 'google' });
}}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
/>
</div>
)}
</div>
)}
{/* Adobe Fonts voľba */}
{activeLayer.fontSource === 'adobe' && (
<div className="space-y-2">
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">
Adobe Typekit CSS URL
</label>
<input
type="text"
placeholder="https://use.typekit.net/xxxx.css"
value={activeLayer.fontUrl || ''}
onChange={(e) => {
const url = e.target.value;
if (activeLayer.fontFamily) {
loadFontDynamically(activeLayer.fontFamily, 'adobe', url);
}
updateActiveLayer({ fontUrl: url, fontSource: 'adobe' });
}}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-mono text-[10px]"
/>
</div>
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">
Názov rodiny písma (Font Family)
</label>
<input
type="text"
placeholder="napr. futura-pt, omnes-pro"
value={activeLayer.fontFamily || ''}
onChange={(e) => {
const family = e.target.value;
if (activeLayer.fontUrl) {
loadFontDynamically(family, 'adobe', activeLayer.fontUrl);
}
updateActiveLayer({ fontFamily: family, fontSource: 'adobe' });
}}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
/>
</div>
</div>
)}
{/* Systémové písma */}
{activeLayer.fontSource === 'system' && (
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">Systémový font</label>
<select
value={activeLayer.fontFamily || 'Arial'}
onChange={(e) => updateActiveLayer({ fontFamily: e.target.value, fontSource: 'system' })}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
>
<option value="Arial">Arial</option>
<option value="Arial Narrow">Arial Narrow</option>
<option value="Impact">Impact</option>
<option value="Trebuchet MS">Trebuchet MS</option>
<option value="Helvetica">Helvetica</option>
<option value="Verdana">Verdana</option>
</select>
</div>
)}
{/* Hrúbka fontu */}
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">Hrúbka písma (Weight)</label>
<select
value={activeLayer.fontWeight || '700'}
onChange={(e) => updateActiveLayer({ fontWeight: e.target.value })}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
>
<option value="400">400 - Normal</option>
<option value="600">600 - SemiBold</option>
<option value="700">700 - Bold</option>
<option value="800">800 - ExtraBold</option>
<option value="900">900 - Black</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">Pozícia X ({activeLayer.x}px)</label>
+615 -31
View File
@@ -4,6 +4,7 @@ import React from 'react';
import { NewsletterModule, ProductItem } from '@/types/newsletter';
import { Plus, Trash2, Image as ImageIcon, Tag, Store } from 'lucide-react';
import { SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign';
import { ImageUploadInput } from './ImageUploadInput';
interface ConfigPanelProps {
module: NewsletterModule | null;
@@ -85,41 +86,136 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
)}
{/* HEADER */}
{module.type === 'header' && (
<div className="space-y-4">
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">URL Loga</label>
<input
type="text"
{module.type === 'header' && (() => {
const navLinks = module.data.navLinks || [
{ label: 'Letáky', url: 'https://www.coop.sk/sk/letaky', variant: 'outline' },
{ label: 'COOP Klub', url: 'https://www.coopklub.sk', variant: 'filled' }
];
const updateNavLink = (index: number, patch: Partial<typeof navLinks[0]>) => {
const updated = [...navLinks];
updated[index] = { ...updated[index], ...patch };
updateData({ navLinks: updated });
};
return (
<div className="space-y-5">
<ImageUploadInput
label="Logo COOP Jednota"
value={module.data.logoUrl || ''}
onChange={(e) => updateData({ logoUrl: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2 font-mono text-xs"
onChange={(url) => updateData({ logoUrl: url })}
/>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Odkaz loga</label>
<input
type="text"
value={module.data.logoLink || ''}
onChange={(e) => updateData({ logoLink: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2"
placeholder="https://www.coop.sk"
/>
</div>
{/* Tlačidlá v hlavičke */}
<div className="pt-2 border-t border-gray-200 space-y-4">
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block">
Tlačidlá v hlavičke
</span>
{/* Tlačidlo 1 (Letáky) */}
<div className="bg-gray-50 p-3 rounded-xl border border-gray-200 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-gray-700">Tlačidlo 1 (Letáky)</span>
<span className="text-[10px] bg-red-100 text-red-700 font-bold px-2 py-0.5 rounded">
{navLinks[0]?.variant === 'filled' ? 'Plné' : 'Obrys'}
</span>
</div>
<div>
<label className="block text-[11px] text-gray-500 mb-1">Text tlačidla</label>
<input
type="text"
value={navLinks[0]?.label || ''}
onChange={(e) => updateNavLink(0, { label: e.target.value })}
className="w-full text-xs font-bold border border-gray-300 rounded-lg p-2 bg-white"
placeholder="Letáky"
/>
</div>
<div>
<label className="block text-[11px] text-gray-500 mb-1">Cieľový odkaz (URL)</label>
<input
type="text"
value={navLinks[0]?.url || ''}
onChange={(e) => updateNavLink(0, { url: e.target.value })}
className="w-full text-xs border border-gray-300 rounded-lg p-2 bg-white"
placeholder="https://www.coop.sk/sk/letaky"
/>
</div>
<div>
<label className="block text-[11px] text-gray-500 mb-1">Štýl zobrazenia</label>
<select
value={navLinks[0]?.variant || 'outline'}
onChange={(e) => updateNavLink(0, { variant: e.target.value as any })}
className="w-full text-xs border border-gray-300 rounded-lg p-2 bg-white font-medium"
>
<option value="outline">Červený obrys (Biele pozadie, červený text)</option>
<option value="filled">Modré plné tlačidlo (Biele písmo)</option>
</select>
</div>
</div>
{/* Tlačidlo 2 (COOP Klub) */}
<div className="bg-gray-50 p-3 rounded-xl border border-gray-200 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-gray-700">Tlačidlo 2 (COOP Klub)</span>
<span className="text-[10px] bg-blue-100 text-blue-700 font-bold px-2 py-0.5 rounded">
{navLinks[1]?.variant === 'filled' ? 'Plné modré' : 'Obrys'}
</span>
</div>
<div>
<label className="block text-[11px] text-gray-500 mb-1">Text tlačidla</label>
<input
type="text"
value={navLinks[1]?.label || ''}
onChange={(e) => updateNavLink(1, { label: e.target.value })}
className="w-full text-xs font-bold border border-gray-300 rounded-lg p-2 bg-white"
placeholder="COOP Klub"
/>
</div>
<div>
<label className="block text-[11px] text-gray-500 mb-1">Cieľový odkaz (URL)</label>
<input
type="text"
value={navLinks[1]?.url || ''}
onChange={(e) => updateNavLink(1, { url: e.target.value })}
className="w-full text-xs border border-gray-300 rounded-lg p-2 bg-white"
placeholder="https://www.coopklub.sk"
/>
</div>
<div>
<label className="block text-[11px] text-gray-500 mb-1">Štýl zobrazenia</label>
<select
value={navLinks[1]?.variant || 'filled'}
onChange={(e) => updateNavLink(1, { variant: e.target.value as any })}
className="w-full text-xs border border-gray-300 rounded-lg p-2 bg-white font-medium"
>
<option value="filled">Modré plné tlačidlo (Biele písmo)</option>
<option value="outline">Červený obrys (Biele pozadie, červený text)</option>
</select>
</div>
</div>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Odkaz loga</label>
<input
type="text"
value={module.data.logoLink || ''}
onChange={(e) => updateData({ logoLink: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2"
/>
</div>
</div>
)}
);
})()}
{/* BANNER */}
{module.type === 'banner' && (
<div className="space-y-4">
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">URL Obrázka bannera</label>
<input
type="text"
value={module.data.imageUrl || ''}
onChange={(e) => updateData({ imageUrl: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2 font-mono text-xs"
/>
</div>
<ImageUploadInput
label="Obrázok bannera"
value={module.data.imageUrl || ''}
onChange={(url) => updateData({ imageUrl: url })}
/>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Odkaz bannera</label>
<input
@@ -269,6 +365,32 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
/>
</div>
<ImageUploadInput
label="Obrázok produktu"
value={p.imageUrl || ''}
onChange={(url) => {
const products = [...module.data.products];
products[pIdx].imageUrl = url;
updateData({ products });
}}
placeholder="https://...produkt.png alebo nahrajte súbor"
/>
<div>
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Platnosť akcie pre tento produkt (override)</label>
<input
type="text"
value={p.validityOverride || ''}
onChange={(e) => {
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"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-[10px] text-gray-500">Cena (€)</label>
@@ -407,6 +529,450 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
</div>
)}
{/* LOYALTY CLUB (VERNOSTNÉ ZĽAVY) */}
{module.type === 'loyalty_club' && (
<div className="space-y-5">
{/* Toggle zapnuté / vypnuté */}
<div className="bg-red-50/60 p-3.5 rounded-xl border border-red-200 flex items-center justify-between">
<div>
<span className="text-xs font-bold text-gray-900 block">Zobraziť v e-maile</span>
<span className="text-[11px] text-gray-500">Sekcia Vernostné zľavy</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={module.data.enabled !== false}
onChange={(e) => updateData({ enabled: e.target.checked })}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-300 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-red-600"></div>
</label>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Názov sekcie</label>
<input
type="text"
value={module.data.sectionTitle || ''}
onChange={(e) => updateData({ sectionTitle: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2 font-bold"
/>
</div>
<ImageUploadInput
label="Obrázok banneru Vernostných zliav"
value={module.data.headerImageUrl || ''}
onChange={(url) => updateData({ headerImageUrl: url })}
/>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Odkaz banneru</label>
<input
type="text"
value={module.data.headerLink || ''}
onChange={(e) => updateData({ headerLink: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2"
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Globálna platnosť akcie</label>
<input
type="text"
value={module.data.globalValidity || ''}
onChange={(e) => updateData({ globalValidity: e.target.value })}
placeholder="napr. 2. 5. – 15. 5. 2026"
className="w-full text-sm border border-gray-300 rounded-lg p-2"
/>
</div>
{/* 2 Vernostné produkty */}
<div className="space-y-4 pt-2">
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block">
Vernostné produkty ({module.data.products?.length || 0})
</span>
{(module.data.products || []).map((p: any, pIdx: number) => (
<div key={p.id || pIdx} className="p-3 bg-gray-50 rounded-xl border border-gray-200 space-y-3">
<span className="text-xs font-bold text-gray-900 block truncate">
{p.title || `Vernostný produkt ${pIdx + 1}`}
</span>
<div>
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Názov produktu</label>
<input
type="text"
value={p.title || ''}
onChange={(e) => {
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"
/>
</div>
<ImageUploadInput
label="Obrázok produktu"
value={p.imageUrl || ''}
onChange={(url) => {
const products = [...module.data.products];
products[pIdx].imageUrl = url;
updateData({ products });
}}
placeholder="https://...produkt.png alebo nahrajte súbor"
/>
<div>
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Platnosť akcie pre tento produkt (override)</label>
<input
type="text"
value={p.validityOverride || ''}
onChange={(e) => {
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"
/>
</div>
{/* Sekcia Bez karty */}
<div className="p-2.5 bg-red-50/40 rounded-lg border border-red-100 space-y-2">
<span className="text-[11px] font-bold text-red-700 block">Bez karty</span>
<div className="grid grid-cols-3 gap-1.5">
<div>
<label className="text-[9px] text-gray-500">Zľava</label>
<input
type="text"
value={p.noCardDiscount || ''}
onChange={(e) => {
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"
/>
</div>
<div>
<label className="text-[9px] text-gray-500">Cena (€)</label>
<input
type="text"
value={p.noCardPriceMain || ''}
onChange={(e) => {
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"
/>
</div>
<div>
<label className="text-[9px] text-gray-500">Centy</label>
<input
type="text"
value={p.noCardPriceSub || ''}
onChange={(e) => {
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"
/>
</div>
</div>
<div>
<label className="text-[9px] text-gray-500">Merná cena (bez karty)</label>
<input
type="text"
value={p.noCardUnitPrice || ''}
onChange={(e) => {
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"
/>
</div>
</div>
{/* Sekcia S kartou */}
<div className="p-2.5 bg-amber-50/50 rounded-lg border border-amber-200 space-y-2">
<span className="text-[11px] font-bold text-amber-900 block">S kartou (Klub)</span>
<div className="grid grid-cols-3 gap-1.5">
<div>
<label className="text-[9px] text-gray-500">Zľava</label>
<input
type="text"
value={p.cardDiscount || ''}
onChange={(e) => {
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"
/>
</div>
<div>
<label className="text-[9px] text-gray-500">Cena (€)</label>
<input
type="text"
value={p.cardPriceMain || ''}
onChange={(e) => {
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"
/>
</div>
<div>
<label className="text-[9px] text-gray-500">Centy</label>
<input
type="text"
value={p.cardPriceSub || ''}
onChange={(e) => {
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"
/>
</div>
</div>
<div>
<label className="text-[9px] text-gray-500">Merná cena (s kartou)</label>
<input
type="text"
value={p.cardUnitPrice || ''}
onChange={(e) => {
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"
/>
</div>
</div>
{/* Predajne */}
<div>
<label className="text-[10px] text-gray-500 block mb-1">Dostupnosť (Piktogramy)</label>
<div className="flex gap-2 text-[10px]">
<label className="flex items-center gap-1 cursor-pointer">
<input
type="checkbox"
checked={p.stores?.potraviny}
onChange={(e) => {
const products = [...module.data.products];
products[pIdx].stores.potraviny = e.target.checked;
updateData({ products });
}}
/>
<span>Potraviny</span>
</label>
<label className="flex items-center gap-1 cursor-pointer">
<input
type="checkbox"
checked={p.stores?.supermarket}
onChange={(e) => {
const products = [...module.data.products];
products[pIdx].stores.supermarket = e.target.checked;
updateData({ products });
}}
/>
<span>Supermarket</span>
</label>
<label className="flex items-center gap-1 cursor-pointer">
<input
type="checkbox"
checked={p.stores?.tempo}
onChange={(e) => {
const products = [...module.data.products];
products[pIdx].stores.tempo = e.target.checked;
updateData({ products });
}}
/>
<span>Tempo</span>
</label>
</div>
</div>
{/* Vlajočka SR */}
<div className="flex items-center gap-2 pt-1">
<input
type="checkbox"
id={`loyalty-flag-${p.id || pIdx}`}
checked={p.showSlovakFlag || false}
onChange={(e) => {
const products = [...module.data.products];
products[pIdx].showSlovakFlag = e.target.checked;
updateData({ products });
}}
className="rounded text-red-600"
/>
<label htmlFor={`loyalty-flag-${p.id || pIdx}`} className="text-xs text-gray-700 cursor-pointer">
Zobraziť slovenskú vlajku / srdce
</label>
</div>
</div>
))}
</div>
</div>
)}
{/* ARTICLES ROW (LETÁKY ALEBO ČLÁNKY) */}
{module.type === 'articles_row' && (
<div className="space-y-5">
{module.data.item1?.btnText ? (
/* AK SÚ TO LETÁKY */
<div className="space-y-4">
<div className="p-3 bg-blue-50/50 rounded-xl border border-blue-100">
<span className="text-xs font-bold text-blue-900 block mb-1">
Leták 1: {module.data.item1?.title || 'Aktuálne letáky'}
</span>
<p className="text-[11px] text-gray-500 mb-3">Farby, texty a rozloženie sú fixne podľa šablóny. Môžete upraviť odkaz a náhľadový obrázok.</p>
<div className="space-y-3">
<div>
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Cieľová URL (kam to smeruje)</label>
<input
type="text"
value={module.data.item1?.linkUrl || ''}
onChange={(e) => updateData({
item1: { ...module.data.item1, linkUrl: e.target.value }
})}
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white"
/>
</div>
<ImageUploadInput
label="Náhľadový obrázok letáku (PNG/JPG)"
value={module.data.item1?.imageUrl || ''}
onChange={(url) => updateData({
item1: { ...module.data.item1, imageUrl: url }
})}
/>
</div>
</div>
<div className="p-3 bg-amber-50/50 rounded-xl border border-amber-100">
<span className="text-xs font-bold text-amber-900 block mb-1">
Leták 2: {module.data.item2?.title || 'Rozšírený leták'}
</span>
<p className="text-[11px] text-gray-500 mb-3">Farby, texty a rozloženie sú fixne podľa šablóny. Môžete upraviť odkaz a náhľadový obrázok.</p>
<div className="space-y-3">
<div>
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Cieľová URL (kam to smeruje)</label>
<input
type="text"
value={module.data.item2?.linkUrl || ''}
onChange={(e) => updateData({
item2: { ...module.data.item2, linkUrl: e.target.value }
})}
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white"
/>
</div>
<ImageUploadInput
label="Náhľadový obrázok letáku (PNG/JPG)"
value={module.data.item2?.imageUrl || ''}
onChange={(url) => updateData({
item2: { ...module.data.item2, imageUrl: url }
})}
/>
</div>
</div>
</div>
) : (
/* AK SÚ TO ČLÁNKY (VEĽKONOČNÝ BARANČEK / VAJÍČKO AMULET) */
<div className="space-y-4">
<div className="p-3 bg-orange-50/50 rounded-xl border border-orange-100">
<span className="text-xs font-bold text-orange-900 block mb-1">
Článok 1 (Recepty)
</span>
<p className="text-[11px] text-gray-500 mb-3">Logá a gradienty sú na pevno. Môžete upraviť text, odkaz a obrázok v pozadí.</p>
<div className="space-y-3">
<div>
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Názov / Text článku</label>
<input
type="text"
value={module.data.item1?.title || ''}
onChange={(e) => updateData({
item1: { ...module.data.item1, title: e.target.value }
})}
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white font-bold"
/>
</div>
<div>
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Cieľová URL (odkaz)</label>
<input
type="text"
value={module.data.item1?.linkUrl || ''}
onChange={(e) => updateData({
item1: { ...module.data.item1, linkUrl: e.target.value }
})}
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white"
/>
</div>
<ImageUploadInput
label="Obrázok v pozadí"
value={module.data.item1?.imageUrl || ''}
onChange={(url) => updateData({
item1: { ...module.data.item1, imageUrl: url }
})}
/>
</div>
</div>
<div className="p-3 bg-blue-50/50 rounded-xl border border-blue-100">
<span className="text-xs font-bold text-blue-900 block mb-1">
Článok 2 (Časopis Jednota)
</span>
<p className="text-[11px] text-gray-500 mb-3">Logá a gradienty sú na pevno. Môžete upraviť text, odkaz a obrázok v pozadí.</p>
<div className="space-y-3">
<div>
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Názov / Text článku</label>
<input
type="text"
value={module.data.item2?.title || ''}
onChange={(e) => updateData({
item2: { ...module.data.item2, title: e.target.value }
})}
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white font-bold"
/>
</div>
<div>
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Cieľová URL (odkaz)</label>
<input
type="text"
value={module.data.item2?.linkUrl || ''}
onChange={(e) => updateData({
item2: { ...module.data.item2, linkUrl: e.target.value }
})}
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white"
/>
</div>
<ImageUploadInput
label="Obrázok v pozadí"
value={module.data.item2?.imageUrl || ''}
onChange={(url) => updateData({
item2: { ...module.data.item2, imageUrl: url }
})}
/>
</div>
</div>
</div>
)}
</div>
)}
{/* FOOTER */}
{module.type === 'footer' && (
<div className="space-y-4">
@@ -419,11 +985,29 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Text odhlásenia</label>
<label className="block text-xs font-semibold text-gray-700 mb-1">Odkaz pre odhlásenie (Unsubscribe URL)</label>
<input
type="text"
value={module.data.unsubscribeText || ''}
onChange={(e) => updateData({ unsubscribeText: e.target.value })}
value={module.data.unsubscribeUrl || ''}
onChange={(e) => updateData({ unsubscribeUrl: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2"
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">Google Play URL</label>
<input
type="text"
value={module.data.googlePlayUrl || ''}
onChange={(e) => updateData({ googlePlayUrl: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2"
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1">App Store URL</label>
<input
type="text"
value={module.data.appStoreUrl || ''}
onChange={(e) => updateData({ appStoreUrl: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2"
/>
</div>
+112
View File
@@ -0,0 +1,112 @@
'use client';
import React, { useRef, useState } from 'react';
import { Upload, Loader2, Image as ImageIcon } from 'lucide-react';
interface ImageUploadInputProps {
label?: string;
value: string;
onChange: (url: string) => void;
placeholder?: string;
}
export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
label = 'Obrázok',
value,
onChange,
placeholder = 'https://... alebo nahrajte súbor'
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [isUploading, setIsUploading] = useState(false);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsUploading(true);
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success && data.url) {
onChange(data.url);
} else {
alert(`Chyba pri nahrávaní obrázka: ${data.error || 'Neznáma chyba'}`);
}
} catch (err: any) {
alert(`Chyba: ${err.message}`);
} finally {
setIsUploading(false);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
return (
<div className="space-y-1.5">
{label && <label className="block text-[11px] font-semibold text-gray-700">{label}</label>}
<div className="flex items-center gap-2">
{/* Náhľad miniatúry */}
<div className="w-10 h-10 rounded-lg bg-gray-100 border border-gray-200 flex-shrink-0 flex items-center justify-center overflow-hidden">
{value ? (
<img
src={value}
alt="Náhľad"
className="w-full h-full object-contain"
onError={(e) => {
(e.target as HTMLElement).style.display = 'none';
}}
/>
) : (
<ImageIcon className="w-4 h-4 text-gray-400" />
)}
</div>
{/* Textové pole s URL */}
<input
type="text"
value={value || ''}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="flex-1 text-xs font-mono border border-gray-300 rounded-lg p-2 bg-white"
/>
{/* Tlačidlo nahrávania */}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/*"
className="hidden"
/>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
className="px-2.5 py-2 text-xs font-bold text-gray-700 bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-lg flex items-center gap-1 transition-colors whitespace-nowrap disabled:opacity-50"
title="Nahrať obrázok zo zariadenia"
>
{isUploading ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin text-red-600" />
<span>Nahrávam...</span>
</>
) : (
<>
<Upload className="w-3.5 h-3.5" />
<span>Nahrať</span>
</>
)}
</button>
</div>
</div>
);
};
@@ -0,0 +1,364 @@
'use client';
import React, { useState, useEffect } from 'react';
import { supabase } from '@/lib/supabase';
import { NewsletterCampaign } from '@/types/newsletter';
import { DEFAULT_CAMPAIGN } from '@/lib/defaultCampaign';
import {
FolderOpen,
Plus,
Copy,
Trash2,
X,
ExternalLink,
Clock,
CheckCircle2,
Layers,
Search,
Sparkles
} from 'lucide-react';
interface SavedCampaignsModalProps {
isOpen: boolean;
onClose: () => void;
currentCampaign: NewsletterCampaign;
onLoadCampaign: (campaign: NewsletterCampaign) => void;
onSaveCurrentAsCopy: (newTitle: string) => Promise<void>;
}
export const SavedCampaignsModal: React.FC<SavedCampaignsModalProps> = ({
isOpen,
onClose,
currentCampaign,
onLoadCampaign,
onSaveCurrentAsCopy
}) => {
const [campaigns, setCampaigns] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState('');
const [isCreatingNew, setIsCreatingNew] = useState(false);
const [newTitle, setNewTitle] = useState('');
const fetchCampaigns = async () => {
setLoading(true);
try {
const { data, error } = await supabase
.from('templates')
.select('*')
.order('updated_at', { ascending: false });
if (error) throw error;
setCampaigns(data || []);
} catch (err: any) {
console.error('Error fetching campaigns:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isOpen) {
fetchCampaigns();
setIsCreatingNew(false);
setNewTitle('');
}
}, [isOpen]);
if (!isOpen) return null;
const handleDuplicate = async (camp: any) => {
try {
const clonedTitle = `Kópia - ${camp.title}`;
const { data, error } = await supabase
.from('templates')
.insert([
{
title: clonedTitle,
status: 'draft',
data: camp.data
}
])
.select();
if (error) throw error;
if (data && data[0]) {
await fetchCampaigns();
onLoadCampaign({
id: data[0].id,
title: data[0].title,
preheader: data[0].data?.preheader || '',
status: 'draft',
modules: data[0].data?.modules || []
});
onClose();
}
} catch (err: any) {
alert(`Chyba pri duplikovaní: ${err.message}`);
}
};
const handleDelete = async (id: string, title: string) => {
if (!confirm(`Naozaj chcete vymazať newsletter "${title}"?`)) return;
try {
const { error } = await supabase.from('templates').delete().eq('id', id);
if (error) throw error;
setCampaigns((prev) => prev.filter((c) => c.id !== id));
if (currentCampaign.id === id) {
// Ak sme vymazali práve otvorený, resetneme ID
onLoadCampaign({ ...currentCampaign, id: '' });
}
} catch (err: any) {
alert(`Chyba pri mazaní: ${err.message}`);
}
};
const handleCreateNew = async () => {
const title = newTitle.trim() || `COOP Newsletter - ${new Date().toLocaleDateString('sk-SK')}`;
try {
const { data, error } = await supabase
.from('templates')
.insert([
{
title,
status: 'draft',
data: {
preheader: DEFAULT_CAMPAIGN.preheader,
modules: JSON.parse(JSON.stringify(DEFAULT_CAMPAIGN.modules))
}
}
])
.select();
if (error) throw error;
if (data && data[0]) {
onLoadCampaign({
id: data[0].id,
title: data[0].title,
preheader: data[0].data?.preheader || '',
status: 'draft',
modules: data[0].data?.modules || []
});
onClose();
}
} catch (err: any) {
alert(`Chyba pri vytváraní: ${err.message}`);
}
};
const handleSaveCurrentCopy = async () => {
const title = newTitle.trim() || `Kópia - ${currentCampaign.title}`;
await onSaveCurrentAsCopy(title);
await fetchCampaigns();
setIsCreatingNew(false);
setNewTitle('');
onClose();
};
const filtered = campaigns.filter((c) =>
(c.title || '').toLowerCase().includes(search.toLowerCase())
);
return (
<div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-white rounded-3xl shadow-2xl max-w-4xl w-full max-h-[85vh] flex flex-col overflow-hidden border border-gray-100">
{/* Horná lišta */}
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-slate-50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-2xl bg-red-100 flex items-center justify-center text-red-600">
<FolderOpen className="w-5 h-5" />
</div>
<div>
<h2 className="text-base font-bold text-gray-900">Moje Newslettre</h2>
<p className="text-xs text-gray-500">Spravujte, otvárajte a duplikujte uložené šablóny e-mailov</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setIsCreatingNew((prev) => !prev)}
className="px-3.5 py-2 text-xs font-bold bg-[#e2001a] hover:bg-[#b80015] text-white rounded-xl flex items-center gap-1.5 shadow-sm transition-all"
>
<Plus className="w-4 h-4" />
<span>Nový newsletter</span>
</button>
<button
onClick={onClose}
className="w-9 h-9 rounded-xl hover:bg-gray-200 text-gray-400 hover:text-gray-700 flex items-center justify-center transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Panel na vytvorenie / uloženie kópie */}
{isCreatingNew && (
<div className="p-4 bg-red-50/50 border-b border-red-100 flex flex-col sm:flex-row items-center gap-3">
<div className="flex-1 w-full">
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="Zadajte názov nového newslettera (napr. Kampaň Veľká Noc 2026)"
className="w-full text-xs border border-gray-300 rounded-xl p-2.5 bg-white font-medium focus:ring-2 focus:ring-red-500 focus:outline-none"
autoFocus
/>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto">
<button
onClick={handleCreateNew}
className="flex-1 sm:flex-initial px-3.5 py-2 text-xs font-bold bg-slate-900 hover:bg-black text-white rounded-xl whitespace-nowrap"
>
Vytvoriť čistý z predlohy
</button>
<button
onClick={handleSaveCurrentCopy}
className="flex-1 sm:flex-initial px-3.5 py-2 text-xs font-bold bg-[#003399] hover:bg-blue-900 text-white rounded-xl whitespace-nowrap"
>
Uložiť aktuálny ako kópiu
</button>
</div>
</div>
)}
{/* Vyhľadávanie */}
<div className="p-4 border-b border-gray-100 bg-white">
<div className="relative">
<Search className="w-4 h-4 text-gray-400 absolute left-3 top-3" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Vyhľadávať medzi uloženými newslettrami..."
className="w-full pl-9 pr-4 py-2 text-xs border border-gray-200 rounded-xl bg-gray-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-red-500"
/>
</div>
</div>
{/* Zoznam kampaní */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50/50">
{loading ? (
<div className="py-12 text-center text-sm text-gray-400">Načítavam uložené newslettre...</div>
) : filtered.length === 0 ? (
<div className="py-12 text-center text-gray-400 space-y-2">
<Sparkles className="w-8 h-8 text-gray-300 mx-auto" />
<p className="text-sm font-semibold text-gray-600">Nenašli sa žiadne uložené newslettre</p>
<p className="text-xs text-gray-400">Vytvorte nový kliknutím na tlačidlo vyššie.</p>
</div>
) : (
filtered.map((camp) => {
const isCurrent = camp.id === currentCampaign.id;
const moduleCount = camp.data?.modules?.length || 0;
const isExported = camp.status === 'exported' || !!camp.brevo_campaign_id;
const updatedDate = camp.updated_at
? new Date(camp.updated_at).toLocaleString('sk-SK', {
day: 'numeric',
month: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
: 'Neznámy dátum';
return (
<div
key={camp.id}
className={`p-4 rounded-2xl border transition-all flex flex-col sm:flex-row sm:items-center justify-between gap-3 ${
isCurrent
? 'bg-red-50/50 border-red-300 ring-2 ring-red-500/10'
: 'bg-white border-gray-200 hover:border-gray-300 hover:shadow-sm'
}`}
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-gray-900">{camp.title}</span>
{isCurrent && (
<span className="text-[10px] font-bold bg-red-600 text-white px-2 py-0.5 rounded-full uppercase tracking-wider">
Práve otvorený
</span>
)}
{isExported && (
<span className="text-[10px] font-bold bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded-full flex items-center gap-1">
<CheckCircle2 className="w-3 h-3 text-emerald-600" /> Vytvorené v Brevo
</span>
)}
</div>
<div className="flex items-center gap-4 text-xs text-gray-500">
<span className="flex items-center gap-1">
<Clock className="w-3.5 h-3.5 text-gray-400" /> {updatedDate}
</span>
<span className="flex items-center gap-1">
<Layers className="w-3.5 h-3.5 text-gray-400" /> {moduleCount} modulov
</span>
{camp.brevo_campaign_id && (
<a
href={`https://app.brevo.com/campaigns/email/edit/${camp.brevo_campaign_id}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline flex items-center gap-1 font-semibold"
onClick={(e) => e.stopPropagation()}
>
<span>Brevo #{camp.brevo_campaign_id}</span>
<ExternalLink className="w-3 h-3" />
</a>
)}
</div>
</div>
{/* Akčné tlačidlá */}
<div className="flex items-center gap-2 pt-2 sm:pt-0">
<button
onClick={() => {
onLoadCampaign({
id: camp.id,
title: camp.title,
preheader: camp.data?.preheader || '',
status: camp.status || 'draft',
brevoCampaignId: camp.brevo_campaign_id,
modules: camp.data?.modules || []
});
onClose();
}}
className="px-3 py-1.5 text-xs font-bold bg-slate-900 hover:bg-black text-white rounded-xl transition-colors"
>
{isCurrent ? 'Pokračovať v úprave' : 'Otvoriť'}
</button>
<button
onClick={() => handleDuplicate(camp)}
className="px-3 py-1.5 text-xs font-bold bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-xl flex items-center gap-1 transition-colors"
title="Vytvoriť kópiu"
>
<Copy className="w-3.5 h-3.5" />
<span>Duplikovať</span>
</button>
<button
onClick={() => handleDelete(camp.id, camp.title)}
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors"
title="Vymazať"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
);
})
)}
</div>
{/* Pätička */}
<div className="px-6 py-3.5 border-t border-gray-100 bg-white flex items-center justify-between text-xs text-gray-500">
<span>Celkovo uložených: {campaigns.length} newsletterov</span>
<button
onClick={onClose}
className="px-4 py-1.5 font-bold text-gray-600 hover:bg-gray-100 rounded-xl transition-colors"
>
Zatvoriť
</button>
</div>
</div>
</div>
);
};
+126 -43
View File
@@ -1,12 +1,16 @@
'use client';
import React from 'react';
import React, { useState } from 'react';
import {
Download,
Send,
ExternalLink,
CheckCircle2,
Plus
Plus,
FolderOpen,
Save,
Copy,
Edit2
} from 'lucide-react';
import { NewsletterCampaign } from '@/types/newsletter';
@@ -14,10 +18,14 @@ interface TopBarProps {
campaign: NewsletterCampaign;
isSaving: boolean;
onSave: () => void;
onUpdateTitle: (title: string) => void;
onOpenSavedCampaigns: () => void;
onDuplicateCampaign: () => void;
onExportZip: () => void;
onExportBrevo: () => void;
isExportingBrevo: boolean;
brevoCampaignUrl?: string | null;
brevoListingUrl?: string | null;
onAddModule: (type: any) => void;
}
@@ -25,81 +33,156 @@ export const TopBar: React.FC<TopBarProps> = ({
campaign,
isSaving,
onSave,
onUpdateTitle,
onOpenSavedCampaigns,
onDuplicateCampaign,
onExportZip,
onExportBrevo,
isExportingBrevo,
brevoCampaignUrl,
brevoListingUrl = 'https://app.brevo.com/campaigns/listing',
onAddModule
}) => {
const [isEditingTitle, setIsEditingTitle] = useState(false);
return (
<header className="h-16 bg-white border-b border-gray-200 px-6 flex items-center justify-between sticky top-0 z-30 shadow-xs">
{/* Ľavá časť: Logo a stav */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-[#e2001a] flex items-center justify-center text-white font-black text-sm shadow-sm tracking-tighter">
COOP
</div>
<div>
<h1 className="text-sm font-bold text-gray-900 leading-tight">
{campaign.title || 'COOP Jednota Newsletter'}
</h1>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[11px] text-gray-500">
{isSaving ? (
<span className="text-amber-600 font-medium animate-pulse">Ukladám do Supabase...</span>
) : (
<span className="text-emerald-600 font-medium flex items-center gap-1">
<CheckCircle2 className="w-3.5 h-3.5" /> Uložené v databáze (Auto-save)
</span>
)}
</span>
<header className="h-16 bg-white border-b border-gray-200 px-4 md:px-6 flex items-center justify-between sticky top-0 z-30 shadow-xs">
{/* Ľavá časť: Logo, Moje newslettre, Titulok a stav */}
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-[#e2001a] flex items-center justify-center text-white font-black text-sm shadow-sm tracking-tighter shrink-0">
COOP
</div>
{/* Tlačidlo Moje newslettre */}
<button
onClick={onOpenSavedCampaigns}
className="flex items-center gap-1.5 text-xs font-bold text-gray-700 bg-gray-100 hover:bg-gray-200 px-3 py-1.5 rounded-xl border border-gray-200 transition-colors shadow-xs"
title="Otvoriť, duplikovať alebo vytvoriť nový newsletter"
>
<FolderOpen className="w-3.5 h-3.5 text-blue-600" />
<span className="hidden sm:inline">Moje newslettre</span>
</button>
<div className="h-6 w-px bg-gray-200 mx-1 hidden sm:block" />
{/* Názov kampane */}
<div className="flex flex-col">
{isEditingTitle ? (
<input
type="text"
autoFocus
value={campaign.title || ''}
onChange={(e) => onUpdateTitle(e.target.value)}
onBlur={() => setIsEditingTitle(false)}
onKeyDown={(e) => {
if (e.key === 'Enter') setIsEditingTitle(false);
}}
className="text-xs font-bold text-gray-900 border border-blue-400 rounded px-2 py-0.5 outline-none bg-blue-50/50 w-56"
/>
) : (
<div
onClick={() => setIsEditingTitle(true)}
className="group flex items-center gap-1.5 cursor-pointer"
title="Kliknite pre úpravu názvu"
>
<h1 className="text-xs md:text-sm font-bold text-gray-900 leading-tight max-w-[200px] md:max-w-xs truncate group-hover:text-blue-600">
{campaign.title || 'COOP Jednota Newsletter'}
</h1>
<Edit2 className="w-3 h-3 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
)}
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[10px] text-gray-500">
{isSaving ? (
<span className="text-amber-600 font-medium animate-pulse">Ukladám...</span>
) : (
<span className="text-emerald-600 font-medium flex items-center gap-1">
<CheckCircle2 className="w-3 h-3" /> Uložené
</span>
)}
</span>
</div>
</div>
{/* Pridanie sekcie */}
<div className="hidden md:flex items-center gap-2 ml-6 pl-6 border-l border-gray-200">
<div className="hidden lg:flex items-center gap-2 ml-4 pl-4 border-l border-gray-200">
<button
onClick={() => onAddModule('banner')}
className="text-xs font-bold text-gray-700 bg-gray-100 hover:bg-gray-200 px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition-colors"
className="text-[11px] font-bold text-gray-700 bg-gray-50 hover:bg-gray-100 border border-gray-200 px-2.5 py-1 rounded-lg flex items-center gap-1 transition-colors"
>
<Plus className="w-3.5 h-3.5 text-gray-500" /> Nový Banner
<Plus className="w-3 h-3 text-gray-500" /> Nový Banner
</button>
<button
onClick={() => onAddModule('products_grid')}
className="text-xs font-bold text-gray-700 bg-gray-100 hover:bg-gray-200 px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition-colors"
className="text-[11px] font-bold text-gray-700 bg-gray-50 hover:bg-gray-100 border border-gray-200 px-2.5 py-1 rounded-lg flex items-center gap-1 transition-colors"
>
<Plus className="w-3.5 h-3.5 text-gray-500" /> Produktový rad
<Plus className="w-3 h-3 text-gray-500" /> Produktový rad
</button>
</div>
</div>
{/* Pravá časť: Export do ZIP a Brevo */}
<div className="flex items-center gap-3">
{/* Pravá časť: Uložiť, Duplikovať, Export do ZIP a Brevo */}
<div className="flex items-center gap-2">
{/* Tlačidlo manuálneho uloženia */}
<button
onClick={onSave}
disabled={isSaving}
className="flex items-center gap-1.5 text-xs font-bold text-gray-700 bg-white border border-gray-200 hover:bg-gray-50 disabled:opacity-50 px-3 py-1.5 rounded-xl transition-all shadow-xs"
title="Uložiť zmeny ihneď do databázy"
>
<Save className="w-3.5 h-3.5 text-gray-600" />
<span className="hidden md:inline">Uložiť</span>
</button>
{/* Tlačidlo duplikovania celej kampane */}
<button
onClick={onDuplicateCampaign}
className="flex items-center gap-1.5 text-xs font-bold text-gray-700 bg-white border border-gray-200 hover:bg-gray-50 px-3 py-1.5 rounded-xl transition-all shadow-xs"
title="Vytvoriť kópiu tohto newslettera"
>
<Copy className="w-3.5 h-3.5 text-gray-600" />
<span className="hidden md:inline">Duplikovať</span>
</button>
<button
onClick={onExportZip}
className="flex items-center gap-1.5 text-xs font-bold text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 px-4 py-2 rounded-xl transition-all shadow-xs hover:shadow-sm"
className="flex items-center gap-1.5 text-xs font-bold text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 px-3.5 py-1.5 rounded-xl transition-all shadow-xs"
>
<Download className="w-4 h-4 text-gray-600" /> Export ZIP
<Download className="w-3.5 h-3.5 text-gray-600" />
<span className="hidden sm:inline">Export ZIP</span>
</button>
{brevoCampaignUrl ? (
<a
href={brevoCampaignUrl}
target="_blank"
rel="noreferrer"
className="flex items-center gap-1.5 text-xs font-bold text-emerald-700 bg-emerald-50 border border-emerald-300 hover:bg-emerald-100 px-4 py-2 rounded-xl transition-all shadow-xs"
>
<ExternalLink className="w-4 h-4" /> Otvoriť v Brevo
</a>
<div className="flex items-center gap-1.5">
<a
href={brevoCampaignUrl}
target="_blank"
rel="noreferrer"
className="flex items-center gap-1.5 text-xs font-bold text-white bg-emerald-600 hover:bg-emerald-700 px-3.5 py-1.5 rounded-xl transition-all shadow-xs"
title="Upraviť a odoslať kampaň priamo v Brevo"
>
<ExternalLink className="w-3.5 h-3.5" />
<span>Upraviť v Brevo</span>
</a>
<a
href={brevoListingUrl || 'https://app.brevo.com/campaigns/listing'}
target="_blank"
rel="noreferrer"
className="hidden lg:flex items-center gap-1 text-[11px] font-bold text-emerald-800 bg-emerald-100 hover:bg-emerald-200 px-2.5 py-1.5 rounded-xl transition-all"
title="Prehľad všetkých e-mailových kampaní v Brevo"
>
Zoznam
</a>
</div>
) : (
<button
onClick={onExportBrevo}
disabled={isExportingBrevo}
className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#003399] hover:bg-blue-800 disabled:opacity-50 px-4 py-2 rounded-xl transition-all shadow-xs"
className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#003399] hover:bg-blue-800 disabled:opacity-50 px-3.5 py-1.5 rounded-xl transition-all shadow-xs"
>
<Send className="w-4 h-4" />
{isExportingBrevo ? 'Odosielam...' : 'Odoslať do Brevo'}
<Send className="w-3.5 h-3.5" />
<span>{isExportingBrevo ? 'Odosielam...' : 'Odoslať do Brevo'}</span>
</button>
)}
</div>