Implement WYSIWYG newsletter editor with live preview, import, Supabase auto-save and Brevo export
This commit is contained in:
1 parent
8763c5bace
commit
538f2ccae3
10 files changed
+2252
-62
No files matched your search
@@ -0,0 +1,420 @@
|
||||
'use client';
|
||||
|
||||
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';
|
||||
|
||||
interface ConfigPanelProps {
|
||||
module: NewsletterModule | null;
|
||||
onUpdateModule: (updated: NewsletterModule) => void;
|
||||
onOpenBadgeManager?: (productId: string) => void;
|
||||
onOpenImport?: () => void;
|
||||
}
|
||||
|
||||
export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
module,
|
||||
onUpdateModule,
|
||||
onOpenBadgeManager,
|
||||
onOpenImport
|
||||
}) => {
|
||||
if (!module) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center p-6 text-center text-gray-400">
|
||||
<p className="text-sm">Vyberte modul v náhľade e-mailu pre úpravu jeho vlastností.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const updateData = (newData: any) => {
|
||||
onUpdateModule({
|
||||
...module,
|
||||
data: { ...module.data, ...newData }
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto p-5 space-y-6 bg-white border-l border-gray-200">
|
||||
<div className="border-b border-gray-100 pb-3">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-red-600 bg-red-50 px-2 py-0.5 rounded">
|
||||
{module.type}
|
||||
</span>
|
||||
<h2 className="text-base font-bold text-gray-900 mt-1">{module.title}</h2>
|
||||
</div>
|
||||
|
||||
{/* TOP ANNOUNCEMENT */}
|
||||
{module.type === 'announcement' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Text badge (napr. TIP)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.tipBadge || ''}
|
||||
onChange={(e) => updateData({ tipBadge: 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">Hlavný text oznamu</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.text || ''}
|
||||
onChange={(e) => updateData({ text: 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">Text odkazu</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.linkText || ''}
|
||||
onChange={(e) => updateData({ linkText: 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">Cieľová URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.linkUrl || ''}
|
||||
onChange={(e) => updateData({ linkUrl: e.target.value })}
|
||||
className="w-full text-sm border border-gray-300 rounded-lg p-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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"
|
||||
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"
|
||||
/>
|
||||
</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>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Odkaz bannera</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.linkUrl || ''}
|
||||
onChange={(e) => updateData({ linkUrl: 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">Nadpis pod bannerom</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.title || ''}
|
||||
onChange={(e) => updateData({ title: 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">Popis</label>
|
||||
<textarea
|
||||
value={module.data.description || ''}
|
||||
onChange={(e) => updateData({ description: e.target.value })}
|
||||
className="w-full text-sm border border-gray-300 rounded-lg p-2 h-20"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Text tlačidla</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.buttonText || ''}
|
||||
onChange={(e) => updateData({ buttonText: 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">Farba tlačidla</label>
|
||||
<input
|
||||
type="color"
|
||||
value={module.data.buttonBgColor || '#e2001a'}
|
||||
onChange={(e) => updateData({ buttonBgColor: e.target.value })}
|
||||
className="w-full h-9 rounded-lg cursor-pointer border border-gray-300 p-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PRODUCTS GRID */}
|
||||
{module.type === 'products_grid' && (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-bold text-gray-800 uppercase tracking-wider">
|
||||
Nastavenia sekcie
|
||||
</h3>
|
||||
{onOpenImport && (
|
||||
<button
|
||||
onClick={onOpenImport}
|
||||
className="text-xs font-bold text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100 px-2.5 py-1 rounded-lg transition-colors"
|
||||
>
|
||||
Hromadný import
|
||||
</button>
|
||||
)}
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
{/* Zoznam produktov */}
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-gray-700">
|
||||
Produkty ({module.data.products?.length || 0})
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newP: ProductItem = {
|
||||
id: `p-${Date.now()}`,
|
||||
title: 'Nový produkt',
|
||||
imageUrl: `${SUPABASE_ASSETS_BASE}produkt_1.png`,
|
||||
priceMain: '1',
|
||||
priceSub: '99',
|
||||
unitPrice: '1kg = 4,99 €',
|
||||
discountText: '20%',
|
||||
badgeText: 'zľava',
|
||||
cardBgColor: '#feeaea',
|
||||
stores: { potraviny: true, supermarket: true, tempo: true }
|
||||
};
|
||||
updateData({ products: [...(module.data.products || []), newP] });
|
||||
}}
|
||||
className="text-xs font-bold text-white bg-gray-900 hover:bg-black px-2.5 py-1 rounded-lg flex items-center gap-1"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" /> Pridať produkt
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(module.data.products || []).map((p: ProductItem, pIdx: number) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="p-3 bg-gray-50 rounded-xl border border-gray-200 space-y-3 relative group"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-gray-900 truncate max-w-[180px]">
|
||||
{p.title || `Produkt ${pIdx + 1}`}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const updated = (module.data.products || []).filter((_: any, i: number) => i !== pIdx);
|
||||
updateData({ products: updated });
|
||||
}}
|
||||
className="text-gray-400 hover:text-red-600 p-1"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={p.title}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].title = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="Názov produktu"
|
||||
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500">Cena (€)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.priceMain}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].priceMain = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500">Centy</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.priceSub}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].priceSub = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500">Zľava %</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.discountText}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].discountText = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500">Merná cena</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.unitPrice}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].unitPrice = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white text-[10px]"
|
||||
/>
|
||||
</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={`flag-${p.id}`}
|
||||
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={`flag-${p.id}`} className="text-xs text-gray-700 cursor-pointer">
|
||||
Zobraziť slovenskú vlajku / srdce
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* FOOTER */}
|
||||
{module.type === 'footer' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Právny text (Disclaimer)</label>
|
||||
<textarea
|
||||
value={module.data.disclaimer || ''}
|
||||
onChange={(e) => updateData({ disclaimer: e.target.value })}
|
||||
className="w-full text-sm border border-gray-300 rounded-lg p-2 h-24"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Text odhlásenia</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.unsubscribeText || ''}
|
||||
onChange={(e) => updateData({ unsubscribeText: e.target.value })}
|
||||
className="w-full text-sm border border-gray-300 rounded-lg p-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user