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,75 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { campaignName, subject, htmlContent, scheduledAt } = await req.json();
|
||||
|
||||
if (!campaignName || !subject || !htmlContent) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Chýbajú povinné polia (campaignName, subject, htmlContent).' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const apiKey = process.env.BREVO_API_KEY;
|
||||
const senderName = process.env.BREVO_SENDER_NAME || 'Jandl';
|
||||
const senderEmail = process.env.BREVO_SENDER_EMAIL || 'tomas.rafay@jandl.sk';
|
||||
|
||||
if (!apiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: 'BREVO_API_KEY nie je nakonfigurovaný na serveri.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Volanie Brevo API v3
|
||||
const payload: any = {
|
||||
name: campaignName,
|
||||
subject: subject,
|
||||
sender: {
|
||||
name: senderName,
|
||||
email: senderEmail
|
||||
},
|
||||
type: 'classic',
|
||||
htmlContent: htmlContent
|
||||
};
|
||||
|
||||
if (scheduledAt) {
|
||||
payload.scheduledAt = scheduledAt;
|
||||
}
|
||||
|
||||
const brevoRes = await fetch('https://api.brevo.com/v3/emailCampaigns', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
'api-key': apiKey
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await brevoRes.json();
|
||||
|
||||
if (!brevoRes.ok) {
|
||||
console.error('Brevo API error:', data);
|
||||
return NextResponse.json(
|
||||
{ error: data.message || 'Chyba pri vytváraní kampane v Brevo', details: data },
|
||||
{ status: brevoRes.status }
|
||||
);
|
||||
}
|
||||
|
||||
// Úspech - vrátime campaignId a URL na správu v Brevo
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
campaignId: data.id,
|
||||
brevoUrl: `https://app.brevo.com/camp/detail/email/${data.id}`,
|
||||
message: 'Kampaň bola úspešne vytvorená v Brevo!'
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Export to Brevo error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Interná chyba servera' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import juice from 'juice';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { html } = await req.json();
|
||||
|
||||
if (!html) {
|
||||
return NextResponse.json({ error: 'HTML obsah je povinný.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const inlined = juice(html, {
|
||||
preserveMediaQueries: true,
|
||||
removeStyleTags: false,
|
||||
applyWidthAttributes: true,
|
||||
applyHeightAttributes: true
|
||||
});
|
||||
|
||||
return NextResponse.json({ html: inlined });
|
||||
} catch (err: any) {
|
||||
console.error('Inlining error:', err);
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
+438
-62
@@ -1,69 +1,445 @@
|
||||
import Image from "next/image";
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter';
|
||||
import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign';
|
||||
import { renderEmailHtml } from '@/lib/emailRenderer';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { TopBar } from '@/components/editor/TopBar';
|
||||
import { ConfigPanel } from '@/components/editor/ConfigPanel';
|
||||
import { ProductImportModal } from '@/components/editor/ProductImportModal';
|
||||
import { BadgeManagerModal } from '@/components/editor/BadgeManagerModal';
|
||||
import JSZip from 'jszip';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { ChevronUp, ChevronDown, Copy, Trash2 } from 'lucide-react';
|
||||
|
||||
export default function Home() {
|
||||
const [campaign, setCampaign] = useState<NewsletterCampaign>(DEFAULT_CAMPAIGN);
|
||||
const [selectedModuleId, setSelectedModuleId] = useState<string | null>('mod-products-fantasticky');
|
||||
const [previewMode, setPreviewMode] = useState<'desktop' | 'mobile'>('desktop');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isExportingBrevo, setIsExportingBrevo] = useState(false);
|
||||
const [brevoUrl, setBrevoUrl] = useState<string | null>(null);
|
||||
|
||||
// Modály
|
||||
const [isImportOpen, setIsImportOpen] = useState(false);
|
||||
const [isBadgeModalOpen, setIsBadgeModalOpen] = useState(false);
|
||||
const [activeProductIdForBadge, setActiveProductIdForBadge] = useState<string | null>(null);
|
||||
|
||||
// Načítanie existujúceho draftu z databázy alebo inicializácia
|
||||
useEffect(() => {
|
||||
async function loadInitial() {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.select('*')
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(1);
|
||||
|
||||
if (data && data.length > 0) {
|
||||
const dbItem = data[0];
|
||||
setCampaign({
|
||||
id: dbItem.id,
|
||||
title: dbItem.title,
|
||||
preheader: dbItem.data.preheader || DEFAULT_CAMPAIGN.preheader,
|
||||
status: dbItem.status,
|
||||
brevoCampaignId: dbItem.brevo_campaign_id,
|
||||
modules: dbItem.data.modules || DEFAULT_CAMPAIGN.modules
|
||||
});
|
||||
if (dbItem.brevo_campaign_id) {
|
||||
setBrevoUrl(`https://app.brevo.com/camp/detail/email/${dbItem.brevo_campaign_id}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading template from Supabase:', err);
|
||||
}
|
||||
}
|
||||
loadInitial();
|
||||
}, []);
|
||||
|
||||
// Auto-save s debounce (1 sekunda)
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
if (!campaign.id) {
|
||||
// Vytvorenie nového záznamu
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.insert([
|
||||
{
|
||||
title: campaign.title,
|
||||
status: campaign.status,
|
||||
data: {
|
||||
preheader: campaign.preheader,
|
||||
modules: campaign.modules
|
||||
}
|
||||
}
|
||||
])
|
||||
.select();
|
||||
|
||||
if (data && data.length > 0) {
|
||||
setCampaign((prev) => ({ ...prev, id: data[0].id }));
|
||||
}
|
||||
} else {
|
||||
// Update existujúceho
|
||||
await supabase
|
||||
.from('templates')
|
||||
.update({
|
||||
title: campaign.title,
|
||||
data: {
|
||||
preheader: campaign.preheader,
|
||||
modules: campaign.modules
|
||||
},
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', campaign.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Auto-save error:', err);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [campaign.title, campaign.preheader, campaign.modules]);
|
||||
|
||||
// Vybraný modul
|
||||
const selectedModule = campaign.modules.find((m) => m.id === selectedModuleId) || null;
|
||||
|
||||
// Aktualizácia modulu
|
||||
const handleUpdateModule = (updated: NewsletterModule) => {
|
||||
setCampaign((prev) => ({
|
||||
...prev,
|
||||
modules: prev.modules.map((m) => (m.id === updated.id ? updated : m))
|
||||
}));
|
||||
};
|
||||
|
||||
// Posun modulu hore/dole
|
||||
const moveModule = (index: number, direction: 'up' | 'down') => {
|
||||
const newModules = [...campaign.modules];
|
||||
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
if (targetIndex < 0 || targetIndex >= newModules.length) return;
|
||||
|
||||
const temp = newModules[index];
|
||||
newModules[index] = newModules[targetIndex];
|
||||
newModules[targetIndex] = temp;
|
||||
|
||||
setCampaign((prev) => ({ ...prev, modules: newModules }));
|
||||
};
|
||||
|
||||
// Duplikácia modulu
|
||||
const duplicateModule = (index: number) => {
|
||||
const original = campaign.modules[index];
|
||||
const clone: NewsletterModule = {
|
||||
...original,
|
||||
id: `mod-${Date.now()}`,
|
||||
title: `${original.title} (kópia)`,
|
||||
data: JSON.parse(JSON.stringify(original.data))
|
||||
};
|
||||
const newModules = [...campaign.modules];
|
||||
newModules.splice(index + 1, 0, clone);
|
||||
setCampaign((prev) => ({ ...prev, modules: newModules }));
|
||||
setSelectedModuleId(clone.id);
|
||||
};
|
||||
|
||||
// Zmazanie modulu
|
||||
const deleteModule = (index: number) => {
|
||||
if (campaign.modules.length <= 1) return;
|
||||
const newModules = campaign.modules.filter((_, i) => i !== index);
|
||||
setCampaign((prev) => ({ ...prev, modules: newModules }));
|
||||
setSelectedModuleId(newModules[0]?.id || null);
|
||||
};
|
||||
|
||||
// Pridanie nového modulu
|
||||
const handleAddModule = (type: any) => {
|
||||
let newMod: NewsletterModule;
|
||||
if (type === 'banner') {
|
||||
newMod = {
|
||||
id: `mod-banner-${Date.now()}`,
|
||||
type: 'banner',
|
||||
title: 'Nový Banner',
|
||||
data: {
|
||||
imageUrl: `${SUPABASE_ASSETS_BASE}aktualne_letaky.jpg`,
|
||||
linkUrl: 'https://www.coop.sk',
|
||||
altText: 'Aktuálne letáky',
|
||||
title: 'Prelistujte si naše najnovšie letáky online',
|
||||
description: 'Všetky akcie a zľavy na jednom mieste priamo vo vašom mobile.',
|
||||
buttonText: 'Pozrieť letáky',
|
||||
buttonUrl: 'https://www.coop.sk',
|
||||
buttonBgColor: '#e2001a'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
newMod = {
|
||||
id: `mod-products-${Date.now()}`,
|
||||
type: 'products_grid',
|
||||
title: 'Nová sekcia produktov',
|
||||
data: {
|
||||
sectionTitle: 'AKČNÁ PONUKA',
|
||||
globalValidity: '2. 5. – 15. 5. 2026',
|
||||
products: [
|
||||
{
|
||||
id: `p-${Date.now()}-1`,
|
||||
title: 'Nový produkt 1',
|
||||
imageUrl: `${SUPABASE_ASSETS_BASE}produkt_5.png`,
|
||||
priceMain: '2',
|
||||
priceSub: '49',
|
||||
unitPrice: '1kg = 9,90 €',
|
||||
discountText: '15%',
|
||||
badgeText: 'zľava',
|
||||
cardBgColor: '#feeaea',
|
||||
stores: { potraviny: true, supermarket: true, tempo: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setCampaign((prev) => ({
|
||||
...prev,
|
||||
modules: [...prev.modules, newMod]
|
||||
}));
|
||||
setSelectedModuleId(newMod.id);
|
||||
};
|
||||
|
||||
// Hromadný import produktov do aktuálne vybraného modulu
|
||||
const handleImportProducts = (imported: ProductItem[]) => {
|
||||
if (!selectedModuleId) return;
|
||||
const target = campaign.modules.find((m) => m.id === selectedModuleId);
|
||||
if (!target || target.type !== 'products_grid') {
|
||||
alert('Pre import prosím najskôr označte produktovú sekciu.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = {
|
||||
...target,
|
||||
data: {
|
||||
...target.data,
|
||||
products: [...(target.data.products || []), ...imported]
|
||||
}
|
||||
};
|
||||
handleUpdateModule(updated);
|
||||
};
|
||||
|
||||
// Export do ZIP (JSZip)
|
||||
const handleExportZip = async () => {
|
||||
const rawHtml = renderEmailHtml(campaign);
|
||||
|
||||
// Inlinovanie CSS pred zabalením
|
||||
let finalHtml = rawHtml;
|
||||
try {
|
||||
const res = await fetch('/api/inline-css', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ html: rawHtml })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.html) finalHtml = data.html;
|
||||
} catch (e) {
|
||||
console.warn('Inliner fallback to raw html:', e);
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file('index.html', finalHtml);
|
||||
|
||||
// Pridáme README návod
|
||||
zip.file(
|
||||
'README.txt',
|
||||
'COOP Jednota Newsletter Export\n\nSúbor index.html obsahuje finálny skompletizovaný a inlinovaný HTML kód pripravený na odoslanie cez ľubovoľný e-mailový systém (Brevo, Mailchimp, Outlook).'
|
||||
);
|
||||
|
||||
const blob = await zip.generateAsync({ type: 'blob' });
|
||||
saveAs(blob, `coop-newsletter-${new Date().toISOString().split('T')[0]}.zip`);
|
||||
};
|
||||
|
||||
// Export do Brevo (API v3)
|
||||
const handleExportBrevo = async () => {
|
||||
setIsExportingBrevo(true);
|
||||
try {
|
||||
const rawHtml = renderEmailHtml(campaign);
|
||||
|
||||
// Inlinovanie
|
||||
let finalHtml = rawHtml;
|
||||
try {
|
||||
const res = await fetch('/api/inline-css', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ html: rawHtml })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.html) finalHtml = data.html;
|
||||
} catch (e) {}
|
||||
|
||||
const response = await fetch('/api/brevo/export', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
campaignName: `${campaign.title} - ${new Date().toLocaleDateString('sk-SK')}`,
|
||||
subject: campaign.title,
|
||||
htmlContent: finalHtml
|
||||
})
|
||||
});
|
||||
|
||||
const resData = await response.json();
|
||||
|
||||
if (resData.success && resData.campaignId) {
|
||||
setBrevoUrl(resData.brevoUrl);
|
||||
// Uloženie stavu do Supabase
|
||||
if (campaign.id) {
|
||||
await supabase
|
||||
.from('templates')
|
||||
.update({
|
||||
status: 'exported',
|
||||
brevo_campaign_id: resData.campaignId
|
||||
})
|
||||
.eq('id', campaign.id);
|
||||
}
|
||||
alert('Kampaň bola úspešne odoslaná do Brevo!');
|
||||
} else {
|
||||
alert(`Chyba pri exporte do Brevo: ${resData.error || 'Neznáma chyba'}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
alert(`Chyba: ${err.message}`);
|
||||
} finally {
|
||||
setIsExportingBrevo(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert h-5 w-[100px]"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the{" "}
|
||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
||||
page.tsx
|
||||
</code>{" "}
|
||||
file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<div className="flex flex-col h-screen overflow-hidden bg-slate-900 font-sans">
|
||||
{/* 1. Horná lišta */}
|
||||
<TopBar
|
||||
campaign={campaign}
|
||||
previewMode={previewMode}
|
||||
setPreviewMode={setPreviewMode}
|
||||
isSaving={isSaving}
|
||||
onSave={() => {}}
|
||||
onExportZip={handleExportZip}
|
||||
onExportBrevo={handleExportBrevo}
|
||||
isExportingBrevo={isExportingBrevo}
|
||||
brevoCampaignUrl={brevoUrl}
|
||||
onAddModule={handleAddModule}
|
||||
/>
|
||||
|
||||
{/* 2. Hlavný pracovný priestor (2 zóny: Stredné plátno a Pravý panel) */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Stredné plátno - Živý WYSIWYG náhľad */}
|
||||
<main className="flex-1 overflow-y-auto p-8 flex items-start justify-center bg-slate-900">
|
||||
<div
|
||||
className={`transition-all duration-300 relative shadow-2xl rounded-2xl overflow-hidden ${
|
||||
previewMode === 'mobile' ? 'w-[390px]' : 'w-[640px]'
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
className="dark:invert h-[14px] w-4"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={14}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
{/* Zoznam modulov na plátne */}
|
||||
<div className="bg-[#001a33] min-h-screen p-4">
|
||||
{campaign.modules.map((mod, idx) => {
|
||||
const isSelected = mod.id === selectedModuleId;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={mod.id}
|
||||
onClick={() => setSelectedModuleId(mod.id)}
|
||||
className={`relative group cursor-pointer transition-all duration-200 mb-3 rounded-xl border-2 ${
|
||||
isSelected
|
||||
? 'border-red-500 ring-4 ring-red-500/20'
|
||||
: 'border-transparent hover:border-slate-600'
|
||||
}`}
|
||||
>
|
||||
{/* Lišta s nástrojmi pre označený modul */}
|
||||
{isSelected && (
|
||||
<div className="absolute -top-3 right-4 z-20 flex items-center gap-1 bg-gray-900 text-white px-2 py-1 rounded-lg text-xs shadow-lg border border-slate-700">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
moveModule(idx, 'up');
|
||||
}}
|
||||
disabled={idx === 0}
|
||||
className="hover:text-red-400 disabled:opacity-30 p-0.5"
|
||||
title="Posunúť hore"
|
||||
>
|
||||
<ChevronUp className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
moveModule(idx, 'down');
|
||||
}}
|
||||
disabled={idx === campaign.modules.length - 1}
|
||||
className="hover:text-red-400 disabled:opacity-30 p-0.5"
|
||||
title="Posunúť dole"
|
||||
>
|
||||
<ChevronDown className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
duplicateModule(idx);
|
||||
}}
|
||||
className="hover:text-blue-400 p-0.5 ml-1"
|
||||
title="Duplikovať"
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteModule(idx);
|
||||
}}
|
||||
className="hover:text-red-400 p-0.5 ml-1"
|
||||
title="Zmazať"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vykreslenie modulu cez iframe alebo dangerouslySetInnerHTML */}
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderEmailHtml({
|
||||
...campaign,
|
||||
modules: [mod]
|
||||
})
|
||||
}}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Pravý konfiguračný panel */}
|
||||
<aside className="w-96 flex-shrink-0 h-full bg-white shadow-xl">
|
||||
<ConfigPanel
|
||||
module={selectedModule}
|
||||
onUpdateModule={handleUpdateModule}
|
||||
onOpenBadgeManager={(pId) => {
|
||||
setActiveProductIdForBadge(pId);
|
||||
setIsBadgeModalOpen(true);
|
||||
}}
|
||||
onOpenImport={() => setIsImportOpen(true)}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Modály */}
|
||||
<ProductImportModal
|
||||
isOpen={isImportOpen}
|
||||
onClose={() => setIsImportOpen(false)}
|
||||
onImportProducts={handleImportProducts}
|
||||
/>
|
||||
|
||||
<BadgeManagerModal
|
||||
isOpen={isBadgeModalOpen}
|
||||
onClose={() => setIsBadgeModalOpen(false)}
|
||||
onSelectBadge={(badge) => {
|
||||
// Priradiť cenovku k produktu
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user