Exact 1:1 COOP template design, native email responsiveness without toggle switcher

This commit is contained in:
Jakub_Hostacny committed 2026-09-24 11:11:17 +02:00
1 parent 538f2ccae3
commit 65f37a74b8
5 files changed
+916 -368

No files matched your search

+54 -75
View File
@@ -1,7 +1,7 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter';
import React, { useState, useEffect } from 'react';
import { NewsletterCampaign, NewsletterModule, ProductItem } from '@/types/newsletter';
import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign';
import { renderEmailHtml } from '@/lib/emailRenderer';
import { supabase } from '@/lib/supabase';
@@ -16,7 +16,6 @@ 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);
@@ -24,9 +23,8 @@ export default function Home() {
// 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
// Načítanie existujúceho draftu z databázy
useEffect(() => {
async function loadInitial() {
try {
@@ -38,16 +36,19 @@ export default function Home() {
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}`);
// 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) {
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
});
if (dbItem.brevo_campaign_id) {
setBrevoUrl(`https://app.brevo.com/camp/detail/email/${dbItem.brevo_campaign_id}`);
}
}
}
} catch (err) {
@@ -63,8 +64,7 @@ export default function Home() {
setIsSaving(true);
try {
if (!campaign.id) {
// Vytvorenie nového záznamu
const { data, error } = await supabase
const { data } = await supabase
.from('templates')
.insert([
{
@@ -82,7 +82,6 @@ export default function Home() {
setCampaign((prev) => ({ ...prev, id: data[0].id }));
}
} else {
// Update existujúceho
await supabase
.from('templates')
.update({
@@ -105,10 +104,8 @@ export default function Home() {
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,
@@ -116,7 +113,6 @@ export default function Home() {
}));
};
// Posun modulu hore/dole
const moveModule = (index: number, direction: 'up' | 'down') => {
const newModules = [...campaign.modules];
const targetIndex = direction === 'up' ? index - 1 : index + 1;
@@ -129,7 +125,6 @@ export default function Home() {
setCampaign((prev) => ({ ...prev, modules: newModules }));
};
// Duplikácia modulu
const duplicateModule = (index: number) => {
const original = campaign.modules[index];
const clone: NewsletterModule = {
@@ -144,7 +139,6 @@ export default function Home() {
setSelectedModuleId(clone.id);
};
// Zmazanie modulu
const deleteModule = (index: number) => {
if (campaign.modules.length <= 1) return;
const newModules = campaign.modules.filter((_, i) => i !== index);
@@ -152,7 +146,6 @@ export default function Home() {
setSelectedModuleId(newModules[0]?.id || null);
};
// Pridanie nového modulu
const handleAddModule = (type: any) => {
let newMod: NewsletterModule;
if (type === 'banner') {
@@ -162,12 +155,12 @@ export default function Home() {
title: 'Nový Banner',
data: {
imageUrl: `${SUPABASE_ASSETS_BASE}aktualne_letaky.jpg`,
linkUrl: 'https://www.coop.sk',
linkUrl: '#',
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',
buttonUrl: '#',
buttonBgColor: '#e2001a'
}
};
@@ -179,6 +172,12 @@ export default function Home() {
data: {
sectionTitle: 'AKČNÁ PONUKA',
globalValidity: '2. 5. – 15. 5. 2026',
cardBgColor: '#feeaea',
cardBorderColor: '#f5d4d4',
badgeHeaderBg: '#fefefe',
badgeHeaderColor: '#000000',
badgePriceBg: '#e2001a',
badgePriceColor: '#fefefe',
products: [
{
id: `p-${Date.now()}-1`,
@@ -204,7 +203,6 @@ export default function Home() {
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);
@@ -223,11 +221,8 @@ export default function Home() {
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', {
@@ -238,29 +233,19 @@ export default function Home() {
const data = await res.json();
if (data.html) finalHtml = data.html;
} catch (e) {
console.warn('Inliner fallback to raw html:', e);
console.warn('Inliner fallback:', 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', {
@@ -286,7 +271,6 @@ export default function Home() {
if (resData.success && resData.campaignId) {
setBrevoUrl(resData.brevoUrl);
// Uloženie stavu do Supabase
if (campaign.id) {
await supabase
.from('templates')
@@ -307,13 +291,14 @@ export default function Home() {
}
};
// Kompilácia celého HTML pre responzívny náhľad v reálnom čase
const fullHtml = renderEmailHtml(campaign);
return (
<div className="flex flex-col h-screen overflow-hidden bg-slate-900 font-sans">
<div className="flex flex-col h-screen overflow-hidden bg-[#001a33] font-sans">
{/* 1. Horná lišta */}
<TopBar
campaign={campaign}
previewMode={previewMode}
setPreviewMode={setPreviewMode}
isSaving={isSaving}
onSave={() => {}}
onExportZip={handleExportZip}
@@ -323,17 +308,13 @@ export default function Home() {
onAddModule={handleAddModule}
/>
{/* 2. Hlavný pracovný priestor (2 zóny: Stredné plátno a Pravý panel) */}
{/* 2. Hlavný pracovný priestor */}
<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]'
}`}
>
{/* Zoznam modulov na plátne */}
<div className="bg-[#001a33] min-h-screen p-4">
{/* 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;
@@ -341,25 +322,28 @@ export default function Home() {
<div
key={mod.id}
onClick={() => setSelectedModuleId(mod.id)}
className={`relative group cursor-pointer transition-all duration-200 mb-3 rounded-xl border-2 ${
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-600'
: 'border-transparent hover:border-slate-700/60'
}`}
>
{/* 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">
<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-0.5"
className="hover:text-red-400 disabled:opacity-30 p-1"
title="Posunúť hore"
>
<ChevronUp className="w-3.5 h-3.5" />
<ChevronUp className="w-4 h-4" />
</button>
<button
onClick={(e) => {
@@ -367,35 +351,35 @@ export default function Home() {
moveModule(idx, 'down');
}}
disabled={idx === campaign.modules.length - 1}
className="hover:text-red-400 disabled:opacity-30 p-0.5"
className="hover:text-red-400 disabled:opacity-30 p-1"
title="Posunúť dole"
>
<ChevronDown className="w-3.5 h-3.5" />
<ChevronDown className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
duplicateModule(idx);
}}
className="hover:text-blue-400 p-0.5 ml-1"
className="hover:text-blue-400 p-1"
title="Duplikovať"
>
<Copy className="w-3.5 h-3.5" />
<Copy className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
deleteModule(idx);
}}
className="hover:text-red-400 p-0.5 ml-1"
className="hover:text-red-400 p-1"
title="Zmazať"
>
<Trash2 className="w-3.5 h-3.5" />
<Trash2 className="w-4 h-4" />
</button>
</div>
)}
{/* Vykreslenie modulu cez iframe alebo dangerouslySetInnerHTML */}
{/* Vykreslenie modulu 1:1 */}
<div
dangerouslySetInnerHTML={{
__html: renderEmailHtml({
@@ -413,14 +397,11 @@ export default function Home() {
</main>
{/* Pravý konfiguračný panel */}
<aside className="w-96 flex-shrink-0 h-full bg-white shadow-xl">
<aside className="w-96 flex-shrink-0 h-full bg-white shadow-2xl z-20">
<ConfigPanel
module={selectedModule}
onUpdateModule={handleUpdateModule}
onOpenBadgeManager={(pId) => {
setActiveProductIdForBadge(pId);
setIsBadgeModalOpen(true);
}}
onOpenBadgeManager={() => setIsBadgeModalOpen(true)}
onOpenImport={() => setIsImportOpen(true)}
/>
</aside>
@@ -436,9 +417,7 @@ export default function Home() {
<BadgeManagerModal
isOpen={isBadgeModalOpen}
onClose={() => setIsBadgeModalOpen(false)}
onSelectBadge={(badge) => {
// Priradiť cenovku k produktu
}}
onSelectBadge={() => {}}
/>
</div>
);