feat: anchor header/footer, editable header buttons, campaign management, image uploads, badge font customization, and brevo export fix
This commit is contained in:
1 parent
3dc5778155
commit
2b4d3643cb
10 files changed
+1819
-178
No files matched your search
@@ -12,7 +12,7 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const apiKey = process.env.BREVO_API_KEY;
|
||||
const senderName = process.env.BREVO_SENDER_NAME || 'Jandl';
|
||||
const senderName = process.env.BREVO_SENDER_NAME || 'Jakub Hostačný (COOP Newsletter)';
|
||||
const senderEmail = process.env.BREVO_SENDER_EMAIL || 'tomas.rafay@jandl.sk';
|
||||
|
||||
if (!apiKey) {
|
||||
@@ -58,11 +58,12 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Úspech - vrátime campaignId a URL na správu v Brevo
|
||||
// Úspech - vrátime campaignId a priame URL na editáciu a prehľad kampaní v novom Brevo dashboarde
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
campaignId: data.id,
|
||||
brevoUrl: `https://app.brevo.com/camp/detail/email/${data.id}`,
|
||||
brevoUrl: `https://app.brevo.com/campaigns/email/edit/${data.id}`,
|
||||
brevoListingUrl: 'https://app.brevo.com/campaigns/listing',
|
||||
message: 'Kampaň bola úspešne vytvorená v Brevo!'
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getSupabaseAdmin } from '@/lib/supabase';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const formData = await req.formData();
|
||||
const file = formData.get('file') as File | null;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'Súbor nebol poskytnutý.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const admin = getSupabaseAdmin();
|
||||
const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const path = `uploads/${Date.now()}-${safeName}`;
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const { error: uploadError } = await admin.storage
|
||||
.from('newsletter-assets')
|
||||
.upload(path, buffer, {
|
||||
contentType: file.type || 'image/png',
|
||||
upsert: true
|
||||
});
|
||||
|
||||
if (uploadError) {
|
||||
console.error('Upload to Supabase Storage error:', uploadError);
|
||||
return NextResponse.json({ error: uploadError.message }, { status: 500 });
|
||||
}
|
||||
|
||||
const { data: pubData } = admin.storage
|
||||
.from('newsletter-assets')
|
||||
.getPublicUrl(path);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
url: pubData.publicUrl
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('API /upload error:', err);
|
||||
return NextResponse.json({ error: err.message || 'Chyba servera pri nahrávaní súboru' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
+284
-70
@@ -9,9 +9,10 @@ import { TopBar } from '@/components/editor/TopBar';
|
||||
import { ConfigPanel } from '@/components/editor/ConfigPanel';
|
||||
import { ProductImportModal } from '@/components/editor/ProductImportModal';
|
||||
import { BadgeEditorModal } from '@/components/editor/BadgeEditorModal';
|
||||
import { SavedCampaignsModal } from '@/components/editor/SavedCampaignsModal';
|
||||
import JSZip from 'jszip';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { ChevronUp, ChevronDown, Copy, Trash2, Edit3 } from 'lucide-react';
|
||||
import { ChevronUp, ChevronDown, Copy, Trash2, Anchor, Lock } from 'lucide-react';
|
||||
|
||||
export default function Home() {
|
||||
const [campaign, setCampaign] = useState<NewsletterCampaign>(DEFAULT_CAMPAIGN);
|
||||
@@ -23,9 +24,30 @@ export default function Home() {
|
||||
// Modály
|
||||
const [isImportOpen, setIsImportOpen] = useState(false);
|
||||
const [isBadgeEditorOpen, setIsBadgeEditorOpen] = useState(false);
|
||||
const [isSavedCampaignsOpen, setIsSavedCampaignsOpen] = useState(false);
|
||||
const [targetProductIdForBadge, setTargetProductIdForBadge] = useState<string | null>(null);
|
||||
|
||||
// Načítanie existujúceho draftu z databázy
|
||||
// Pomocné funkcie pre fixné ukotvenie hlavičky a pätičky
|
||||
const isTopModule = (type: string) => type === 'header' || type === 'announcement';
|
||||
const isBottomModule = (type: string) => type === 'footer';
|
||||
const isAnchoredModule = (type: string) => isTopModule(type) || isBottomModule(type);
|
||||
|
||||
const enforceAnchors = (mods: NewsletterModule[]): NewsletterModule[] => {
|
||||
const topMods = mods.filter((m) => isTopModule(m.type));
|
||||
const bottomMods = mods.filter((m) => isBottomModule(m.type));
|
||||
const middleMods = mods.filter((m) => !isAnchoredModule(m.type));
|
||||
|
||||
// announcement je pred headerom, ak existujú oba
|
||||
topMods.sort((a, b) => {
|
||||
if (a.type === 'announcement' && b.type === 'header') return -1;
|
||||
if (a.type === 'header' && b.type === 'announcement') return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return [...topMods, ...middleMods, ...bottomMods];
|
||||
};
|
||||
|
||||
// Načítanie existujúceho draftu z databázy pri štarte
|
||||
useEffect(() => {
|
||||
async function loadInitial() {
|
||||
try {
|
||||
@@ -44,10 +66,10 @@ export default function Home() {
|
||||
preheader: dbItem.data.preheader || DEFAULT_CAMPAIGN.preheader,
|
||||
status: dbItem.status,
|
||||
brevoCampaignId: dbItem.brevo_campaign_id,
|
||||
modules: dbItem.data.modules
|
||||
modules: enforceAnchors(dbItem.data.modules)
|
||||
});
|
||||
if (dbItem.brevo_campaign_id) {
|
||||
setBrevoUrl(`https://app.brevo.com/camp/detail/email/${dbItem.brevo_campaign_id}`);
|
||||
setBrevoUrl(`https://app.brevo.com/campaigns/email/edit/${dbItem.brevo_campaign_id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +124,7 @@ export default function Home() {
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [campaign.title, campaign.preheader, campaign.modules]);
|
||||
}, [campaign.title, campaign.preheader, campaign.modules, campaign.id]);
|
||||
|
||||
const selectedModule = campaign.modules.find((m) => m.id === selectedModuleId) || null;
|
||||
|
||||
@@ -113,39 +135,56 @@ export default function Home() {
|
||||
}));
|
||||
};
|
||||
|
||||
// Posun modulu: Hlavička a pätička sú ukotvené a bežné moduly sa nemôžu dostať pred hlavičku ani za pätičku
|
||||
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 currentMod = campaign.modules[index];
|
||||
if (!currentMod || isAnchoredModule(currentMod.type)) return;
|
||||
|
||||
const topCount = campaign.modules.filter((m) => isTopModule(m.type)).length;
|
||||
const footerIndex = campaign.modules.findIndex((m) => isBottomModule(m.type));
|
||||
const maxAllowedIndex = footerIndex !== -1 ? footerIndex - 1 : campaign.modules.length - 1;
|
||||
|
||||
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
if (targetIndex < topCount || targetIndex > maxAllowedIndex) return;
|
||||
|
||||
const newModules = [...campaign.modules];
|
||||
const temp = newModules[index];
|
||||
newModules[index] = newModules[targetIndex];
|
||||
newModules[targetIndex] = temp;
|
||||
|
||||
setCampaign((prev) => ({ ...prev, modules: newModules }));
|
||||
setCampaign((prev) => ({ ...prev, modules: enforceAnchors(newModules) }));
|
||||
};
|
||||
|
||||
// Duplikácia: Hlavička a pätička sa nedajú duplikovať
|
||||
const duplicateModule = (index: number) => {
|
||||
const original = campaign.modules[index];
|
||||
if (isAnchoredModule(original.type)) return;
|
||||
|
||||
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 }));
|
||||
setCampaign((prev) => ({ ...prev, modules: enforceAnchors(newModules) }));
|
||||
setSelectedModuleId(clone.id);
|
||||
};
|
||||
|
||||
// Zmazanie: Hlavička a pätička sa nedajú vymazať
|
||||
const deleteModule = (index: number) => {
|
||||
const mod = campaign.modules[index];
|
||||
if (isAnchoredModule(mod.type)) return;
|
||||
if (campaign.modules.length <= 1) return;
|
||||
|
||||
const newModules = campaign.modules.filter((_, i) => i !== index);
|
||||
setCampaign((prev) => ({ ...prev, modules: newModules }));
|
||||
setCampaign((prev) => ({ ...prev, modules: enforceAnchors(newModules) }));
|
||||
setSelectedModuleId(newModules[0]?.id || null);
|
||||
};
|
||||
|
||||
// Pridanie modulu: Vloží sa vždy PRED pätičku (pätička ostáva pevne na spodku)
|
||||
const handleAddModule = (type: any) => {
|
||||
let newMod: NewsletterModule;
|
||||
if (type === 'banner') {
|
||||
@@ -196,10 +235,16 @@ export default function Home() {
|
||||
};
|
||||
}
|
||||
|
||||
setCampaign((prev) => ({
|
||||
...prev,
|
||||
modules: [...prev.modules, newMod]
|
||||
}));
|
||||
setCampaign((prev) => {
|
||||
const footerIdx = prev.modules.findIndex((m) => isBottomModule(m.type));
|
||||
const newModules = [...prev.modules];
|
||||
if (footerIdx !== -1) {
|
||||
newModules.splice(footerIdx, 0, newMod);
|
||||
} else {
|
||||
newModules.push(newMod);
|
||||
}
|
||||
return { ...prev, modules: enforceAnchors(newModules) };
|
||||
});
|
||||
setSelectedModuleId(newMod.id);
|
||||
};
|
||||
|
||||
@@ -221,6 +266,89 @@ export default function Home() {
|
||||
handleUpdateModule(updated);
|
||||
};
|
||||
|
||||
// Manuálne uloženie kampane
|
||||
const handleManualSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
if (!campaign.id) {
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.insert([
|
||||
{
|
||||
title: campaign.title,
|
||||
status: campaign.status,
|
||||
data: {
|
||||
preheader: campaign.preheader,
|
||||
modules: campaign.modules
|
||||
}
|
||||
}
|
||||
])
|
||||
.select();
|
||||
|
||||
if (error) throw error;
|
||||
if (data && data.length > 0) {
|
||||
setCampaign((prev) => ({ ...prev, id: data[0].id }));
|
||||
}
|
||||
} else {
|
||||
const { error } = await supabase
|
||||
.from('templates')
|
||||
.update({
|
||||
title: campaign.title,
|
||||
data: {
|
||||
preheader: campaign.preheader,
|
||||
modules: campaign.modules
|
||||
},
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', campaign.id);
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
alert('Kampaň bola úspešne uložená do databázy!');
|
||||
} catch (err: any) {
|
||||
alert(`Chyba pri ukladaní: ${err.message}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Duplikácia aktuálnej kampane v Supabase
|
||||
const handleDuplicateCurrentCampaign = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const newTitle = `${campaign.title} (kópia)`;
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.insert([
|
||||
{
|
||||
title: newTitle,
|
||||
status: 'draft',
|
||||
data: {
|
||||
preheader: campaign.preheader,
|
||||
modules: campaign.modules
|
||||
}
|
||||
}
|
||||
])
|
||||
.select();
|
||||
|
||||
if (error) throw error;
|
||||
if (data && data.length > 0) {
|
||||
setCampaign({
|
||||
id: data[0].id,
|
||||
title: newTitle,
|
||||
preheader: campaign.preheader,
|
||||
status: 'draft',
|
||||
modules: campaign.modules
|
||||
});
|
||||
alert(`Kampaň bola zduplikovaná ako "${newTitle}"!`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
alert(`Chyba pri duplikovaní: ${err.message}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportZip = async () => {
|
||||
const rawHtml = renderEmailHtml(campaign);
|
||||
let finalHtml = rawHtml;
|
||||
@@ -319,17 +447,24 @@ export default function Home() {
|
||||
});
|
||||
};
|
||||
|
||||
const topCount = campaign.modules.filter((m) => isTopModule(m.type)).length;
|
||||
const footerIdx = campaign.modules.findIndex((m) => isBottomModule(m.type));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen overflow-hidden bg-slate-950 font-sans">
|
||||
{/* 1. Horná lišta */}
|
||||
<TopBar
|
||||
campaign={campaign}
|
||||
isSaving={isSaving}
|
||||
onSave={() => {}}
|
||||
onSave={handleManualSave}
|
||||
onUpdateTitle={(title) => setCampaign((prev) => ({ ...prev, title }))}
|
||||
onOpenSavedCampaigns={() => setIsSavedCampaignsOpen(true)}
|
||||
onDuplicateCampaign={handleDuplicateCurrentCampaign}
|
||||
onExportZip={handleExportZip}
|
||||
onExportBrevo={handleExportBrevo}
|
||||
isExportingBrevo={isExportingBrevo}
|
||||
brevoCampaignUrl={brevoUrl}
|
||||
brevoListingUrl="https://app.brevo.com/campaigns/listing"
|
||||
onAddModule={handleAddModule}
|
||||
/>
|
||||
|
||||
@@ -347,75 +482,112 @@ export default function Home() {
|
||||
<div className="w-full max-w-[600px] my-4">
|
||||
{campaign.modules.map((mod, idx) => {
|
||||
const isSelected = mod.id === selectedModuleId;
|
||||
const isAnchored = isAnchoredModule(mod.type);
|
||||
const isTop = isTopModule(mod.type);
|
||||
const isBottom = isBottomModule(mod.type);
|
||||
|
||||
const canMoveUp = !isAnchored && idx > topCount;
|
||||
const canMoveDown = !isAnchored && idx < (footerIdx !== -1 ? footerIdx - 1 : campaign.modules.length - 1);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={mod.id}
|
||||
onClick={() => setSelectedModuleId(mod.id)}
|
||||
className={`relative cursor-pointer transition-all duration-150 ${
|
||||
className={`relative cursor-pointer transition-all duration-150 rounded-2xl ${
|
||||
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'
|
||||
? 'ring-2 ring-offset-4 ring-offset-[#001a33] ring-red-500 shadow-2xl'
|
||||
: 'hover:ring-1 hover:ring-offset-2 hover:ring-offset-[#001a33] hover:ring-white/30'
|
||||
}`}
|
||||
>
|
||||
{/* 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">
|
||||
<div className="absolute -top-3 right-4 z-40 flex items-center gap-1.5 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 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>
|
||||
|
||||
{/* Ak je modul ukotvený (Hlavička / Pätička), nezobrazujeme tlačidlá na presun a zmazanie */}
|
||||
{isAnchored ? (
|
||||
<span className="text-[9px] font-bold bg-white/10 text-amber-300 px-2 py-0.5 rounded-full border border-white/10 uppercase flex items-center gap-1">
|
||||
<Lock className="w-2.5 h-2.5" />
|
||||
{isBottom ? 'Ukotvená pätička' : 'Ukotvená hlavička'}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
moveModule(idx, 'up');
|
||||
}}
|
||||
disabled={!canMoveUp}
|
||||
className="hover:text-red-400 disabled:opacity-30 p-1"
|
||||
title={canMoveUp ? 'Posunúť vyššie' : 'Nemožno posunúť nad hlavičku'}
|
||||
>
|
||||
<ChevronUp className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
moveModule(idx, 'down');
|
||||
}}
|
||||
disabled={!canMoveDown}
|
||||
className="hover:text-red-400 disabled:opacity-30 p-1"
|
||||
title={canMoveDown ? 'Posunúť nižšie' : 'Nemožno posunúť pod pätičku'}
|
||||
>
|
||||
<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ť sekciu"
|
||||
>
|
||||
<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ť sekciu"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vykreslenie čistého modulu bez obaľovacieho gradientu na každom bloku */}
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderSingleModuleHtml(mod, campaign)
|
||||
}}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
{/* Vykreslenie modulu: ak je loyalty_club vypnutý, zobrazíme prívetivý placeholder */}
|
||||
{mod.type === 'loyalty_club' && mod.data.enabled === false ? (
|
||||
<div className="p-8 my-4 border-2 border-dashed border-red-500/40 bg-red-950/30 rounded-2xl text-center">
|
||||
<p className="text-xs font-bold text-red-300 uppercase tracking-wider">
|
||||
Sekcia Vernostné zľavy (Vypnutá)
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-300 mt-1">
|
||||
Táto sekcia je v odosielanom e-maile skrytá. Kliknutím sem ju môžete aktivovať.
|
||||
</p>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUpdateModule({ ...mod, data: { ...mod.data, enabled: true } });
|
||||
}}
|
||||
className="mt-3 px-4 py-1.5 bg-red-600 hover:bg-red-700 text-white text-xs font-bold rounded-lg transition-colors shadow-sm"
|
||||
>
|
||||
Aktivovať vernostné zľavy
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderSingleModuleHtml(mod, campaign)
|
||||
}}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -453,6 +625,48 @@ export default function Home() {
|
||||
currentPriceSub={targetProduct?.priceSub || '99'}
|
||||
currentUnitPrice={targetProduct?.unitPrice || '1kg = 4,91 €'}
|
||||
/>
|
||||
|
||||
{/* Modál Moje newslettre (Ukladanie, duplikácia, vytváranie a prepínanie) */}
|
||||
<SavedCampaignsModal
|
||||
isOpen={isSavedCampaignsOpen}
|
||||
onClose={() => setIsSavedCampaignsOpen(false)}
|
||||
currentCampaign={campaign}
|
||||
onLoadCampaign={(selected: NewsletterCampaign) => {
|
||||
setCampaign({ ...selected, modules: enforceAnchors(selected.modules) });
|
||||
setSelectedModuleId(selected.modules[1]?.id || selected.modules[0]?.id || null);
|
||||
if (selected.brevoCampaignId) {
|
||||
setBrevoUrl(`https://app.brevo.com/campaigns/email/edit/${selected.brevoCampaignId}`);
|
||||
} else {
|
||||
setBrevoUrl(null);
|
||||
}
|
||||
}}
|
||||
onSaveCurrentAsCopy={async (newTitle: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.insert([
|
||||
{
|
||||
title: newTitle,
|
||||
status: 'draft',
|
||||
data: {
|
||||
preheader: campaign.preheader,
|
||||
modules: campaign.modules
|
||||
}
|
||||
}
|
||||
])
|
||||
.select();
|
||||
|
||||
if (error) throw error;
|
||||
if (data && data[0]) {
|
||||
setCampaign({
|
||||
id: data[0].id,
|
||||
title: newTitle,
|
||||
preheader: campaign.preheader,
|
||||
status: 'draft',
|
||||
modules: campaign.modules
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,46 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
currentPriceSub = '99',
|
||||
currentUnitPrice = '1kg = 4,91 €'
|
||||
}) => {
|
||||
const POPULAR_FONTS = [
|
||||
{ name: 'Poppins', source: 'google' as const },
|
||||
{ name: 'Montserrat', source: 'google' as const },
|
||||
{ name: 'Oswald', source: 'google' as const },
|
||||
{ name: 'Bebas Neue', source: 'google' as const },
|
||||
{ name: 'Anton', source: 'google' as const },
|
||||
{ name: 'Roboto Condensed', source: 'google' as const },
|
||||
{ name: 'Barlow Semi Condensed', source: 'google' as const },
|
||||
{ name: 'Inter', source: 'google' as const },
|
||||
{ name: 'Open Sans', source: 'google' as const },
|
||||
{ name: 'Arial Narrow', source: 'system' as const },
|
||||
{ name: 'Impact', source: 'system' as const }
|
||||
];
|
||||
|
||||
const loadFontDynamically = (fontFamily: string, source: 'system' | 'google' | 'adobe' = 'google', url?: string) => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
if (source === 'adobe' && url) {
|
||||
const existing = document.querySelector(`link[data-adobe-font="${url}"]`);
|
||||
if (!existing) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
link.setAttribute('data-adobe-font', url);
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
} else if (source === 'google' && fontFamily) {
|
||||
const clean = fontFamily.trim().replace(/\s+/g, '+');
|
||||
const href = `https://fonts.googleapis.com/css2?family=${clean}:wght@400;600;700;800;900&display=swap`;
|
||||
const existing = document.querySelector(`link[data-google-font="${clean}"]`);
|
||||
if (!existing) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = href;
|
||||
link.setAttribute('data-google-font', clean);
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [badges, setBadges] = useState<BadgeTemplate[]>([]);
|
||||
const [activeBadge, setActiveBadge] = useState<BadgeTemplate | null>(null);
|
||||
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
||||
@@ -32,6 +72,17 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
const [isExportingPng, setIsExportingPng] = useState(false);
|
||||
const [isSavingDb, setIsSavingDb] = useState(false);
|
||||
|
||||
// Načítanie písiem pre všetky vrstvy aktívnej cenovky
|
||||
useEffect(() => {
|
||||
if (activeBadge?.layersConfig) {
|
||||
activeBadge.layersConfig.forEach((l) => {
|
||||
if (l.fontFamily) {
|
||||
loadFontDynamically(l.fontFamily, l.fontSource || 'google', l.fontUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [activeBadge]);
|
||||
|
||||
// Načítanie existujúcich šablón zo Supabase
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -366,9 +417,11 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
top: `${layer.y}px`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
fontSize: `${layer.fontSize}px`,
|
||||
fontWeight: layer.fontWeight as any,
|
||||
fontWeight: (layer.fontWeight || '700') as any,
|
||||
color: layer.fill,
|
||||
fontFamily: 'Poppins, Arial, sans-serif',
|
||||
fontFamily: layer.fontFamily
|
||||
? `'${layer.fontFamily}', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`
|
||||
: 'Poppins, Arial, sans-serif',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
@@ -530,6 +583,181 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Font nastavenie */}
|
||||
<div className="space-y-2 pt-2 border-t border-gray-200">
|
||||
<label className="block text-[11px] font-semibold text-gray-700">
|
||||
Písmo (Font) a Hrúbka
|
||||
</label>
|
||||
|
||||
{/* Zdroj písma */}
|
||||
<div className="grid grid-cols-3 gap-1 bg-gray-200 p-0.5 rounded-lg text-[10px] font-bold text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateActiveLayer({ fontSource: 'google' });
|
||||
}}
|
||||
className={`py-1 rounded ${
|
||||
(activeLayer.fontSource || 'google') === 'google'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Google
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateActiveLayer({ fontSource: 'adobe' });
|
||||
}}
|
||||
className={`py-1 rounded ${
|
||||
activeLayer.fontSource === 'adobe'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Adobe
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateActiveLayer({ fontSource: 'system' });
|
||||
}}
|
||||
className={`py-1 rounded ${
|
||||
activeLayer.fontSource === 'system'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Systém
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Google Fonts voľba */}
|
||||
{(activeLayer.fontSource || 'google') === 'google' && (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="block text-[10px] text-gray-500 mb-0.5">Výber Google Fontu</label>
|
||||
<select
|
||||
value={
|
||||
POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins'))
|
||||
? activeLayer.fontFamily || 'Poppins'
|
||||
: 'custom'
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (e.target.value !== 'custom') {
|
||||
loadFontDynamically(e.target.value, 'google');
|
||||
updateActiveLayer({ fontFamily: e.target.value, fontSource: 'google' });
|
||||
}
|
||||
}}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
|
||||
>
|
||||
{POPULAR_FONTS.filter((f) => f.source === 'google').map((f) => (
|
||||
<option key={f.name} value={f.name}>
|
||||
{f.name}
|
||||
</option>
|
||||
))}
|
||||
<option value="custom">Iný Google Font (zadať vlastný)...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{(!POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins')) ||
|
||||
activeLayer.fontFamily === 'custom') && (
|
||||
<div>
|
||||
<label className="block text-[10px] text-gray-500 mb-0.5">Názov Google Fontu</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="napr. Montserrat, Oswald, Syne"
|
||||
value={activeLayer.fontFamily === 'custom' ? '' : activeLayer.fontFamily || ''}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
loadFontDynamically(val, 'google');
|
||||
updateActiveLayer({ fontFamily: val, fontSource: 'google' });
|
||||
}}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Adobe Fonts voľba */}
|
||||
{activeLayer.fontSource === 'adobe' && (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="block text-[10px] text-gray-500 mb-0.5">
|
||||
Adobe Typekit CSS URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://use.typekit.net/xxxx.css"
|
||||
value={activeLayer.fontUrl || ''}
|
||||
onChange={(e) => {
|
||||
const url = e.target.value;
|
||||
if (activeLayer.fontFamily) {
|
||||
loadFontDynamically(activeLayer.fontFamily, 'adobe', url);
|
||||
}
|
||||
updateActiveLayer({ fontUrl: url, fontSource: 'adobe' });
|
||||
}}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-mono text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] text-gray-500 mb-0.5">
|
||||
Názov rodiny písma (Font Family)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="napr. futura-pt, omnes-pro"
|
||||
value={activeLayer.fontFamily || ''}
|
||||
onChange={(e) => {
|
||||
const family = e.target.value;
|
||||
if (activeLayer.fontUrl) {
|
||||
loadFontDynamically(family, 'adobe', activeLayer.fontUrl);
|
||||
}
|
||||
updateActiveLayer({ fontFamily: family, fontSource: 'adobe' });
|
||||
}}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Systémové písma */}
|
||||
{activeLayer.fontSource === 'system' && (
|
||||
<div>
|
||||
<label className="block text-[10px] text-gray-500 mb-0.5">Systémový font</label>
|
||||
<select
|
||||
value={activeLayer.fontFamily || 'Arial'}
|
||||
onChange={(e) => updateActiveLayer({ fontFamily: e.target.value, fontSource: 'system' })}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
|
||||
>
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Arial Narrow">Arial Narrow</option>
|
||||
<option value="Impact">Impact</option>
|
||||
<option value="Trebuchet MS">Trebuchet MS</option>
|
||||
<option value="Helvetica">Helvetica</option>
|
||||
<option value="Verdana">Verdana</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hrúbka fontu */}
|
||||
<div>
|
||||
<label className="block text-[10px] text-gray-500 mb-0.5">Hrúbka písma (Weight)</label>
|
||||
<select
|
||||
value={activeLayer.fontWeight || '700'}
|
||||
onChange={(e) => updateActiveLayer({ fontWeight: e.target.value })}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
|
||||
>
|
||||
<option value="400">400 - Normal</option>
|
||||
<option value="600">600 - SemiBold</option>
|
||||
<option value="700">700 - Bold</option>
|
||||
<option value="800">800 - ExtraBold</option>
|
||||
<option value="900">900 - Black</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-[10px] text-gray-500 mb-0.5">Pozícia X ({activeLayer.x}px)</label>
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
import { ImageUploadInput } from './ImageUploadInput';
|
||||
|
||||
interface ConfigPanelProps {
|
||||
module: NewsletterModule | null;
|
||||
@@ -85,41 +86,136 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
)}
|
||||
|
||||
{/* 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"
|
||||
{module.type === 'header' && (() => {
|
||||
const navLinks = module.data.navLinks || [
|
||||
{ label: 'Letáky', url: 'https://www.coop.sk/sk/letaky', variant: 'outline' },
|
||||
{ label: 'COOP Klub', url: 'https://www.coopklub.sk', variant: 'filled' }
|
||||
];
|
||||
|
||||
const updateNavLink = (index: number, patch: Partial<typeof navLinks[0]>) => {
|
||||
const updated = [...navLinks];
|
||||
updated[index] = { ...updated[index], ...patch };
|
||||
updateData({ navLinks: updated });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<ImageUploadInput
|
||||
label="Logo COOP Jednota"
|
||||
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"
|
||||
onChange={(url) => updateData({ logoUrl: url })}
|
||||
/>
|
||||
<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"
|
||||
placeholder="https://www.coop.sk"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tlačidlá v hlavičke */}
|
||||
<div className="pt-2 border-t border-gray-200 space-y-4">
|
||||
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block">
|
||||
Tlačidlá v hlavičke
|
||||
</span>
|
||||
|
||||
{/* Tlačidlo 1 (Letáky) */}
|
||||
<div className="bg-gray-50 p-3 rounded-xl border border-gray-200 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-gray-700">Tlačidlo 1 (Letáky)</span>
|
||||
<span className="text-[10px] bg-red-100 text-red-700 font-bold px-2 py-0.5 rounded">
|
||||
{navLinks[0]?.variant === 'filled' ? 'Plné' : 'Obrys'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] text-gray-500 mb-1">Text tlačidla</label>
|
||||
<input
|
||||
type="text"
|
||||
value={navLinks[0]?.label || ''}
|
||||
onChange={(e) => updateNavLink(0, { label: e.target.value })}
|
||||
className="w-full text-xs font-bold border border-gray-300 rounded-lg p-2 bg-white"
|
||||
placeholder="Letáky"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] text-gray-500 mb-1">Cieľový odkaz (URL)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={navLinks[0]?.url || ''}
|
||||
onChange={(e) => updateNavLink(0, { url: e.target.value })}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-2 bg-white"
|
||||
placeholder="https://www.coop.sk/sk/letaky"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] text-gray-500 mb-1">Štýl zobrazenia</label>
|
||||
<select
|
||||
value={navLinks[0]?.variant || 'outline'}
|
||||
onChange={(e) => updateNavLink(0, { variant: e.target.value as any })}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-2 bg-white font-medium"
|
||||
>
|
||||
<option value="outline">Červený obrys (Biele pozadie, červený text)</option>
|
||||
<option value="filled">Modré plné tlačidlo (Biele písmo)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tlačidlo 2 (COOP Klub) */}
|
||||
<div className="bg-gray-50 p-3 rounded-xl border border-gray-200 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-gray-700">Tlačidlo 2 (COOP Klub)</span>
|
||||
<span className="text-[10px] bg-blue-100 text-blue-700 font-bold px-2 py-0.5 rounded">
|
||||
{navLinks[1]?.variant === 'filled' ? 'Plné modré' : 'Obrys'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] text-gray-500 mb-1">Text tlačidla</label>
|
||||
<input
|
||||
type="text"
|
||||
value={navLinks[1]?.label || ''}
|
||||
onChange={(e) => updateNavLink(1, { label: e.target.value })}
|
||||
className="w-full text-xs font-bold border border-gray-300 rounded-lg p-2 bg-white"
|
||||
placeholder="COOP Klub"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] text-gray-500 mb-1">Cieľový odkaz (URL)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={navLinks[1]?.url || ''}
|
||||
onChange={(e) => updateNavLink(1, { url: e.target.value })}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-2 bg-white"
|
||||
placeholder="https://www.coopklub.sk"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] text-gray-500 mb-1">Štýl zobrazenia</label>
|
||||
<select
|
||||
value={navLinks[1]?.variant || 'filled'}
|
||||
onChange={(e) => updateNavLink(1, { variant: e.target.value as any })}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-2 bg-white font-medium"
|
||||
>
|
||||
<option value="filled">Modré plné tlačidlo (Biele písmo)</option>
|
||||
<option value="outline">Červený obrys (Biele pozadie, červený text)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<ImageUploadInput
|
||||
label="Obrázok bannera"
|
||||
value={module.data.imageUrl || ''}
|
||||
onChange={(url) => updateData({ imageUrl: url })}
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Odkaz bannera</label>
|
||||
<input
|
||||
@@ -269,6 +365,32 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ImageUploadInput
|
||||
label="Obrázok produktu"
|
||||
value={p.imageUrl || ''}
|
||||
onChange={(url) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].imageUrl = url;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="https://...produkt.png alebo nahrajte súbor"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Platnosť akcie pre tento produkt (override)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.validityOverride || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].validityOverride = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder={`Predvolená: ${module.data.globalValidity || '2. 5. – 15. 5. 2026'}`}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500">Cena (€)</label>
|
||||
@@ -407,6 +529,450 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* LOYALTY CLUB (VERNOSTNÉ ZĽAVY) */}
|
||||
{module.type === 'loyalty_club' && (
|
||||
<div className="space-y-5">
|
||||
{/* Toggle zapnuté / vypnuté */}
|
||||
<div className="bg-red-50/60 p-3.5 rounded-xl border border-red-200 flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-xs font-bold text-gray-900 block">Zobraziť v e-maile</span>
|
||||
<span className="text-[11px] text-gray-500">Sekcia Vernostné zľavy</span>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={module.data.enabled !== false}
|
||||
onChange={(e) => updateData({ enabled: e.target.checked })}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-300 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-red-600"></div>
|
||||
</label>
|
||||
</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>
|
||||
|
||||
<ImageUploadInput
|
||||
label="Obrázok banneru Vernostných zliav"
|
||||
value={module.data.headerImageUrl || ''}
|
||||
onChange={(url) => updateData({ headerImageUrl: url })}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Odkaz banneru</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.headerLink || ''}
|
||||
onChange={(e) => updateData({ headerLink: 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">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>
|
||||
|
||||
{/* 2 Vernostné produkty */}
|
||||
<div className="space-y-4 pt-2">
|
||||
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block">
|
||||
Vernostné produkty ({module.data.products?.length || 0})
|
||||
</span>
|
||||
|
||||
{(module.data.products || []).map((p: any, pIdx: number) => (
|
||||
<div key={p.id || pIdx} className="p-3 bg-gray-50 rounded-xl border border-gray-200 space-y-3">
|
||||
<span className="text-xs font-bold text-gray-900 block truncate">
|
||||
{p.title || `Vernostný produkt ${pIdx + 1}`}
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Názov produktu</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.title || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].title = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ImageUploadInput
|
||||
label="Obrázok produktu"
|
||||
value={p.imageUrl || ''}
|
||||
onChange={(url) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].imageUrl = url;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="https://...produkt.png alebo nahrajte súbor"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Platnosť akcie pre tento produkt (override)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.validityOverride || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].validityOverride = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder={`Predvolená: ${module.data.globalValidity || '2. 5. – 15. 5. 2026'}`}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sekcia Bez karty */}
|
||||
<div className="p-2.5 bg-red-50/40 rounded-lg border border-red-100 space-y-2">
|
||||
<span className="text-[11px] font-bold text-red-700 block">Bez karty</span>
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
<div>
|
||||
<label className="text-[9px] text-gray-500">Zľava</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.noCardDiscount || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].noCardDiscount = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="21%"
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] text-gray-500">Cena (€)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.noCardPriceMain || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].noCardPriceMain = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="0"
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] text-gray-500">Centy</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.noCardPriceSub || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].noCardPriceSub = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="79"
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] text-gray-500">Merná cena (bez karty)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.noCardUnitPrice || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].noCardUnitPrice = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="1kg = 9,88 €"
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sekcia S kartou */}
|
||||
<div className="p-2.5 bg-amber-50/50 rounded-lg border border-amber-200 space-y-2">
|
||||
<span className="text-[11px] font-bold text-amber-900 block">S kartou (Klub)</span>
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
<div>
|
||||
<label className="text-[9px] text-gray-500">Zľava</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.cardDiscount || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].cardDiscount = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="38%"
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] text-gray-500">Cena (€)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.cardPriceMain || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].cardPriceMain = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="0"
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] text-gray-500">Centy</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.cardPriceSub || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].cardPriceSub = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="62"
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] text-gray-500">Merná cena (s kartou)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={p.cardUnitPrice || ''}
|
||||
onChange={(e) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].cardUnitPrice = e.target.value;
|
||||
updateData({ products });
|
||||
}}
|
||||
placeholder="1kg = 7,75 €"
|
||||
className="w-full text-xs border border-gray-300 rounded p-1 bg-white"
|
||||
/>
|
||||
</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={`loyalty-flag-${p.id || pIdx}`}
|
||||
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={`loyalty-flag-${p.id || pIdx}`} className="text-xs text-gray-700 cursor-pointer">
|
||||
Zobraziť slovenskú vlajku / srdce
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ARTICLES ROW (LETÁKY ALEBO ČLÁNKY) */}
|
||||
{module.type === 'articles_row' && (
|
||||
<div className="space-y-5">
|
||||
{module.data.item1?.btnText ? (
|
||||
/* AK SÚ TO LETÁKY */
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 bg-blue-50/50 rounded-xl border border-blue-100">
|
||||
<span className="text-xs font-bold text-blue-900 block mb-1">
|
||||
Leták 1: {module.data.item1?.title || 'Aktuálne letáky'}
|
||||
</span>
|
||||
<p className="text-[11px] text-gray-500 mb-3">Farby, texty a rozloženie sú fixne podľa šablóny. Môžete upraviť odkaz a náhľadový obrázok.</p>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Cieľová URL (kam to smeruje)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.item1?.linkUrl || ''}
|
||||
onChange={(e) => updateData({
|
||||
item1: { ...module.data.item1, linkUrl: e.target.value }
|
||||
})}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<ImageUploadInput
|
||||
label="Náhľadový obrázok letáku (PNG/JPG)"
|
||||
value={module.data.item1?.imageUrl || ''}
|
||||
onChange={(url) => updateData({
|
||||
item1: { ...module.data.item1, imageUrl: url }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-amber-50/50 rounded-xl border border-amber-100">
|
||||
<span className="text-xs font-bold text-amber-900 block mb-1">
|
||||
Leták 2: {module.data.item2?.title || 'Rozšírený leták'}
|
||||
</span>
|
||||
<p className="text-[11px] text-gray-500 mb-3">Farby, texty a rozloženie sú fixne podľa šablóny. Môžete upraviť odkaz a náhľadový obrázok.</p>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Cieľová URL (kam to smeruje)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.item2?.linkUrl || ''}
|
||||
onChange={(e) => updateData({
|
||||
item2: { ...module.data.item2, linkUrl: e.target.value }
|
||||
})}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<ImageUploadInput
|
||||
label="Náhľadový obrázok letáku (PNG/JPG)"
|
||||
value={module.data.item2?.imageUrl || ''}
|
||||
onChange={(url) => updateData({
|
||||
item2: { ...module.data.item2, imageUrl: url }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* AK SÚ TO ČLÁNKY (VEĽKONOČNÝ BARANČEK / VAJÍČKO AMULET) */
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 bg-orange-50/50 rounded-xl border border-orange-100">
|
||||
<span className="text-xs font-bold text-orange-900 block mb-1">
|
||||
Článok 1 (Recepty)
|
||||
</span>
|
||||
<p className="text-[11px] text-gray-500 mb-3">Logá a gradienty sú na pevno. Môžete upraviť text, odkaz a obrázok v pozadí.</p>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Názov / Text článku</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.item1?.title || ''}
|
||||
onChange={(e) => updateData({
|
||||
item1: { ...module.data.item1, title: e.target.value }
|
||||
})}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white font-bold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Cieľová URL (odkaz)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.item1?.linkUrl || ''}
|
||||
onChange={(e) => updateData({
|
||||
item1: { ...module.data.item1, linkUrl: e.target.value }
|
||||
})}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<ImageUploadInput
|
||||
label="Obrázok v pozadí"
|
||||
value={module.data.item1?.imageUrl || ''}
|
||||
onChange={(url) => updateData({
|
||||
item1: { ...module.data.item1, imageUrl: url }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-blue-50/50 rounded-xl border border-blue-100">
|
||||
<span className="text-xs font-bold text-blue-900 block mb-1">
|
||||
Článok 2 (Časopis Jednota)
|
||||
</span>
|
||||
<p className="text-[11px] text-gray-500 mb-3">Logá a gradienty sú na pevno. Môžete upraviť text, odkaz a obrázok v pozadí.</p>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Názov / Text článku</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.item2?.title || ''}
|
||||
onChange={(e) => updateData({
|
||||
item2: { ...module.data.item2, title: e.target.value }
|
||||
})}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white font-bold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-600 font-semibold block mb-0.5">Cieľová URL (odkaz)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.item2?.linkUrl || ''}
|
||||
onChange={(e) => updateData({
|
||||
item2: { ...module.data.item2, linkUrl: e.target.value }
|
||||
})}
|
||||
className="w-full text-xs border border-gray-300 rounded p-1.5 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<ImageUploadInput
|
||||
label="Obrázok v pozadí"
|
||||
value={module.data.item2?.imageUrl || ''}
|
||||
onChange={(url) => updateData({
|
||||
item2: { ...module.data.item2, imageUrl: url }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* FOOTER */}
|
||||
{module.type === 'footer' && (
|
||||
<div className="space-y-4">
|
||||
@@ -419,11 +985,29 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Text odhlásenia</label>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Odkaz pre odhlásenie (Unsubscribe URL)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.unsubscribeText || ''}
|
||||
onChange={(e) => updateData({ unsubscribeText: e.target.value })}
|
||||
value={module.data.unsubscribeUrl || ''}
|
||||
onChange={(e) => updateData({ unsubscribeUrl: 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">Google Play URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.googlePlayUrl || ''}
|
||||
onChange={(e) => updateData({ googlePlayUrl: 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">App Store URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.data.appStoreUrl || ''}
|
||||
onChange={(e) => updateData({ appStoreUrl: e.target.value })}
|
||||
className="w-full text-sm border border-gray-300 rounded-lg p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Upload, Loader2, Image as ImageIcon } from 'lucide-react';
|
||||
|
||||
interface ImageUploadInputProps {
|
||||
label?: string;
|
||||
value: string;
|
||||
onChange: (url: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
|
||||
label = 'Obrázok',
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'https://... alebo nahrajte súbor'
|
||||
}) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success && data.url) {
|
||||
onChange(data.url);
|
||||
} else {
|
||||
alert(`Chyba pri nahrávaní obrázka: ${data.error || 'Neznáma chyba'}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
alert(`Chyba: ${err.message}`);
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
{label && <label className="block text-[11px] font-semibold text-gray-700">{label}</label>}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Náhľad miniatúry */}
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-100 border border-gray-200 flex-shrink-0 flex items-center justify-center overflow-hidden">
|
||||
{value ? (
|
||||
<img
|
||||
src={value}
|
||||
alt="Náhľad"
|
||||
className="w-full h-full object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Textové pole s URL */}
|
||||
<input
|
||||
type="text"
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="flex-1 text-xs font-mono border border-gray-300 rounded-lg p-2 bg-white"
|
||||
/>
|
||||
|
||||
{/* Tlačidlo nahrávania */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading}
|
||||
className="px-2.5 py-2 text-xs font-bold text-gray-700 bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-lg flex items-center gap-1 transition-colors whitespace-nowrap disabled:opacity-50"
|
||||
title="Nahrať obrázok zo zariadenia"
|
||||
>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-red-600" />
|
||||
<span>Nahrávam...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
<span>Nahrať</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,364 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { NewsletterCampaign } from '@/types/newsletter';
|
||||
import { DEFAULT_CAMPAIGN } from '@/lib/defaultCampaign';
|
||||
import {
|
||||
FolderOpen,
|
||||
Plus,
|
||||
Copy,
|
||||
Trash2,
|
||||
X,
|
||||
ExternalLink,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
Layers,
|
||||
Search,
|
||||
Sparkles
|
||||
} from 'lucide-react';
|
||||
|
||||
interface SavedCampaignsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
currentCampaign: NewsletterCampaign;
|
||||
onLoadCampaign: (campaign: NewsletterCampaign) => void;
|
||||
onSaveCurrentAsCopy: (newTitle: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const SavedCampaignsModal: React.FC<SavedCampaignsModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
currentCampaign,
|
||||
onLoadCampaign,
|
||||
onSaveCurrentAsCopy
|
||||
}) => {
|
||||
const [campaigns, setCampaigns] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [isCreatingNew, setIsCreatingNew] = useState(false);
|
||||
const [newTitle, setNewTitle] = useState('');
|
||||
|
||||
const fetchCampaigns = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.select('*')
|
||||
.order('updated_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
setCampaigns(data || []);
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching campaigns:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
fetchCampaigns();
|
||||
setIsCreatingNew(false);
|
||||
setNewTitle('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleDuplicate = async (camp: any) => {
|
||||
try {
|
||||
const clonedTitle = `Kópia - ${camp.title}`;
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.insert([
|
||||
{
|
||||
title: clonedTitle,
|
||||
status: 'draft',
|
||||
data: camp.data
|
||||
}
|
||||
])
|
||||
.select();
|
||||
|
||||
if (error) throw error;
|
||||
if (data && data[0]) {
|
||||
await fetchCampaigns();
|
||||
onLoadCampaign({
|
||||
id: data[0].id,
|
||||
title: data[0].title,
|
||||
preheader: data[0].data?.preheader || '',
|
||||
status: 'draft',
|
||||
modules: data[0].data?.modules || []
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
} catch (err: any) {
|
||||
alert(`Chyba pri duplikovaní: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string, title: string) => {
|
||||
if (!confirm(`Naozaj chcete vymazať newsletter "${title}"?`)) return;
|
||||
|
||||
try {
|
||||
const { error } = await supabase.from('templates').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
setCampaigns((prev) => prev.filter((c) => c.id !== id));
|
||||
if (currentCampaign.id === id) {
|
||||
// Ak sme vymazali práve otvorený, resetneme ID
|
||||
onLoadCampaign({ ...currentCampaign, id: '' });
|
||||
}
|
||||
} catch (err: any) {
|
||||
alert(`Chyba pri mazaní: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNew = async () => {
|
||||
const title = newTitle.trim() || `COOP Newsletter - ${new Date().toLocaleDateString('sk-SK')}`;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('templates')
|
||||
.insert([
|
||||
{
|
||||
title,
|
||||
status: 'draft',
|
||||
data: {
|
||||
preheader: DEFAULT_CAMPAIGN.preheader,
|
||||
modules: JSON.parse(JSON.stringify(DEFAULT_CAMPAIGN.modules))
|
||||
}
|
||||
}
|
||||
])
|
||||
.select();
|
||||
|
||||
if (error) throw error;
|
||||
if (data && data[0]) {
|
||||
onLoadCampaign({
|
||||
id: data[0].id,
|
||||
title: data[0].title,
|
||||
preheader: data[0].data?.preheader || '',
|
||||
status: 'draft',
|
||||
modules: data[0].data?.modules || []
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
} catch (err: any) {
|
||||
alert(`Chyba pri vytváraní: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveCurrentCopy = async () => {
|
||||
const title = newTitle.trim() || `Kópia - ${currentCampaign.title}`;
|
||||
await onSaveCurrentAsCopy(title);
|
||||
await fetchCampaigns();
|
||||
setIsCreatingNew(false);
|
||||
setNewTitle('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const filtered = campaigns.filter((c) =>
|
||||
(c.title || '').toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-3xl shadow-2xl max-w-4xl w-full max-h-[85vh] flex flex-col overflow-hidden border border-gray-100">
|
||||
{/* Horná lišta */}
|
||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-slate-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-2xl bg-red-100 flex items-center justify-center text-red-600">
|
||||
<FolderOpen className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-gray-900">Moje Newslettre</h2>
|
||||
<p className="text-xs text-gray-500">Spravujte, otvárajte a duplikujte uložené šablóny e-mailov</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setIsCreatingNew((prev) => !prev)}
|
||||
className="px-3.5 py-2 text-xs font-bold bg-[#e2001a] hover:bg-[#b80015] text-white rounded-xl flex items-center gap-1.5 shadow-sm transition-all"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Nový newsletter</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-9 h-9 rounded-xl hover:bg-gray-200 text-gray-400 hover:text-gray-700 flex items-center justify-center transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Panel na vytvorenie / uloženie kópie */}
|
||||
{isCreatingNew && (
|
||||
<div className="p-4 bg-red-50/50 border-b border-red-100 flex flex-col sm:flex-row items-center gap-3">
|
||||
<div className="flex-1 w-full">
|
||||
<input
|
||||
type="text"
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
placeholder="Zadajte názov nového newslettera (napr. Kampaň Veľká Noc 2026)"
|
||||
className="w-full text-xs border border-gray-300 rounded-xl p-2.5 bg-white font-medium focus:ring-2 focus:ring-red-500 focus:outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||
<button
|
||||
onClick={handleCreateNew}
|
||||
className="flex-1 sm:flex-initial px-3.5 py-2 text-xs font-bold bg-slate-900 hover:bg-black text-white rounded-xl whitespace-nowrap"
|
||||
>
|
||||
Vytvoriť čistý z predlohy
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveCurrentCopy}
|
||||
className="flex-1 sm:flex-initial px-3.5 py-2 text-xs font-bold bg-[#003399] hover:bg-blue-900 text-white rounded-xl whitespace-nowrap"
|
||||
>
|
||||
Uložiť aktuálny ako kópiu
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vyhľadávanie */}
|
||||
<div className="p-4 border-b border-gray-100 bg-white">
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 text-gray-400 absolute left-3 top-3" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Vyhľadávať medzi uloženými newslettrami..."
|
||||
className="w-full pl-9 pr-4 py-2 text-xs border border-gray-200 rounded-xl bg-gray-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Zoznam kampaní */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50/50">
|
||||
{loading ? (
|
||||
<div className="py-12 text-center text-sm text-gray-400">Načítavam uložené newslettre...</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="py-12 text-center text-gray-400 space-y-2">
|
||||
<Sparkles className="w-8 h-8 text-gray-300 mx-auto" />
|
||||
<p className="text-sm font-semibold text-gray-600">Nenašli sa žiadne uložené newslettre</p>
|
||||
<p className="text-xs text-gray-400">Vytvorte nový kliknutím na tlačidlo vyššie.</p>
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((camp) => {
|
||||
const isCurrent = camp.id === currentCampaign.id;
|
||||
const moduleCount = camp.data?.modules?.length || 0;
|
||||
const isExported = camp.status === 'exported' || !!camp.brevo_campaign_id;
|
||||
const updatedDate = camp.updated_at
|
||||
? new Date(camp.updated_at).toLocaleString('sk-SK', {
|
||||
day: 'numeric',
|
||||
month: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
: 'Neznámy dátum';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={camp.id}
|
||||
className={`p-4 rounded-2xl border transition-all flex flex-col sm:flex-row sm:items-center justify-between gap-3 ${
|
||||
isCurrent
|
||||
? 'bg-red-50/50 border-red-300 ring-2 ring-red-500/10'
|
||||
: 'bg-white border-gray-200 hover:border-gray-300 hover:shadow-sm'
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-bold text-gray-900">{camp.title}</span>
|
||||
{isCurrent && (
|
||||
<span className="text-[10px] font-bold bg-red-600 text-white px-2 py-0.5 rounded-full uppercase tracking-wider">
|
||||
Práve otvorený
|
||||
</span>
|
||||
)}
|
||||
{isExported && (
|
||||
<span className="text-[10px] font-bold bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded-full flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3 h-3 text-emerald-600" /> Vytvorené v Brevo
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3.5 h-3.5 text-gray-400" /> {updatedDate}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Layers className="w-3.5 h-3.5 text-gray-400" /> {moduleCount} modulov
|
||||
</span>
|
||||
{camp.brevo_campaign_id && (
|
||||
<a
|
||||
href={`https://app.brevo.com/campaigns/email/edit/${camp.brevo_campaign_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline flex items-center gap-1 font-semibold"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span>Brevo #{camp.brevo_campaign_id}</span>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Akčné tlačidlá */}
|
||||
<div className="flex items-center gap-2 pt-2 sm:pt-0">
|
||||
<button
|
||||
onClick={() => {
|
||||
onLoadCampaign({
|
||||
id: camp.id,
|
||||
title: camp.title,
|
||||
preheader: camp.data?.preheader || '',
|
||||
status: camp.status || 'draft',
|
||||
brevoCampaignId: camp.brevo_campaign_id,
|
||||
modules: camp.data?.modules || []
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
className="px-3 py-1.5 text-xs font-bold bg-slate-900 hover:bg-black text-white rounded-xl transition-colors"
|
||||
>
|
||||
{isCurrent ? 'Pokračovať v úprave' : 'Otvoriť'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleDuplicate(camp)}
|
||||
className="px-3 py-1.5 text-xs font-bold bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-xl flex items-center gap-1 transition-colors"
|
||||
title="Vytvoriť kópiu"
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
<span>Duplikovať</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(camp.id, camp.title)}
|
||||
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors"
|
||||
title="Vymazať"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pätička */}
|
||||
<div className="px-6 py-3.5 border-t border-gray-100 bg-white flex items-center justify-between text-xs text-gray-500">
|
||||
<span>Celkovo uložených: {campaigns.length} newsletterov</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-1.5 font-bold text-gray-600 hover:bg-gray-100 rounded-xl transition-colors"
|
||||
>
|
||||
Zatvoriť
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Download,
|
||||
Send,
|
||||
ExternalLink,
|
||||
CheckCircle2,
|
||||
Plus
|
||||
Plus,
|
||||
FolderOpen,
|
||||
Save,
|
||||
Copy,
|
||||
Edit2
|
||||
} from 'lucide-react';
|
||||
import { NewsletterCampaign } from '@/types/newsletter';
|
||||
|
||||
@@ -14,10 +18,14 @@ interface TopBarProps {
|
||||
campaign: NewsletterCampaign;
|
||||
isSaving: boolean;
|
||||
onSave: () => void;
|
||||
onUpdateTitle: (title: string) => void;
|
||||
onOpenSavedCampaigns: () => void;
|
||||
onDuplicateCampaign: () => void;
|
||||
onExportZip: () => void;
|
||||
onExportBrevo: () => void;
|
||||
isExportingBrevo: boolean;
|
||||
brevoCampaignUrl?: string | null;
|
||||
brevoListingUrl?: string | null;
|
||||
onAddModule: (type: any) => void;
|
||||
}
|
||||
|
||||
@@ -25,81 +33,156 @@ export const TopBar: React.FC<TopBarProps> = ({
|
||||
campaign,
|
||||
isSaving,
|
||||
onSave,
|
||||
onUpdateTitle,
|
||||
onOpenSavedCampaigns,
|
||||
onDuplicateCampaign,
|
||||
onExportZip,
|
||||
onExportBrevo,
|
||||
isExportingBrevo,
|
||||
brevoCampaignUrl,
|
||||
brevoListingUrl = 'https://app.brevo.com/campaigns/listing',
|
||||
onAddModule
|
||||
}) => {
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
|
||||
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 stav */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-[#e2001a] flex items-center justify-center text-white font-black text-sm shadow-sm tracking-tighter">
|
||||
COOP
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-sm font-bold text-gray-900 leading-tight">
|
||||
{campaign.title || 'COOP Jednota Newsletter'}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-[11px] text-gray-500">
|
||||
{isSaving ? (
|
||||
<span className="text-amber-600 font-medium animate-pulse">Ukladám do Supabase...</span>
|
||||
) : (
|
||||
<span className="text-emerald-600 font-medium flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" /> Uložené v databáze (Auto-save)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<header className="h-16 bg-white border-b border-gray-200 px-4 md:px-6 flex items-center justify-between sticky top-0 z-30 shadow-xs">
|
||||
{/* Ľavá časť: Logo, Moje newslettre, Titulok a stav */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-[#e2001a] flex items-center justify-center text-white font-black text-sm shadow-sm tracking-tighter shrink-0">
|
||||
COOP
|
||||
</div>
|
||||
|
||||
{/* Tlačidlo Moje newslettre */}
|
||||
<button
|
||||
onClick={onOpenSavedCampaigns}
|
||||
className="flex items-center gap-1.5 text-xs font-bold text-gray-700 bg-gray-100 hover:bg-gray-200 px-3 py-1.5 rounded-xl border border-gray-200 transition-colors shadow-xs"
|
||||
title="Otvoriť, duplikovať alebo vytvoriť nový newsletter"
|
||||
>
|
||||
<FolderOpen className="w-3.5 h-3.5 text-blue-600" />
|
||||
<span className="hidden sm:inline">Moje newslettre</span>
|
||||
</button>
|
||||
|
||||
<div className="h-6 w-px bg-gray-200 mx-1 hidden sm:block" />
|
||||
|
||||
{/* Názov kampane */}
|
||||
<div className="flex flex-col">
|
||||
{isEditingTitle ? (
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={campaign.title || ''}
|
||||
onChange={(e) => onUpdateTitle(e.target.value)}
|
||||
onBlur={() => setIsEditingTitle(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') setIsEditingTitle(false);
|
||||
}}
|
||||
className="text-xs font-bold text-gray-900 border border-blue-400 rounded px-2 py-0.5 outline-none bg-blue-50/50 w-56"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => setIsEditingTitle(true)}
|
||||
className="group flex items-center gap-1.5 cursor-pointer"
|
||||
title="Kliknite pre úpravu názvu"
|
||||
>
|
||||
<h1 className="text-xs md:text-sm font-bold text-gray-900 leading-tight max-w-[200px] md:max-w-xs truncate group-hover:text-blue-600">
|
||||
{campaign.title || 'COOP Jednota Newsletter'}
|
||||
</h1>
|
||||
<Edit2 className="w-3 h-3 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-[10px] text-gray-500">
|
||||
{isSaving ? (
|
||||
<span className="text-amber-600 font-medium animate-pulse">Ukladám...</span>
|
||||
) : (
|
||||
<span className="text-emerald-600 font-medium flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3 h-3" /> Uložené
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pridanie sekcie */}
|
||||
<div className="hidden md:flex items-center gap-2 ml-6 pl-6 border-l border-gray-200">
|
||||
<div className="hidden lg:flex items-center gap-2 ml-4 pl-4 border-l border-gray-200">
|
||||
<button
|
||||
onClick={() => onAddModule('banner')}
|
||||
className="text-xs font-bold text-gray-700 bg-gray-100 hover:bg-gray-200 px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition-colors"
|
||||
className="text-[11px] font-bold text-gray-700 bg-gray-50 hover:bg-gray-100 border border-gray-200 px-2.5 py-1 rounded-lg flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 text-gray-500" /> Nový Banner
|
||||
<Plus className="w-3 h-3 text-gray-500" /> Nový Banner
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAddModule('products_grid')}
|
||||
className="text-xs font-bold text-gray-700 bg-gray-100 hover:bg-gray-200 px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition-colors"
|
||||
className="text-[11px] font-bold text-gray-700 bg-gray-50 hover:bg-gray-100 border border-gray-200 px-2.5 py-1 rounded-lg flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 text-gray-500" /> Produktový rad
|
||||
<Plus className="w-3 h-3 text-gray-500" /> Produktový rad
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pravá časť: Export do ZIP a Brevo */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Pravá časť: Uložiť, Duplikovať, Export do ZIP a Brevo */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Tlačidlo manuálneho uloženia */}
|
||||
<button
|
||||
onClick={onSave}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-1.5 text-xs font-bold text-gray-700 bg-white border border-gray-200 hover:bg-gray-50 disabled:opacity-50 px-3 py-1.5 rounded-xl transition-all shadow-xs"
|
||||
title="Uložiť zmeny ihneď do databázy"
|
||||
>
|
||||
<Save className="w-3.5 h-3.5 text-gray-600" />
|
||||
<span className="hidden md:inline">Uložiť</span>
|
||||
</button>
|
||||
|
||||
{/* Tlačidlo duplikovania celej kampane */}
|
||||
<button
|
||||
onClick={onDuplicateCampaign}
|
||||
className="flex items-center gap-1.5 text-xs font-bold text-gray-700 bg-white border border-gray-200 hover:bg-gray-50 px-3 py-1.5 rounded-xl transition-all shadow-xs"
|
||||
title="Vytvoriť kópiu tohto newslettera"
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5 text-gray-600" />
|
||||
<span className="hidden md:inline">Duplikovať</span>
|
||||
</button>
|
||||
|
||||
<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-4 py-2 rounded-xl transition-all shadow-xs hover:shadow-sm"
|
||||
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-1.5 rounded-xl transition-all shadow-xs"
|
||||
>
|
||||
<Download className="w-4 h-4 text-gray-600" /> Export ZIP
|
||||
<Download className="w-3.5 h-3.5 text-gray-600" />
|
||||
<span className="hidden sm:inline">Export ZIP</span>
|
||||
</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-4 py-2 rounded-xl transition-all shadow-xs"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" /> Otvoriť v Brevo
|
||||
</a>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<a
|
||||
href={brevoCampaignUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-1.5 text-xs font-bold text-white bg-emerald-600 hover:bg-emerald-700 px-3.5 py-1.5 rounded-xl transition-all shadow-xs"
|
||||
title="Upraviť a odoslať kampaň priamo v Brevo"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
<span>Upraviť v Brevo</span>
|
||||
</a>
|
||||
<a
|
||||
href={brevoListingUrl || 'https://app.brevo.com/campaigns/listing'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="hidden lg:flex items-center gap-1 text-[11px] font-bold text-emerald-800 bg-emerald-100 hover:bg-emerald-200 px-2.5 py-1.5 rounded-xl transition-all"
|
||||
title="Prehľad všetkých e-mailových kampaní v Brevo"
|
||||
>
|
||||
Zoznam
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={onExportBrevo}
|
||||
disabled={isExportingBrevo}
|
||||
className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#003399] hover:bg-blue-800 disabled:opacity-50 px-4 py-2 rounded-xl transition-all shadow-xs"
|
||||
className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#003399] hover:bg-blue-800 disabled:opacity-50 px-3.5 py-1.5 rounded-xl transition-all shadow-xs"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
{isExportingBrevo ? 'Odosielam...' : 'Odoslať do Brevo'}
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
<span>{isExportingBrevo ? 'Odosielam...' : 'Odoslať do Brevo'}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+37
-29
@@ -209,12 +209,12 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 15px 0 20px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="force-bg-red" bgcolor="#e2001a" style="background-color: #e2001a; background: linear-gradient(to right, #ff3d4f, #e2001a); border-radius: 12px; box-shadow: 0 10px 25px rgba(226, 0, 26, 0.3); border: 1px solid rgba(255,255,255,0.2);">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="force-bg-red" bgcolor="#e2001a" style="background-color: #e2001a; background: linear-gradient(to right, #ff3d4f, #e2001a); border: 0; border-radius: 12px; box-shadow: 0 10px 25px rgba(226, 0, 26, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 10px 20px; text-align: center;">
|
||||
<!--[if mso]><table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="600"><tr><td align="center" valign="middle"><![endif]-->
|
||||
<div style="display: inline-block; vertical-align: middle;" class="stack-column-center">
|
||||
<span class="top-tip" style="background-color: #fefefe; color: #e2001a; font-size: 10px; font-weight: 800; padding: 2px 8px; border-radius: 4px; margin: 4px 5px; vertical-align: middle; display: inline-block; letter-spacing: 1px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">${escapeHtml(m.data.tipBadge || 'TIP')}</span>
|
||||
<span class="top-tip" style="background-color: #fefefe; color: #e2001a; font-size: 10px; font-weight: 800; padding: 2px 8px; border: 0; border-radius: 4px; margin: 4px 5px; vertical-align: middle; display: inline-block; letter-spacing: 1px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">${escapeHtml(m.data.tipBadge || 'TIP')}</span>
|
||||
</div>
|
||||
<div style="display: inline-block; vertical-align: middle;" class="stack-column-center">
|
||||
<span class="top-msg force-text-white" style="color: #fefefe; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; vertical-align: middle; display: inline-block; padding: 5px 10px;">${escapeHtml(m.data.text || '')}</span>
|
||||
@@ -232,16 +232,20 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
</table>`;
|
||||
|
||||
case 'header':
|
||||
const navLinks = (m.data.navLinks || []).map((link: any) => {
|
||||
const rawNav = m.data.navLinks && m.data.navLinks.length > 0 ? m.data.navLinks : [
|
||||
{ label: 'Letáky', url: 'https://www.coop.sk/sk/letaky', variant: 'outline' },
|
||||
{ label: 'COOP Klub', url: 'https://www.coopklub.sk', variant: 'filled' }
|
||||
];
|
||||
const navLinks = rawNav.map((link: any) => {
|
||||
if (link.variant === 'filled') {
|
||||
return `<a href="${escapeHtml(link.url)}" class="header-link force-text-white force-bg-blue" style="color: #fefefe; font-size: 12px; font-weight: 700; text-decoration: none; text-transform: uppercase; padding: 10px 16px; background-color: #003399; border-radius: 20px; display: inline-block; letter-spacing: 0.5px; box-shadow: 0 4px 10px rgba(0, 51, 153, 0.2);">${escapeHtml(link.label)}</a>`;
|
||||
return `<a href="${escapeHtml(link.url || '#')}" class="header-link force-text-white force-bg-blue" style="color: #fefefe; font-size: 12px; font-weight: 700; text-decoration: none; text-transform: uppercase; padding: 10px 16px; background-color: #003399; border-radius: 20px; display: inline-block; letter-spacing: 0.5px; box-shadow: 0 4px 10px rgba(0, 51, 153, 0.2);">${escapeHtml(link.label || 'Odkaz')}</a>`;
|
||||
}
|
||||
return `<a href="${escapeHtml(link.url)}" class="header-link" style="color: #e2001a; font-size: 12px; font-weight: 700; text-decoration: none; text-transform: uppercase; padding: 8px 16px; border: 2px solid #e2001a; border-radius: 20px; margin-right: 6px; display: inline-block; letter-spacing: 0.5px;">${escapeHtml(link.label)}</a>`;
|
||||
return `<a href="${escapeHtml(link.url || '#')}" class="header-link" style="color: #e2001a; font-size: 12px; font-weight: 700; text-decoration: none; text-transform: uppercase; padding: 8px 16px; border: 2px solid #e2001a; border-radius: 20px; margin-right: 6px; display: inline-block; letter-spacing: 0.5px;">${escapeHtml(link.label || 'Odkaz')}</a>`;
|
||||
}).join('\n');
|
||||
|
||||
return `
|
||||
<!-- 1. HLAVIČKA -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="card-white card-white-bg" bgcolor="#fefefe" style="background-color: #fefefe; border: 1px solid #fefefe; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); overflow: hidden;">
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="card-white card-white-bg" bgcolor="#fefefe" style="background-color: #fefefe; border: 0; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); overflow: hidden;">
|
||||
<tr>
|
||||
<td style="padding: 15px 20px;" class="mobile-padding">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
@@ -264,11 +268,11 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
case 'banner':
|
||||
return `
|
||||
<!-- KAMPAŇ BANNER -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="card-white card-white-bg" bgcolor="#fefefe" style="background-color: #fefefe; border: 1px solid #fefefe; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); overflow: hidden;">
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="card-white card-white-bg" bgcolor="#fefefe" style="background-color: #fefefe; border: 0; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); overflow: hidden;">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="${escapeHtml(m.data.linkUrl || '#')}">
|
||||
<img src="${escapeHtml(m.data.imageUrl)}" width="600" alt="${escapeHtml(m.data.altText || '')}" style="display: block; border: 0; width: 100%; max-width: 100%; border-top-left-radius: 16px; border-top-right-radius: 16px;">
|
||||
<img src="${escapeHtml(m.data.imageUrl)}" width="600" alt="${escapeHtml(m.data.altText || '')}" style="display: block; border: 0; width: 100%; max-width: 100%; border-radius: ${m.data.title || m.data.description || m.data.buttonText ? '16px 16px 0 0' : '16px'};">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -335,6 +339,9 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
</table>`;
|
||||
|
||||
case 'loyalty_club':
|
||||
if (m.data.enabled === false) {
|
||||
return '';
|
||||
}
|
||||
const lProducts = m.data.products || [];
|
||||
return `
|
||||
<!-- 7. VERNOSTNÉ ZĽAVY (COOP KLUB) -->
|
||||
@@ -347,7 +354,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
<!-- Banner karta -->
|
||||
<tr>
|
||||
<td style="padding-bottom: 20px;">
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="card-white card-vernostna" bgcolor="#e6e1d9" style="background-color: #e6e1d9; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); overflow: hidden;">
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="card-white card-vernostna" bgcolor="#e6e1d9" style="background-color: #e6e1d9; border: 0; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); overflow: hidden;">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="${escapeHtml(m.data.headerLink || '#')}">
|
||||
@@ -386,7 +393,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
<tr>
|
||||
<!-- Leták 1 -->
|
||||
<td width="50%" valign="top" style="padding-right: 6px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="${it1.bgColor}" style="background-color: ${it1.bgColor}; border-radius: 16px; overflow: hidden; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="${it1.bgColor}" style="background-color: ${it1.bgColor}; border: 0; border-radius: 16px; overflow: hidden; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="${escapeHtml(it1.linkUrl || '#')}">
|
||||
@@ -410,7 +417,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
</td>
|
||||
<!-- Leták 2 -->
|
||||
<td width="50%" valign="top" style="padding-left: 6px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="${it2.bgColor}" style="background-color: ${it2.bgColor}; border-radius: 16px; overflow: hidden; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="${it2.bgColor}" style="background-color: ${it2.bgColor}; border: 0; border-radius: 16px; overflow: hidden; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="${escapeHtml(it2.linkUrl || '#')}">
|
||||
@@ -444,7 +451,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
<tr>
|
||||
<!-- Článok 1 -->
|
||||
<td width="50%" valign="top" style="padding-right: 6px; padding-bottom: 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="${it1.bgColor}" style="background-color: ${it1.bgColor}; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.15);">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="${it1.bgColor}" style="background-color: ${it1.bgColor}; border: 0; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.15);">
|
||||
<tr>
|
||||
<td background="${escapeHtml(it1.imageUrl)}" bgcolor="${it1.bgColor}" width="300" height="240" valign="bottom" style="background-image: url('${escapeHtml(it1.imageUrl)}'); background-size: cover; background-position: center;">
|
||||
<!--[if gte mso 9]>
|
||||
@@ -473,7 +480,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
</td>
|
||||
<!-- Článok 2 -->
|
||||
<td width="50%" valign="top" style="padding-left: 6px; padding-bottom: 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="${it2.bgColor}" style="background-color: ${it2.bgColor}; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.15);">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="${it2.bgColor}" style="background-color: ${it2.bgColor}; border: 0; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.15);">
|
||||
<tr>
|
||||
<td background="${escapeHtml(it2.imageUrl)}" bgcolor="${it2.bgColor}" width="300" height="240" valign="bottom" style="background-image: url('${escapeHtml(it2.imageUrl)}'); background-size: cover; background-position: center;">
|
||||
<!--[if gte mso 9]>
|
||||
@@ -507,7 +514,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
case 'footer':
|
||||
return `
|
||||
<!-- 9. PÄTIČKA (1:1 s originálnou šablónou) -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="card-white card-white-bg" bgcolor="#fefefe" style="background-color: #fefefe; border: 1px solid #fefefe; border-radius: 16px; padding: 40px 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);">
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="card-white card-white-bg" bgcolor="#fefefe" style="background-color: #fefefe; border: 0; border-radius: 16px; padding: 40px 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);">
|
||||
<tr>
|
||||
<td style="padding: 30px 10px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
@@ -548,7 +555,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
||||
</table>
|
||||
|
||||
<!-- Disclaimer a Unsubscribe -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-top: 15px; border-top: 1px solid rgba(0,0,0,0.1); padding-top: 15px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-top: 15px; border-top: 0; padding-top: 15px;">
|
||||
<tr>
|
||||
<td class="force-text-dark" style="text-align: center; font-size: 11px; color: #666666; line-height: 1.5;">
|
||||
<a href="${escapeHtml(m.data.recipesUrl || 'https://recepty.coop.sk')}" style="color: #444444; text-decoration: underline;">Prihláste sa</a> aj na odber newslettera (Ne)tradičných receptov.
|
||||
@@ -584,13 +591,13 @@ function renderExactProductCard(
|
||||
|
||||
const storesHtml = `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right">
|
||||
${p.stores?.potraviny ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 1px solid #fefefe; background-color: #e2001a; padding: 0; line-height: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_potraviny.png" height="10" alt="P" style="display: block; height: 10px; width: auto;"></td></tr></table></td></tr>` : ''}
|
||||
${p.stores?.supermarket ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 1px solid #fefefe; background-color: #f9890e; padding: 0; line-height: 0; border-top: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_supermarket.png" height="10" alt="S" style="display: block; height: 10px; width: auto;"></td></tr></table></td></tr>` : ''}
|
||||
${p.stores?.tempo ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 1px solid #fefefe; background-color: #005239; padding: 0; line-height: 0; border-top: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_tempo.png" height="10" alt="T" style="display: block; height: 10px; width: auto;"></td></tr></table></td></tr>` : ''}
|
||||
${p.stores?.potraviny ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 0; background-color: #e2001a; padding: 0; line-height: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_potraviny.png" height="10" alt="P" style="display: block; height: 10px; width: auto; border: 0;"></td></tr></table></td></tr>` : ''}
|
||||
${p.stores?.supermarket ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 0; background-color: #f9890e; padding: 0; line-height: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_supermarket.png" height="10" alt="S" style="display: block; height: 10px; width: auto; border: 0;"></td></tr></table></td></tr>` : ''}
|
||||
${p.stores?.tempo ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 0; background-color: #005239; padding: 0; line-height: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_tempo.png" height="10" alt="T" style="display: block; height: 10px; width: auto; border: 0;"></td></tr></table></td></tr>` : ''}
|
||||
</table>`;
|
||||
|
||||
return `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%" class="glass-product" bgcolor="${cardBg}" style="background-color: ${cardBg}; border: 1px solid ${cardBorder}; border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%" class="glass-product" bgcolor="${cardBg}" style="background-color: ${cardBg}; border: 0; border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);">
|
||||
<tr>
|
||||
<td style="padding: 15px 15px 0 15px;" valign="top">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
@@ -625,7 +632,7 @@ function renderExactProductCard(
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" style="width: 95px; margin: 0 auto;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="background-color: ${badgeHdrBg}; border: 2px solid #fefefe; border-radius: 12px 0 0 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.18); width: 85px; overflow: hidden;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="background-color: ${badgeHdrBg}; border: 0; border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.18); width: 85px; overflow: hidden;">
|
||||
${badgeCustomTxt ? `
|
||||
<tr>
|
||||
<td bgcolor="${badgeHdrBg}" style="padding: 4px 4px; text-align: center; background-color: ${badgeHdrBg};">
|
||||
@@ -639,7 +646,7 @@ function renderExactProductCard(
|
||||
</td>
|
||||
</tr>` : '')}
|
||||
<tr>
|
||||
<td class="force-text-white" bgcolor="${badgePrcBg}" style="background-color: ${badgePrcBg}; padding: 4px 2px 2px 2px; font-family: 'Arial Narrow', Arial, sans-serif; color: ${badgePrcColor}; font-weight: 900; line-height: 1; text-align: center; border-radius: 0 0 0 8px;">
|
||||
<td class="force-text-white" bgcolor="${badgePrcBg}" style="background-color: ${badgePrcBg}; padding: 4px 2px 2px 2px; font-family: 'Arial Narrow', Arial, sans-serif; color: ${badgePrcColor}; font-weight: 900; line-height: 1; text-align: center; border-radius: 0 0 12px 12px;">
|
||||
<span style="font-size: 52px;">${escapeHtml(p.priceMain)}</span><sup style="font-size: 26px; vertical-align: top; line-height: 1.2;">${escapeHtml(p.priceSub)}</sup>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -660,20 +667,21 @@ function renderExactProductCard(
|
||||
}
|
||||
|
||||
function renderExactLoyaltyCard(p: any, validity: string): string {
|
||||
const cardValidity = p.validityOverride || validity;
|
||||
const storesHtml = `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right">
|
||||
${p.stores?.potraviny ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 1px solid #fefefe; background-color: #e2001a; padding: 0; line-height: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_potraviny.png" height="10" alt="P" style="display: block; height: 10px; width: auto;"></td></tr></table></td></tr>` : ''}
|
||||
${p.stores?.supermarket ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 1px solid #fefefe; background-color: #f9890e; padding: 0; line-height: 0; border-top: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_supermarket.png" height="10" alt="S" style="display: block; height: 10px; width: auto;"></td></tr></table></td></tr>` : ''}
|
||||
${p.stores?.tempo ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 1px solid #fefefe; background-color: #005239; padding: 0; line-height: 0; border-top: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_tempo.png" height="10" alt="T" style="display: block; height: 10px; width: auto;"></td></tr></table></td></tr>` : ''}
|
||||
${p.stores?.potraviny ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 0; background-color: #e2001a; padding: 0; line-height: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_potraviny.png" height="10" alt="P" style="display: block; height: 10px; width: auto; border: 0;"></td></tr></table></td></tr>` : ''}
|
||||
${p.stores?.supermarket ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 0; background-color: #f9890e; padding: 0; line-height: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_supermarket.png" height="10" alt="S" style="display: block; height: 10px; width: auto; border: 0;"></td></tr></table></td></tr>` : ''}
|
||||
${p.stores?.tempo ? `<tr><td align="right" style="line-height: 0;"><table role="presentation" cellspacing="0" cellpadding="0" border="0" align="right"><tr><td style="border: 0; background-color: #005239; padding: 0; line-height: 0;"><img src="${SUPABASE_ASSETS_BASE}stit_tempo.png" height="10" alt="T" style="display: block; height: 10px; width: auto; border: 0;"></td></tr></table></td></tr>` : ''}
|
||||
</table>`;
|
||||
|
||||
return `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%" class="glass-product card-vernostna" bgcolor="#e6e1d9" style="background-color: #e6e1d9; border: 1px solid #cfc8bb; border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%" class="glass-product card-vernostna" bgcolor="#e6e1d9" style="background-color: #e6e1d9; border: 0; border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);">
|
||||
<tr>
|
||||
<td style="padding: 15px 15px 0 15px;" valign="top">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="font-size: 9px; color: #666666; font-weight: 500;" valign="top">${escapeHtml(validity)}</td>
|
||||
<td style="font-size: 9px; color: #666666; font-weight: 500;" valign="top">${escapeHtml(cardValidity)}</td>
|
||||
<td align="right">${storesHtml}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -699,7 +707,7 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
|
||||
<tr>
|
||||
<!-- Bez karty -->
|
||||
<td width="42%" valign="bottom" style="padding-right: 0;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fefefe; border: 2px solid #fefefe; border-radius: 10px 0 0 10px; box-shadow: -2px 4px 8px rgba(0,0,0,0.12); overflow: hidden;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fefefe; border: 0; border-radius: 10px 0 0 10px; box-shadow: -2px 4px 8px rgba(0,0,0,0.12); overflow: hidden;">
|
||||
<tr>
|
||||
<td class="force-bg-red" bgcolor="#e2001a" style="background-color: #e2001a; padding: 4px 2px; text-align: center;">
|
||||
<div style="font-size: 6px; color: #fefefe; font-weight: 700; text-transform: uppercase; line-height: 1; margin-bottom: 1px;">Bez karty</div>
|
||||
@@ -715,7 +723,7 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
|
||||
</td>
|
||||
<!-- S kartou -->
|
||||
<td width="58%" valign="top">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fefefe; border: 2px solid #fefefe; border-radius: 10px 0 0 0; box-shadow: 4px 4px 12px rgba(0,0,0,0.15); overflow: hidden;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fefefe; border: 0; border-radius: 0 10px 10px 0; box-shadow: 4px 4px 12px rgba(0,0,0,0.15); overflow: hidden;">
|
||||
<tr>
|
||||
<td style="background-color: #fefefe; padding: 5px 4px 2px 4px; text-align: right;">
|
||||
<div style="font-size: 7px; color: #e2001a; font-weight: 800; text-transform: uppercase; line-height: 1; text-align: center; margin-bottom: 1px;">S kartou</div>
|
||||
@@ -726,7 +734,7 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="force-bg-red force-text-white" bgcolor="#e2001a" style="background-color: #e2001a; padding: 3px 2px; text-align: center; color: #fefefe; font-family: 'Arial Narrow', Arial, sans-serif; font-weight: 900; line-height: 1; border-radius: 0;">
|
||||
<td class="force-bg-red force-text-white" bgcolor="#e2001a" style="background-color: #e2001a; padding: 3px 2px; text-align: center; color: #fefefe; font-family: 'Arial Narrow', Arial, sans-serif; font-weight: 900; line-height: 1; border-radius: 0 0 10px 0;">
|
||||
<span style="font-size: 48px;">${escapeHtml(p.cardPriceMain)}</span><sup style="font-size: 24px; vertical-align: top; line-height: 1.2;">${escapeHtml(p.cardPriceSub)}</sup>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -146,6 +146,9 @@ export interface BadgeLayer {
|
||||
y: number;
|
||||
fontSize?: number;
|
||||
fontWeight?: string;
|
||||
fontFamily?: string;
|
||||
fontSource?: 'system' | 'google' | 'adobe';
|
||||
fontUrl?: string;
|
||||
fill: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user