Implement WYSIWYG newsletter editor with live preview, import, Supabase auto-save and Brevo export

This commit is contained in:
Jakub_Hostacny committed 2026-09-24 11:04:13 +02:00
1 parent 8763c5bace
commit 538f2ccae3
10 files changed
+2252 -62

No files matched your search

+196
View File
@@ -0,0 +1,196 @@
'use client';
import React, { useState, useEffect } from 'react';
import { BadgeTemplate } from '@/types/newsletter';
import { supabase } from '@/lib/supabase';
import { Tag, Plus, Check, Trash2, X } from 'lucide-react';
interface BadgeManagerModalProps {
isOpen: boolean;
onClose: () => void;
onSelectBadge: (badge: BadgeTemplate) => void;
}
export const BadgeManagerModal: React.FC<BadgeManagerModalProps> = ({
isOpen,
onClose,
onSelectBadge
}) => {
const [badges, setBadges] = useState<BadgeTemplate[]>([]);
const [loading, setLoading] = useState(false);
const [creating, setCreating] = useState(false);
const [newBadgeName, setNewBadgeName] = useState('');
const [newBadgeColor, setNewBadgeColor] = useState('#e2001a');
const [newBadgeText, setNewBadgeText] = useState('SUPER CENA');
useEffect(() => {
if (isOpen) {
loadBadges();
}
}, [isOpen]);
const loadBadges = async () => {
setLoading(true);
try {
const { data, error } = await supabase.from('badges').select('*').order('created_at', { ascending: false });
if (data && !error) {
setBadges(
data.map((d: any) => ({
id: d.id,
name: d.name,
svgRaw: d.svg_raw,
layersConfig: d.layers_config || []
}))
);
}
} catch (err) {
console.error('Error loading badges:', err);
} finally {
setLoading(false);
}
};
const createBadge = async () => {
if (!newBadgeName) return;
const svg = `<svg viewBox="0 0 160 80" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="80" rx="8" fill="${newBadgeColor}"/></svg>`;
const layers = [
{ id: 'l1', type: 'text', text: newBadgeText, x: 80, y: 35, fontSize: 18, fontWeight: '900', fill: '#ffffff', align: 'center' },
{ id: 'l2', type: 'text', text: '{zlava}', x: 80, y: 65, fontSize: 24, fontWeight: '900', fill: '#ffd100', align: 'center' }
];
try {
const { data, error } = await supabase.from('badges').insert([
{
name: newBadgeName,
svg_raw: svg,
layers_config: layers
}
]).select();
if (data && !error) {
loadBadges();
setCreating(false);
setNewBadgeName('');
}
} catch (err) {
console.error('Error creating badge:', err);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-xs flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<Tag className="w-5 h-5 text-red-600" />
<h2 className="text-lg font-bold text-gray-900">Správca grafických cenoviek</h2>
</div>
<button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded-lg">
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto space-y-4">
<div className="flex justify-between items-center mb-2">
<p className="text-sm text-gray-500">
Vyberte cenovku pre produkt alebo navrhnite vlastnú.
</p>
<button
onClick={() => setCreating(!creating)}
className="text-xs font-bold text-red-600 hover:text-red-700 flex items-center gap-1 border border-red-200 px-3 py-1.5 rounded-lg hover:bg-red-50"
>
<Plus className="w-3.5 h-3.5" />
Nová cenovka
</button>
</div>
{/* Create Form */}
{creating && (
<div className="p-4 bg-gray-50 rounded-xl border border-gray-200 space-y-3">
<h3 className="text-xs font-bold text-gray-800 uppercase tracking-wider">Vytvoriť cenovku</h3>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Názov cenovky</label>
<input
type="text"
value={newBadgeName}
onChange={(e) => setNewBadgeName(e.target.value)}
placeholder="Napr. Modrá novinka"
className="w-full text-sm border border-gray-300 rounded-lg p-2"
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Text cenovky</label>
<input
type="text"
value={newBadgeText}
onChange={(e) => setNewBadgeText(e.target.value)}
placeholder="Napr. EXTRA CENA"
className="w-full text-sm border border-gray-300 rounded-lg p-2"
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Farba pozadia</label>
<div className="flex items-center gap-2">
<input
type="color"
value={newBadgeColor}
onChange={(e) => setNewBadgeColor(e.target.value)}
className="w-8 h-8 rounded-md cursor-pointer border-0"
/>
<span className="text-xs font-mono">{newBadgeColor}</span>
</div>
</div>
</div>
<div className="flex justify-end gap-2 pt-2">
<button
onClick={() => setCreating(false)}
className="px-3 py-1.5 text-xs font-semibold text-gray-600"
>
Zrušiť
</button>
<button
onClick={createBadge}
className="px-4 py-1.5 text-xs font-bold bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Uložiť do databázy
</button>
</div>
</div>
)}
{/* Badges List */}
<div className="grid grid-cols-2 gap-4">
{badges.map((b) => (
<div
key={b.id}
className="border border-gray-200 rounded-xl p-4 flex flex-col items-center justify-between hover:border-red-400 hover:shadow-md transition-all group bg-white"
>
<span className="text-xs font-bold text-gray-700 mb-2">{b.name}</span>
<div
className="w-36 h-20 flex items-center justify-center relative my-2 overflow-hidden rounded-lg shadow-xs"
dangerouslySetInnerHTML={{ __html: b.svgRaw || '' }}
/>
<button
onClick={() => {
onSelectBadge(b);
onClose();
}}
className="mt-3 w-full py-1.5 text-xs font-bold text-white bg-gray-900 group-hover:bg-red-600 rounded-lg transition-colors flex items-center justify-center gap-1.5"
>
<Check className="w-3.5 h-3.5" />
Použiť na produkt
</button>
</div>
))}
</div>
</div>
</div>
</div>
);
};
+420
View File
@@ -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>
);
};
@@ -0,0 +1,326 @@
'use client';
import React, { useState } from 'react';
import Papa from 'papaparse';
import { XMLParser } from 'fast-xml-parser';
import { ProductItem } from '@/types/newsletter';
import { Upload, Check, AlertCircle, FileText, X } from 'lucide-react';
import { supabase } from '@/lib/supabase';
interface ImportModalProps {
isOpen: boolean;
onClose: () => void;
onImportProducts: (products: ProductItem[]) => void;
}
export const ProductImportModal: React.FC<ImportModalProps> = ({
isOpen,
onClose,
onImportProducts
}) => {
const [file, setFile] = useState<File | null>(null);
const [fileType, setFileType] = useState<'csv' | 'json' | 'xml'>('csv');
const [parsedRows, setParsedRows] = useState<any[]>([]);
const [columns, setColumns] = useState<string[]>([]);
// Mapovanie stĺpcov
const [mapping, setMapping] = useState<{
title: string;
priceMain: string;
priceSub: string;
unitPrice: string;
discountText: string;
imageUrl: string;
validity: string;
}>({
title: '',
priceMain: '',
priceSub: '',
unitPrice: '',
discountText: '',
imageUrl: '',
validity: ''
});
const [presetName, setPresetName] = useState('');
const [savedPresets, setSavedPresets] = useState<any[]>([]);
if (!isOpen) return null;
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const uploaded = e.target.files?.[0];
if (!uploaded) return;
setFile(uploaded);
const ext = uploaded.name.split('.').pop()?.toLowerCase();
const reader = new FileReader();
if (ext === 'csv') {
setFileType('csv');
reader.onload = (event) => {
const text = event.target?.result as string;
Papa.parse(text, {
header: true,
skipEmptyLines: true,
complete: (results) => {
if (results.data.length > 0) {
setParsedRows(results.data);
const headers = Object.keys(results.data[0] as object);
setColumns(headers);
autoGuessMapping(headers);
}
}
});
};
reader.readAsText(uploaded);
} else if (ext === 'json') {
setFileType('json');
reader.onload = (event) => {
try {
const json = JSON.parse(event.target?.result as string);
const arrayData = Array.isArray(json) ? json : json.products || [json];
setParsedRows(arrayData);
if (arrayData.length > 0) {
const headers = Object.keys(arrayData[0]);
setColumns(headers);
autoGuessMapping(headers);
}
} catch (err) {
alert('Chyba pri parsovaní JSON súboru.');
}
};
reader.readAsText(uploaded);
} else if (ext === 'xml') {
setFileType('xml');
reader.onload = (event) => {
try {
const parser = new XMLParser({ ignoreAttributes: false });
const jsonObj = parser.parse(event.target?.result as string);
// Hľadáme pole produktov
const possibleArray =
jsonObj.products?.product || jsonObj.items?.item || Object.values(jsonObj)[0];
const arrayData = Array.isArray(possibleArray) ? possibleArray : [possibleArray];
setParsedRows(arrayData);
if (arrayData.length > 0) {
const headers = Object.keys(arrayData[0]);
setColumns(headers);
autoGuessMapping(headers);
}
} catch (err) {
alert('Chyba pri parsovaní XML súboru.');
}
};
reader.readAsText(uploaded);
}
};
const autoGuessMapping = (headers: string[]) => {
const newMapping = { ...mapping };
headers.forEach((h) => {
const lower = h.toLowerCase();
if (lower.includes('nazov') || lower.includes('title') || lower.includes('name')) newMapping.title = h;
if (lower.includes('hlavna') || lower.includes('euro') || lower.includes('price_main')) newMapping.priceMain = h;
if (lower.includes('cent') || lower.includes('sub') || lower.includes('price_sub')) newMapping.priceSub = h;
if (lower.includes('merna') || lower.includes('unit') || lower.includes('kg')) newMapping.unitPrice = h;
if (lower.includes('zlava') || lower.includes('discount')) newMapping.discountText = h;
if (lower.includes('obrazok') || lower.includes('image') || lower.includes('foto')) newMapping.imageUrl = h;
if (lower.includes('platnost') || lower.includes('valid')) newMapping.validity = h;
});
setMapping(newMapping);
};
const applyImport = () => {
if (!mapping.title) {
alert('Prosím vyberte stĺpec pre Názov produktu.');
return;
}
const importedProducts: ProductItem[] = parsedRows.map((row, idx) => {
let pMain = String(row[mapping.priceMain] || '0').trim();
let pSub = String(row[mapping.priceSub] || '00').trim();
// Ak je cena v jednom stĺpci napr. "1.99" alebo "1,99"
if (!mapping.priceSub && mapping.priceMain) {
const fullPrice = String(row[mapping.priceMain] || '0').replace(',', '.');
const parts = fullPrice.split('.');
pMain = parts[0] || '0';
pSub = parts[1] || '00';
if (pSub.length === 1) pSub += '0';
}
return {
id: `imported-${Date.now()}-${idx}`,
title: String(row[mapping.title] || 'Nový produkt'),
imageUrl: row[mapping.imageUrl] || 'https://dqexxfspkylqttrtxsww.supabase.co/storage/v1/object/public/newsletter-assets/produkt_1.png',
priceMain: pMain,
priceSub: pSub,
unitPrice: row[mapping.unitPrice] || '',
discountText: row[mapping.discountText] ? `${row[mapping.discountText]}%` : '',
badgeText: 'zľava',
cardBgColor: '#feeaea',
stores: { potraviny: true, supermarket: true, tempo: true },
validityOverride: row[mapping.validity] || undefined
};
});
onImportProducts(importedProducts);
onClose();
};
return (
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-xs flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<Upload className="w-5 h-5 text-red-600" />
<h2 className="text-lg font-bold text-gray-900">Hromadný import produktov</h2>
</div>
<button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded-lg">
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto space-y-6">
{/* File Upload Box */}
<div className="border-2 border-dashed border-gray-300 hover:border-red-500 rounded-xl p-6 text-center cursor-pointer transition-colors bg-gray-50/50">
<input
type="file"
accept=".csv,.json,.xml"
onChange={handleFileUpload}
className="hidden"
id="file-upload-input"
/>
<label htmlFor="file-upload-input" className="cursor-pointer">
<FileText className="w-10 h-10 text-gray-400 mx-auto mb-2" />
<p className="font-semibold text-gray-700">
{file ? file.name : 'Kliknite sem alebo pretiahnite CSV, JSON alebo XML'}
</p>
<p className="text-xs text-gray-400 mt-1">Podporované formáty: .csv, .json, .xml</p>
</label>
</div>
{/* Mapping Table */}
{columns.length > 0 && (
<div className="space-y-4">
<h3 className="text-sm font-bold text-gray-800 uppercase tracking-wider">
Vizuálny Data Mapper (Párovanie stĺpcov)
</h3>
<div className="grid grid-cols-2 gap-4 bg-gray-50 p-4 rounded-xl border border-gray-200">
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Názov produktu *</label>
<select
value={mapping.title}
onChange={(e) => setMapping({ ...mapping, title: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
>
<option value="">-- Vyberte stĺpec --</option>
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Hlavná cena (Eurá)</label>
<select
value={mapping.priceMain}
onChange={(e) => setMapping({ ...mapping, priceMain: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
>
<option value="">-- Vyberte stĺpec --</option>
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Centy (podružná cena)</label>
<select
value={mapping.priceSub}
onChange={(e) => setMapping({ ...mapping, priceSub: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
>
<option value="">-- Vyberte stĺpec (voliteľné) --</option>
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Merná cena (napr. 1kg = 4,91 €)</label>
<select
value={mapping.unitPrice}
onChange={(e) => setMapping({ ...mapping, unitPrice: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
>
<option value="">-- Vyberte stĺpec --</option>
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Zľava %</label>
<select
value={mapping.discountText}
onChange={(e) => setMapping({ ...mapping, discountText: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
>
<option value="">-- Vyberte stĺpec --</option>
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">URL obrázka</label>
<select
value={mapping.imageUrl}
onChange={(e) => setMapping({ ...mapping, imageUrl: e.target.value })}
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
>
<option value="">-- Vyberte stĺpec --</option>
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
</div>
{/* Živý náhľad prvých 2 riadkov */}
{parsedRows.length > 0 && (
<div className="border border-gray-200 rounded-xl overflow-hidden">
<div className="bg-gray-100 px-4 py-2 text-xs font-bold text-gray-700">
Živý náhľad prvých položiek ({parsedRows.length} nájdených záznamov)
</div>
<div className="p-3 text-xs space-y-2 bg-white">
{parsedRows.slice(0, 2).map((row, idx) => (
<div key={idx} className="flex items-center justify-between border-b border-gray-100 pb-2">
<span className="font-semibold text-gray-800">{row[mapping.title] || 'Bez názvu'}</span>
<span className="text-red-600 font-bold">
{row[mapping.priceMain] || '0'},{row[mapping.priceSub] || '00'} €
</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-gray-100 bg-gray-50 flex items-center justify-between">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-semibold text-gray-600 hover:text-gray-900"
>
Zrušiť
</button>
<button
onClick={applyImport}
disabled={parsedRows.length === 0}
className="px-5 py-2 text-sm font-bold bg-red-600 text-white rounded-xl hover:bg-red-700 disabled:opacity-50 transition-colors flex items-center gap-2"
>
<Check className="w-4 h-4" />
Importovať do kampane
</button>
</div>
</div>
</div>
);
};
+139
View File
@@ -0,0 +1,139 @@
'use client';
import React from 'react';
import {
Download,
Send,
ExternalLink,
Smartphone,
Monitor,
Save,
CheckCircle2,
Plus
} from 'lucide-react';
import { NewsletterCampaign } from '@/types/newsletter';
interface TopBarProps {
campaign: NewsletterCampaign;
previewMode: 'desktop' | 'mobile';
setPreviewMode: (m: 'desktop' | 'mobile') => void;
isSaving: boolean;
onSave: () => void;
onExportZip: () => void;
onExportBrevo: () => void;
isExportingBrevo: boolean;
brevoCampaignUrl?: string | null;
onAddModule: (type: any) => void;
}
export const TopBar: React.FC<TopBarProps> = ({
campaign,
previewMode,
setPreviewMode,
isSaving,
onSave,
onExportZip,
onExportBrevo,
isExportingBrevo,
brevoCampaignUrl,
onAddModule
}) => {
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 názov */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-red-600 flex items-center justify-center text-white font-black text-sm">
CJ
</div>
<div>
<h1 className="text-sm font-bold text-gray-900 leading-tight">
{campaign.title || 'COOP Newsletter'}
</h1>
<div className="flex items-center gap-2">
<span className="text-[10px] text-gray-500">
{isSaving ? (
<span className="text-amber-600 animate-pulse">Ukladám do Supabase...</span>
) : (
<span className="text-emerald-600 flex items-center gap-1">
<CheckCircle2 className="w-3 h-3" /> Uložené v Supabase
</span>
)}
</span>
</div>
</div>
</div>
{/* Pridať modul dropdown */}
<div className="hidden md:flex items-center gap-1.5 ml-4 pl-4 border-l border-gray-200">
<button
onClick={() => onAddModule('banner')}
className="text-xs font-semibold text-gray-700 bg-gray-100 hover:bg-gray-200 px-2.5 py-1.5 rounded-lg flex items-center gap-1 transition-colors"
>
<Plus className="w-3.5 h-3.5 text-gray-500" /> Banner
</button>
<button
onClick={() => onAddModule('products_grid')}
className="text-xs font-semibold text-gray-700 bg-gray-100 hover:bg-gray-200 px-2.5 py-1.5 rounded-lg flex items-center gap-1 transition-colors"
>
<Plus className="w-3.5 h-3.5 text-gray-500" /> Produkty
</button>
</div>
</div>
{/* Stredná časť: Prepínač Desktop / Mobil */}
<div className="flex items-center bg-gray-100 p-1 rounded-xl border border-gray-200">
<button
onClick={() => setPreviewMode('desktop')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
previewMode === 'desktop'
? 'bg-white text-gray-900 shadow-xs'
: 'text-gray-500 hover:text-gray-900'
}`}
>
<Monitor className="w-3.5 h-3.5" /> Desktop
</button>
<button
onClick={() => setPreviewMode('mobile')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
previewMode === 'mobile'
? 'bg-white text-gray-900 shadow-xs'
: 'text-gray-500 hover:text-gray-900'
}`}
>
<Smartphone className="w-3.5 h-3.5" /> Mobil
</button>
</div>
{/* Pravá časť: Export do ZIP a Brevo */}
<div className="flex items-center gap-2.5">
<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-3.5 py-2 rounded-xl transition-all shadow-xs"
>
<Download className="w-3.5 h-3.5 text-gray-600" /> Export ZIP
</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-3.5 py-2 rounded-xl transition-all"
>
<ExternalLink className="w-3.5 h-3.5" /> Otvoriť v Brevo
</a>
) : (
<button
onClick={onExportBrevo}
disabled={isExportingBrevo}
className="flex items-center gap-1.5 text-xs font-bold text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 px-3.5 py-2 rounded-xl transition-all shadow-xs"
>
<Send className="w-3.5 h-3.5" />
{isExportingBrevo ? 'Odosielam...' : 'Odoslať do Brevo'}
</button>
)}
</div>
</header>
);
};