Fix canvas background to exact COOP gradient, remove module borders/gaps, add interactive Badge Editor SVG to PNG

This commit is contained in:
Jakub_Hostacny committed 2026-09-24 11:15:54 +02:00
1 parent 65f37a74b8
commit ab74922b93
5 files changed
+537 -99

No files matched your search

+133 -97
View File
@@ -1,17 +1,17 @@
'use client';
import React, { useState, useEffect } from 'react';
import { NewsletterCampaign, NewsletterModule, ProductItem } from '@/types/newsletter';
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 { BadgeEditorModal } from '@/components/editor/BadgeEditorModal';
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
import { ChevronUp, ChevronDown, Copy, Trash2 } from 'lucide-react';
import { ChevronUp, ChevronDown, Copy, Trash2, Edit3 } from 'lucide-react';
export default function Home() {
const [campaign, setCampaign] = useState<NewsletterCampaign>(DEFAULT_CAMPAIGN);
@@ -22,13 +22,14 @@ export default function Home() {
// Modály
const [isImportOpen, setIsImportOpen] = useState(false);
const [isBadgeModalOpen, setIsBadgeModalOpen] = useState(false);
const [isBadgeEditorOpen, setIsBadgeEditorOpen] = useState(false);
const [targetProductIdForBadge, setTargetProductIdForBadge] = useState<string | null>(null);
// Načítanie existujúceho draftu z databázy
useEffect(() => {
async function loadInitial() {
try {
const { data, error } = await supabase
const { data } = await supabase
.from('templates')
.select('*')
.order('updated_at', { ascending: false })
@@ -36,8 +37,7 @@ export default function Home() {
if (data && data.length > 0) {
const dbItem = data[0];
// Ak má databáza rovnakú štruktúru, načítame ju, inak necháme DEFAULT_CAMPAIGN 1:1
if (dbItem.data && dbItem.data.modules && dbItem.data.modules.length >= 6) {
if (dbItem.data && dbItem.data.modules && dbItem.data.modules.length >= 7) {
setCampaign({
id: dbItem.id,
title: dbItem.title,
@@ -291,11 +291,36 @@ export default function Home() {
}
};
// Kompilácia celého HTML pre responzívny náhľad v reálnom čase
const fullHtml = renderEmailHtml(campaign);
// Vybraný produkt pre editor cenoviek
let targetProduct: ProductItem | null = null;
if (targetProductIdForBadge && selectedModule?.data?.products) {
targetProduct = selectedModule.data.products.find((p: any) => p.id === targetProductIdForBadge) || null;
}
const handleApplyBadgeToProduct = (badge: BadgeTemplate, renderedPngUrl?: string) => {
if (!targetProductIdForBadge || !selectedModule) return;
const updatedProducts = (selectedModule.data.products || []).map((p: ProductItem) => {
if (p.id === targetProductIdForBadge) {
return {
...p,
badgeId: badge.id,
customBadgePngUrl: renderedPngUrl || p.customBadgePngUrl
};
}
return p;
});
handleUpdateModule({
...selectedModule,
data: {
...selectedModule.data,
products: updatedProducts
}
});
};
return (
<div className="flex flex-col h-screen overflow-hidden bg-[#001a33] font-sans">
<div className="flex flex-col h-screen overflow-hidden bg-slate-950 font-sans">
{/* 1. Horná lišta */}
<TopBar
campaign={campaign}
@@ -310,89 +335,93 @@ export default function Home() {
{/* 2. Hlavný pracovný priestor */}
<div className="flex flex-1 overflow-hidden">
{/* Stredné plátno - Živý responzívny e-mail v iframe pre 100% izoláciu CSS a médií */}
<main className="flex-1 overflow-y-auto p-4 md:p-8 flex items-start justify-center bg-[#000d1a]">
<div className="w-full max-w-[680px] shadow-2xl rounded-2xl overflow-hidden border border-blue-950/50 bg-[#001a33]">
{/* Zoznam modulov pre priamu editáciu */}
<div className="p-3 bg-[#001a33] space-y-3">
{campaign.modules.map((mod, idx) => {
const isSelected = mod.id === selectedModuleId;
{/* Stredné plátno - Presný originálny gradient a čisté rozloženie */}
<main
className="flex-1 overflow-y-auto p-4 md:p-8 flex items-start justify-center"
style={{
backgroundColor: '#001a33',
backgroundImage: 'linear-gradient(135deg, #005bb5 0%, #000d1a 100%)'
}}
>
{/* E-mail Container 1:1 k originálu */}
<div className="w-full max-w-[600px] my-4">
{campaign.modules.map((mod, idx) => {
const isSelected = mod.id === selectedModuleId;
return (
return (
<div
key={mod.id}
onClick={() => setSelectedModuleId(mod.id)}
className={`relative cursor-pointer transition-all duration-150 ${
isSelected
? 'outline-2 outline-offset-4 outline-red-500 rounded-2xl shadow-2xl'
: 'hover:outline-1 hover:outline-offset-2 hover:outline-white/30 rounded-2xl'
}`}
>
{/* Lišta s nástrojmi pre označený modul */}
{isSelected && (
<div className="absolute -top-3 right-4 z-40 flex items-center gap-1 bg-[#001a33] text-white px-3 py-1.5 rounded-full text-xs shadow-2xl border border-white/20 backdrop-blur-md">
<span className="text-[10px] font-bold text-gray-300 mr-1.5 uppercase tracking-wider">
{mod.title}
</span>
<button
onClick={(e) => {
e.stopPropagation();
moveModule(idx, 'up');
}}
disabled={idx === 0}
className="hover:text-red-400 disabled:opacity-30 p-1"
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-1"
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-1 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-1 ml-1"
title="Zmazať"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
)}
{/* Vykreslenie modulu bez vnútorných medzier navyše */}
<div
key={mod.id}
onClick={() => setSelectedModuleId(mod.id)}
className={`relative group cursor-pointer transition-all duration-200 rounded-2xl border-2 ${
isSelected
? 'border-red-500 ring-4 ring-red-500/20'
: 'border-transparent hover:border-slate-700/60'
}`}
>
{/* Lišta s nástrojmi pre označený modul */}
{isSelected && (
<div className="absolute top-2 right-2 z-30 flex items-center gap-1.5 bg-slate-900/95 backdrop-blur-md text-white px-2.5 py-1.5 rounded-xl text-xs shadow-2xl border border-slate-700">
<span className="text-[10px] font-bold text-gray-400 mr-1 uppercase">
{mod.title}
</span>
<button
onClick={(e) => {
e.stopPropagation();
moveModule(idx, 'up');
}}
disabled={idx === 0}
className="hover:text-red-400 disabled:opacity-30 p-1"
title="Posunúť hore"
>
<ChevronUp className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
moveModule(idx, 'down');
}}
disabled={idx === campaign.modules.length - 1}
className="hover:text-red-400 disabled:opacity-30 p-1"
title="Posunúť dole"
>
<ChevronDown className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
duplicateModule(idx);
}}
className="hover:text-blue-400 p-1"
title="Duplikovať"
>
<Copy className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
deleteModule(idx);
}}
className="hover:text-red-400 p-1"
title="Zmazať"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
)}
{/* Vykreslenie modulu 1:1 */}
<div
dangerouslySetInnerHTML={{
__html: renderEmailHtml({
...campaign,
modules: [mod]
})
}}
className="pointer-events-none"
/>
</div>
);
})}
</div>
dangerouslySetInnerHTML={{
__html: renderEmailHtml({
...campaign,
modules: [mod]
})
}}
className="pointer-events-none"
/>
</div>
);
})}
</div>
</main>
@@ -401,23 +430,30 @@ export default function Home() {
<ConfigPanel
module={selectedModule}
onUpdateModule={handleUpdateModule}
onOpenBadgeManager={() => setIsBadgeModalOpen(true)}
onOpenBadgeManager={(pId) => {
setTargetProductIdForBadge(pId);
setIsBadgeEditorOpen(true);
}}
onOpenImport={() => setIsImportOpen(true)}
/>
</aside>
</div>
{/* Modály */}
{/* Modál Hromadného importu */}
<ProductImportModal
isOpen={isImportOpen}
onClose={() => setIsImportOpen(false)}
onImportProducts={handleImportProducts}
/>
<BadgeManagerModal
isOpen={isBadgeModalOpen}
onClose={() => setIsBadgeModalOpen(false)}
onSelectBadge={() => {}}
{/* Modál Editora cenoviek (SVG -> PNG) */}
<BadgeEditorModal
isOpen={isBadgeEditorOpen}
onClose={() => setIsBadgeEditorOpen(false)}
onSelectBadge={handleApplyBadgeToProduct}
currentDiscount={targetProduct?.discountText || '25%'}
currentPriceMain={targetProduct?.priceMain || '1'}
currentPriceSub={targetProduct?.priceSub || '99'}
/>
</div>
);