feat: zmena farby pozadia produktov a editovatelne nazvy modulov

This commit is contained in:
Jakub_Hostacny committed 2026-10-08 09:49:33 +02:00
1 parent 4e4e5e5af3
commit 00d681bec2
7 files changed
+325 -87

No files matched your search

+4 -4
View File
@@ -63,8 +63,8 @@ export async function POST(req: NextRequest) {
success: true,
updated: true,
campaignId: Number(existingCampaignId),
brevoUrl: 'https://app.brevo.com/campaigns',
brevoListingUrl: 'https://app.brevo.com/campaigns',
brevoUrl: 'https://app.brevo.com/campaigns/listing',
brevoListingUrl: 'https://app.brevo.com/campaigns/listing',
message: 'Kampaň bola úspešne aktualizovaná v Brevo!'
});
}
@@ -113,8 +113,8 @@ export async function POST(req: NextRequest) {
success: true,
updated: false,
campaignId: data.id,
brevoUrl: 'https://app.brevo.com/campaigns',
brevoListingUrl: 'https://app.brevo.com/campaigns',
brevoUrl: 'https://app.brevo.com/campaigns/listing',
brevoListingUrl: 'https://app.brevo.com/campaigns/listing',
message: 'Kampaň bola úspešne vytvorená v Brevo!'
});
} catch (error: any) {
+2 -2
View File
@@ -51,8 +51,8 @@ export async function GET(req: NextRequest) {
name: data.name,
status: data.status,
subject: data.subject,
brevoUrl: 'https://app.brevo.com/campaigns',
brevoListingUrl: 'https://app.brevo.com/campaigns'
brevoUrl: 'https://app.brevo.com/campaigns/listing',
brevoListingUrl: 'https://app.brevo.com/campaigns/listing'
});
} catch (err: any) {
console.error('Brevo verify error:', err);
+1 -1
View File
@@ -144,7 +144,7 @@ export default function Home() {
modules: enforceAnchors(dbItem.data.modules)
});
if (dbItem.brevo_campaign_id) {
setBrevoUrl('https://app.brevo.com/campaigns');
setBrevoUrl('https://app.brevo.com/campaigns/listing');
}
}
}
+209 -7
View File
@@ -2,10 +2,19 @@
import React from 'react';
import { NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter';
import { Plus, Trash2, Image as ImageIcon, Tag, Store, Globe } from 'lucide-react';
import { Plus, Trash2, Image as ImageIcon, Tag, Store, Globe, Palette, Check } from 'lucide-react';
import { SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign';
import { ImageUploadInput } from './ImageUploadInput';
const PRODUCT_BG_PRESETS = [
{ label: 'Červená', sub: 'Fantasticky', color: '#feeaea', border: '#f5d4d4' },
{ label: 'Žltá', sub: 'Domáce', color: '#fef0c7', border: '#e6d49a' },
{ label: 'Hnedá', sub: 'Gril', color: '#dcb085', border: '#c49a6c' },
{ label: 'Sivá', sub: 'Neutrálna', color: '#e6e1d9', border: '#d5cfc5' },
{ label: 'Zelená', sub: 'Bio', color: '#e8f5e9', border: '#c8e6c9' },
{ label: 'Biela', sub: 'Čistá', color: '#ffffff', border: '#e5e7eb' },
];
interface ConfigPanelProps {
module: NewsletterModule | null;
onUpdateModule: (updated: NewsletterModule) => void;
@@ -40,11 +49,20 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
return (
<div className="h-full overflow-y-auto p-5 space-y-6 bg-white border-l border-gray-200">
<div className="border-b border-gray-100 pb-3">
<span className="text-[10px] font-bold uppercase tracking-wider text-red-600 bg-red-50 px-2 py-0.5 rounded">
{module.type}
</span>
<h2 className="text-base font-bold text-gray-900 mt-1">{module.title}</h2>
<div className="border-b border-gray-100 pb-3 space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold uppercase tracking-wider text-red-600 bg-red-50 px-2 py-0.5 rounded">
{module.type}
</span>
<span className="text-[10px] text-gray-400 font-medium">Názov modulu v editore</span>
</div>
<input
type="text"
value={module.title}
onChange={(e) => onUpdateModule({ ...module, title: e.target.value })}
placeholder="Názov modulu..."
className="w-full text-base font-bold text-gray-900 border border-gray-200 hover:border-gray-300 focus:border-red-500 rounded-lg px-2.5 py-1.5 bg-gray-50/50 hover:bg-white focus:bg-white focus:outline-none focus:ring-1 focus:ring-red-500 transition-all"
/>
</div>
{/* TOP ANNOUNCEMENT */}
@@ -307,6 +325,99 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
/>
</div>
{/* Farba pozadia skupiny produktov */}
<div className="bg-gray-50 p-3.5 rounded-xl border border-gray-200 space-y-2.5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<Palette className="w-3.5 h-3.5 text-gray-700" />
<label className="text-xs font-bold text-gray-800">Farba pozadia produktov</label>
</div>
<span className="text-[10px] font-mono font-bold px-1.5 py-0.5 bg-white border border-gray-200 rounded text-gray-700 uppercase">
{module.data.cardBgColor || '#feeaea'}
</span>
</div>
{/* Rýchle predvoľby */}
<div className="grid grid-cols-3 gap-1.5">
{PRODUCT_BG_PRESETS.map((preset) => {
const currentBg = (module.data.cardBgColor || '#feeaea').toLowerCase();
const isSelected = currentBg === preset.color.toLowerCase();
return (
<button
key={preset.color}
type="button"
onClick={() => {
const newColor = preset.color;
const newBorder = preset.border;
const updatedProducts = (module.data.products || []).map((p: ProductItem) => ({
...p,
cardBgColor: newColor,
}));
updateData({
cardBgColor: newColor,
cardBorderColor: newBorder,
products: updatedProducts,
});
}}
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-lg border text-[11px] font-medium transition-all text-left ${
isSelected
? 'border-red-600 bg-white shadow-xs ring-1 ring-red-500 font-bold'
: 'border-gray-200 bg-white hover:border-gray-300 text-gray-700'
}`}
>
<span
className="w-3.5 h-3.5 rounded-full border border-black/15 shrink-0 flex items-center justify-center"
style={{ backgroundColor: preset.color }}
>
{isSelected && <Check className="w-2.5 h-2.5 text-gray-800" />}
</span>
<span className="truncate">{preset.label}</span>
</button>
);
})}
</div>
{/* Vlastná farba (Color Picker + HEX input) */}
<div className="flex items-center gap-2 pt-2 border-t border-gray-200">
<span className="text-[10px] text-gray-500 font-semibold shrink-0">Vlastná:</span>
<div className="flex items-center gap-1.5 flex-1">
<input
type="color"
value={module.data.cardBgColor || '#feeaea'}
onChange={(e) => {
const newColor = e.target.value;
const updatedProducts = (module.data.products || []).map((p: ProductItem) => ({
...p,
cardBgColor: newColor,
}));
updateData({
cardBgColor: newColor,
products: updatedProducts,
});
}}
className="w-7 h-7 rounded border border-gray-300 cursor-pointer p-0.5 bg-white shrink-0"
/>
<input
type="text"
value={module.data.cardBgColor || '#feeaea'}
onChange={(e) => {
const newColor = e.target.value;
const updatedProducts = (module.data.products || []).map((p: ProductItem) => ({
...p,
cardBgColor: newColor,
}));
updateData({
cardBgColor: newColor,
products: updatedProducts,
});
}}
placeholder="#feeaea"
className="w-full text-xs font-mono border border-gray-300 rounded px-2 py-1 bg-white uppercase text-gray-800 font-bold"
/>
</div>
</div>
</div>
{/* Zoznam produktov */}
<div className="space-y-4 pt-2">
<div className="flex items-center justify-between">
@@ -324,7 +435,7 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
unitPrice: '1kg = 4,99 €',
discountText: '20%',
badgeText: 'zľava',
cardBgColor: '#feeaea',
cardBgColor: module.data.cardBgColor || '#feeaea',
stores: { potraviny: true, supermarket: true, tempo: true }
};
updateData({ products: [...(module.data.products || []), newP] });
@@ -791,6 +902,97 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
/>
</div>
{/* Farba pozadia vernostných kariet */}
<div className="bg-gray-50 p-3.5 rounded-xl border border-gray-200 space-y-2.5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<Palette className="w-3.5 h-3.5 text-gray-700" />
<label className="text-xs font-bold text-gray-800">Farba pozadia produktov</label>
</div>
<span className="text-[10px] font-mono font-bold px-1.5 py-0.5 bg-white border border-gray-200 rounded text-gray-700 uppercase">
{module.data.cardBgColor || '#e6e1d9'}
</span>
</div>
{/* Rýchle predvoľby */}
<div className="grid grid-cols-3 gap-1.5">
{PRODUCT_BG_PRESETS.map((preset) => {
const currentBg = (module.data.cardBgColor || '#e6e1d9').toLowerCase();
const isSelected = currentBg === preset.color.toLowerCase();
return (
<button
key={preset.color}
type="button"
onClick={() => {
const newColor = preset.color;
const updatedProducts = (module.data.products || []).map((p: any) => ({
...p,
cardBgColor: newColor,
}));
updateData({
cardBgColor: newColor,
products: updatedProducts,
});
}}
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-lg border text-[11px] font-medium transition-all text-left ${
isSelected
? 'border-red-600 bg-white shadow-xs ring-1 ring-red-500 font-bold'
: 'border-gray-200 bg-white hover:border-gray-300 text-gray-700'
}`}
>
<span
className="w-3.5 h-3.5 rounded-full border border-black/15 shrink-0 flex items-center justify-center"
style={{ backgroundColor: preset.color }}
>
{isSelected && <Check className="w-2.5 h-2.5 text-gray-800" />}
</span>
<span className="truncate">{preset.label}</span>
</button>
);
})}
</div>
{/* Vlastná farba (Color Picker + HEX input) */}
<div className="flex items-center gap-2 pt-2 border-t border-gray-200">
<span className="text-[10px] text-gray-500 font-semibold shrink-0">Vlastná:</span>
<div className="flex items-center gap-1.5 flex-1">
<input
type="color"
value={module.data.cardBgColor || '#e6e1d9'}
onChange={(e) => {
const newColor = e.target.value;
const updatedProducts = (module.data.products || []).map((p: any) => ({
...p,
cardBgColor: newColor,
}));
updateData({
cardBgColor: newColor,
products: updatedProducts,
});
}}
className="w-7 h-7 rounded border border-gray-300 cursor-pointer p-0.5 bg-white shrink-0"
/>
<input
type="text"
value={module.data.cardBgColor || '#e6e1d9'}
onChange={(e) => {
const newColor = e.target.value;
const updatedProducts = (module.data.products || []).map((p: any) => ({
...p,
cardBgColor: newColor,
}));
updateData({
cardBgColor: newColor,
products: updatedProducts,
});
}}
placeholder="#e6e1d9"
className="w-full text-xs font-mono border border-gray-300 rounded px-2 py-1 bg-white uppercase text-gray-800 font-bold"
/>
</div>
</div>
</div>
{/* 2 Vernostné produkty */}
<div className="space-y-4 pt-2">
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block">
+13 -2
View File
@@ -212,9 +212,9 @@ export const TopBar: React.FC<TopBarProps> = ({
) : isVerifiedInBrevo && campaign.brevoCampaignId ? (
/* Kampaň je overená priamo v Brevo: Ponúkame úpravu, aktualizáciu alebo odoslanie ako novú */
<div className="flex items-center gap-1.5 bg-emerald-50 border border-emerald-200 p-1 rounded-xl">
{/* Odkaz na úpravu v Brevo */}
{/* Odkaz na zoznam / úpravu v Brevo */}
<a
href="https://app.brevo.com/campaigns"
href={brevoCampaignUrl || brevoListingUrl || 'https://app.brevo.com/campaigns/listing'}
target="_blank"
rel="noreferrer"
className="flex items-center gap-1 text-xs font-bold text-white bg-emerald-600 hover:bg-emerald-700 px-3 py-1.2 rounded-lg transition-all shadow-xs"
@@ -235,6 +235,17 @@ export const TopBar: React.FC<TopBarProps> = ({
<span className="hidden xl:inline">{isExportingBrevo ? 'Ukladám...' : 'Aktualizovať'}</span>
</button>
{/* Tlačidlo na odoslanie ako úplne novej kampane */}
<button
onClick={() => onExportBrevo(false)}
disabled={isExportingBrevo}
className="flex items-center gap-1 text-xs font-bold text-emerald-800 hover:bg-emerald-100 px-2 py-1.2 rounded-lg transition-all"
title="Vytvoriť a odoslať túto kampaň do Brevo ako novú kópiu"
>
<Send className="w-3 h-3 text-emerald-700" />
<span className="hidden 2xl:inline">Ako novú</span>
</button>
{/* Tlačidlo na overenie stavu */}
<button
onClick={() => verifyCampaignInBrevo()}
+94 -71
View File
@@ -273,7 +273,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
<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-radius: ${m.data.title || m.data.description || m.data.buttonText ? '16px 16px 0 0' : '16px'};">
<img src="${escapeHtml(m.data.imageUrl)}" width="600" alt="${escapeHtml(m.data.altText || m.data.title || m.title || 'COOP Jednota')}" 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>
@@ -315,10 +315,10 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 12px;">
<tr>
${row.map((p, idx) => `
<td width="50%" valign="top" height="1" style="padding-${idx === 0 ? 'right: 6px' : 'left: 6px'}; padding-bottom: 0;">
${renderExactProductCard(p, globalValidity, cardBg, cardBorder, badgeHdrBg, badgeHdrColor, badgePrcBg, badgePrcColor, badgeCustomTxt)}
<td width="50%" valign="top" height="100%" style="padding-${idx === 0 ? 'right: 6px' : 'left: 6px'}; padding-bottom: 0; height: 100%;">
${renderExactProductCard(p, globalValidity, cardBg, cardBorder, badgeHdrBg, badgeHdrColor, badgePrcBg, badgePrcColor, badgeCustomTxt, m.data.sectionTitle)}
</td>`).join('\n')}
${row.length === 1 ? '<td width="50%" valign="top"></td>' : ''}
${row.length === 1 ? '<td width="50%" valign="top" height="100%" style="height: 100%;"></td>' : ''}
</tr>
</table>
`).join('\n');
@@ -359,7 +359,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
<tr>
<td>
<a href="${escapeHtml(m.data.headerLink || '#')}">
<img src="${escapeHtml(m.data.headerImageUrl)}" width="600" alt="Vernostné zľavy" style="display: block; border: 0; width: 100%; max-width: 100%; border-radius: 16px;">
<img src="${escapeHtml(m.data.headerImageUrl)}" width="600" alt="${escapeHtml(m.data.title || m.data.sectionTitle || 'Vernostné zľavy')}" style="display: block; border: 0; width: 100%; max-width: 100%; border-radius: 16px;">
</a>
</td>
</tr>
@@ -372,7 +372,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
${lProducts.map((p: any, idx: number) => `
<td width="50%" valign="top" height="1" style="padding-${idx === 0 ? 'right: 6px' : 'left: 6px'}; padding-bottom: 0;">
<td width="50%" valign="top" height="100%" style="padding-${idx === 0 ? 'right: 6px' : 'left: 6px'}; padding-bottom: 0; height: 100%;">
${renderExactLoyaltyCard(p, m.data.globalValidity || '2. 5. – 15. 5. 2026')}
</td>`).join('\n')}
</tr>
@@ -393,22 +393,32 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<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: 0; border-radius: 16px; overflow: hidden; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);">
<tr>
<td>
<td width="50%" valign="top" height="100%" style="padding-right: 6px; height: 100%;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="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); height: 100%; min-height: 100%;">
<tr height="1">
<td valign="top" style="line-height: 0; height: 1px;">
<a href="${escapeHtml(it1.linkUrl || '#')}">
<img src="${escapeHtml(it1.imageUrl)}" width="300" alt="${escapeHtml(it1.title)}" style="display: block; border: 0; width: 100%; max-width: 100%;">
<img src="${escapeHtml(it1.imageUrl)}" width="300" alt="${escapeHtml(it1.title || m.data.title || m.title || 'Aktuálny leták')}" style="display: block; border: 0; width: 100%; max-width: 100%;">
</a>
</td>
</tr>
<tr>
<td style="padding: 25px 15px 30px 15px; text-align: center;">
<h3 class="force-text-white" style="margin: 0 0 15px 0; font-size: 17px; color: ${it1.textColor || '#fefefe'}; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;">${escapeHtml(it1.title)}</h3>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;" class="btn-table">
<td valign="bottom" style="padding: 20px 15px 25px 15px; text-align: center; height: 100%;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%" style="height: 100%;">
<tr>
<td style="text-align: center;">
<a href="${escapeHtml(it1.linkUrl || '#')}" class="btn force-text-white" style="background-color: ${it1.btnBgColor || '#e2001a'}; color: #fefefe; font-family: 'Poppins', sans-serif; font-size: 12px; font-weight: 800; text-decoration: none; padding: 10px 24px; border-radius: 25px; display: inline-block; text-transform: uppercase; letter-spacing: 0.5px; box-shadow: 0 4px 12px rgba(226, 0, 26, 0.35);">${escapeHtml(it1.btnText)}</a>
<td valign="top" style="text-align: center; min-height: 44px; padding-bottom: 15px;">
<h3 class="force-text-white" style="margin: 0; font-size: 17px; color: ${it1.textColor || '#fefefe'}; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; line-height: 1.25;">${escapeHtml(it1.title)}</h3>
</td>
</tr>
<tr height="1">
<td valign="bottom" align="center" style="text-align: center; height: 1px;">
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;" class="btn-table">
<tr>
<td style="text-align: center;">
<a href="${escapeHtml(it1.linkUrl || '#')}" class="btn force-text-white" style="background-color: ${it1.btnBgColor || '#e2001a'}; color: #fefefe; font-family: 'Poppins', sans-serif; font-size: 12px; font-weight: 800; text-decoration: none; padding: 10px 24px; border-radius: 25px; display: inline-block; text-transform: uppercase; letter-spacing: 0.5px; box-shadow: 0 4px 12px rgba(226, 0, 26, 0.35);">${escapeHtml(it1.btnText)}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
@@ -417,22 +427,32 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
</table>
</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: 0; border-radius: 16px; overflow: hidden; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);">
<tr>
<td>
<td width="50%" valign="top" height="100%" style="padding-left: 6px; height: 100%;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="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); height: 100%; min-height: 100%;">
<tr height="1">
<td valign="top" style="line-height: 0; height: 1px;">
<a href="${escapeHtml(it2.linkUrl || '#')}">
<img src="${escapeHtml(it2.imageUrl)}" width="300" alt="${escapeHtml(it2.title)}" style="display: block; border: 0; width: 100%; max-width: 100%;">
<img src="${escapeHtml(it2.imageUrl)}" width="300" alt="${escapeHtml(it2.title || m.data.title || m.title || 'Aktuálny leták')}" style="display: block; border: 0; width: 100%; max-width: 100%;">
</a>
</td>
</tr>
<tr>
<td style="padding: 25px 15px 30px 15px; text-align: center;">
<h3 style="margin: 0 0 15px 0; font-size: 17px; color: ${it2.textColor || '#29446f'}; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;">${escapeHtml(it2.title)}</h3>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;" class="btn-table">
<td valign="bottom" style="padding: 20px 15px 25px 15px; text-align: center; height: 100%;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%" style="height: 100%;">
<tr>
<td style="text-align: center;">
<a href="${escapeHtml(it2.linkUrl || '#')}" class="btn force-text-white" style="background-color: ${it2.btnBgColor || '#29446f'}; color: #fefefe; font-family: 'Poppins', sans-serif; font-size: 12px; font-weight: 800; text-decoration: none; padding: 10px 24px; border-radius: 25px; display: inline-block; text-transform: uppercase; letter-spacing: 0.5px; box-shadow: 0 4px 12px rgba(41, 68, 111, 0.3);">${escapeHtml(it2.btnText)}</a>
<td valign="top" style="text-align: center; min-height: 44px; padding-bottom: 15px;">
<h3 style="margin: 0; font-size: 17px; color: ${it2.textColor || '#29446f'}; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; line-height: 1.25;">${escapeHtml(it2.title)}</h3>
</td>
</tr>
<tr height="1">
<td valign="bottom" align="center" style="text-align: center; height: 1px;">
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;" class="btn-table">
<tr>
<td style="text-align: center;">
<a href="${escapeHtml(it2.linkUrl || '#')}" class="btn force-text-white" style="background-color: ${it2.btnBgColor || '#29446f'}; color: #fefefe; font-family: 'Poppins', sans-serif; font-size: 12px; font-weight: 800; text-decoration: none; padding: 10px 24px; border-radius: 25px; display: inline-block; text-transform: uppercase; letter-spacing: 0.5px; box-shadow: 0 4px 12px rgba(41, 68, 111, 0.3);">${escapeHtml(it2.btnText)}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
@@ -451,10 +471,10 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<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: 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;">
<td width="50%" valign="top" height="100%" style="padding-right: 6px; padding-bottom: 20px; height: 100%;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="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); height: 100%; min-height: 100%;">
<tr height="240">
<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; height: 240px;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:300px;height:240px;">
<v:fill type="frame" src="${escapeHtml(it1.imageUrl)}" color="${it1.bgColor}" />
@@ -464,7 +484,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%">
<tr>
<td align="center" valign="bottom" style="padding-bottom: 10px;">
<img src="${escapeHtml(it1.badgeImgUrl)}" width="70" alt="Recepty" style="display: block; border: 0; margin: 0 auto;">
<img src="${escapeHtml(it1.badgeImgUrl)}" width="70" alt="${escapeHtml(it1.title || 'Recepty')}" style="display: block; border: 0; margin: 0 auto;">
</td>
</tr>
</table>
@@ -473,17 +493,17 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
</td>
</tr>
<tr>
<td style="padding: 10px 15px 25px 15px; text-align: center; height: 60px;" valign="top">
<h3 class="force-text-white" style="margin: 0; font-size: 17px; color: #fefefe; font-weight: 800; text-transform: uppercase; letter-spacing: 0.8px; line-height: 1.2;">${it1.title}</h3>
<td style="padding: 15px 15px 25px 15px; text-align: center; height: 100%;" valign="middle">
<h3 class="force-text-white" style="margin: 0; font-size: 17px; color: #fefefe; font-weight: 800; text-transform: uppercase; letter-spacing: 0.8px; line-height: 1.25;">${escapeHtml(it1.title)}</h3>
</td>
</tr>
</table>
</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: 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;">
<td width="50%" valign="top" height="100%" style="padding-left: 6px; padding-bottom: 20px; height: 100%;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="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); height: 100%; min-height: 100%;">
<tr height="240">
<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; height: 240px;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:300px;height:240px;">
<v:fill type="frame" src="${escapeHtml(it2.imageUrl)}" color="${it2.bgColor}" />
@@ -493,7 +513,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%">
<tr>
<td align="center" valign="bottom" style="padding-bottom: 10px;">
<img src="${escapeHtml(it2.badgeImgUrl)}" width="70" alt="Časopis Jednota" style="display: block; border: 0; margin: 0 auto;">
<img src="${escapeHtml(it2.badgeImgUrl)}" width="70" alt="${escapeHtml(it2.title || 'Časopis Jednota')}" style="display: block; border: 0; margin: 0 auto;">
</td>
</tr>
</table>
@@ -502,8 +522,8 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
</td>
</tr>
<tr>
<td style="padding: 10px 15px 25px 15px; text-align: center; height: 60px;" valign="top">
<h3 class="force-text-white" style="margin: 0; font-size: 17px; color: #fefefe; font-weight: 800; text-transform: uppercase; letter-spacing: 0.8px; line-height: 1.2;">${it2.title}</h3>
<td style="padding: 15px 15px 25px 15px; text-align: center; height: 100%;" valign="middle">
<h3 class="force-text-white" style="margin: 0; font-size: 17px; color: #fefefe; font-weight: 800; text-transform: uppercase; letter-spacing: 0.8px; line-height: 1.25;">${escapeHtml(it2.title)}</h3>
</td>
</tr>
</table>
@@ -586,21 +606,23 @@ function renderExactProductCard(
badgeHdrColor: string,
badgePrcBg: string,
badgePrcColor: string,
badgeCustomTxt: string | null
badgeCustomTxt: string | null,
sectionTitle?: string
): string {
const validity = p.validityOverride || globalValidity;
const effectiveCardBg = p.cardBgColor || cardBg || '#feeaea';
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: 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>` : ''}
${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="Potraviny" 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="Supermarket" 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="Tempo" 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: 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%" height="100%" class="glass-product" bgcolor="${effectiveCardBg}" style="background-color: ${effectiveCardBg}; border: 0; border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); height: 100%; min-height: 100%;">
<tr height="1">
<td style="padding: 15px 15px 0 15px; height: 1px;" 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>
@@ -609,14 +631,14 @@ function renderExactProductCard(
</table>
</td>
</tr>
<tr>
<tr height="1">
<td style="padding: 0 8px 15px 8px; text-align: center; height: 160px;" valign="middle">
<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(p.title)}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">
<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(p.title || sectionTitle || 'Produkt')}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">
</td>
</tr>
<tr>
<td style="padding: 0 8px 10px 8px; text-align: center; height: 50px;" valign="top">
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600;">${escapeHtml(p.title)}</h3>
<td style="padding: 0 8px 10px 8px; text-align: center; height: 100%; min-height: 44px;" valign="top">
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>
</td>
</tr>
<tr height="1">
@@ -646,7 +668,7 @@ function renderExactProductCard(
${p.showSlovakFlag ? `
<tr>
<td align="right" style="line-height: 0; padding: 0;">
<img src="${SUPABASE_ASSETS_BASE}vlajka_srdce.png" width="40" alt="SK" style="display: block; margin-top: -26px; margin-right: -10px; padding: 4px; filter: drop-shadow(1px 0 0 #fefefe) drop-shadow(-1px 0 0 #fefefe) drop-shadow(0 1px 0 #fefefe) drop-shadow(0 -1px 0 #fefefe);">
<img src="${SUPABASE_ASSETS_BASE}vlajka_srdce.png" width="40" alt="Slovenský výrobok" style="display: block; margin-top: -26px; margin-right: -10px; padding: 4px; filter: drop-shadow(1px 0 0 #fefefe) drop-shadow(-1px 0 0 #fefefe) drop-shadow(0 1px 0 #fefefe) drop-shadow(0 -1px 0 #fefefe);">
</td>
</tr>` : ''}
</table>`;
@@ -658,7 +680,7 @@ function renderExactProductCard(
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="${bAlign}" style="${marginStyle} width: ${bWidth}px; padding-top: ${padTop}px; padding-bottom: ${padBottom}px;">
<tr>
<td align="${bAlign}">
<img src="${escapeHtml(p.customBadgePngUrl)}" width="${bWidth}" alt="Cenovka" style="display: block; width: 100%; max-width: ${bWidth}px; margin: 0 auto;">
<img src="${escapeHtml(p.customBadgePngUrl)}" width="${bWidth}" alt="${escapeHtml(p.title || 'Cenovka')}" style="display: block; width: 100%; max-width: ${bWidth}px; margin: 0 auto;">
</td>
</tr>
</table>`;
@@ -696,15 +718,15 @@ function renderExactProductCard(
${p.showSlovakFlag ? `
<tr>
<td align="right" style="line-height: 0; padding: 0;">
<img src="${SUPABASE_ASSETS_BASE}vlajka_srdce.png" width="40" alt="SK" style="display: block; margin-top: -26px; margin-right: -10px; padding: 4px; filter: drop-shadow(1px 0 0 #fefefe) drop-shadow(-1px 0 0 #fefefe) drop-shadow(0 1px 0 #fefefe) drop-shadow(0 -1px 0 #fefefe);">
<img src="${SUPABASE_ASSETS_BASE}vlajka_srdce.png" width="40" alt="Slovenský výrobok" style="display: block; margin-top: -26px; margin-right: -10px; padding: 4px; filter: drop-shadow(1px 0 0 #fefefe) drop-shadow(-1px 0 0 #fefefe) drop-shadow(0 1px 0 #fefefe) drop-shadow(0 -1px 0 #fefefe);">
</td>
</tr>` : ''}
</table>`;
})()}
</td>
</tr>
<tr>
<td align="center" style="padding: 2px 8px 12px 8px; text-align: center;">
<tr height="1">
<td align="center" valign="bottom" style="padding: 2px 8px 12px 8px; text-align: center; height: 1px;">
${p.unitPrice ? `<div style="font-size: 9px; color: #666666; font-weight: 500; font-family: Arial, sans-serif; text-align: center;">${escapeHtml(p.unitPrice)}</div>` : '<div style="font-size: 9px; line-height: 9px; height: 9px;">&nbsp;</div>'}
</td>
</tr>
@@ -715,15 +737,16 @@ 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: 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>` : ''}
${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="Potraviny" 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="Supermarket" 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="Tempo" style="display: block; height: 10px; width: auto; border: 0;"></td></tr></table></td></tr>` : ''}
</table>`;
const loyaltyBg = p.cardBgColor || '#e6e1d9';
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: 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%" height="100%" class="glass-product card-vernostna" bgcolor="${loyaltyBg}" style="background-color: ${loyaltyBg}; border: 0; border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); height: 100%; min-height: 100%;">
<tr height="1">
<td style="padding: 15px 15px 0 15px; height: 1px;" 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(cardValidity)}</td>
@@ -732,18 +755,18 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
</table>
</td>
</tr>
<tr>
<tr height="1">
<td style="padding: 0 8px 15px 8px; text-align: center; height: 160px;" valign="middle">
<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(p.title)}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">
<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(p.title || 'Produkt')}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">
</td>
</tr>
<tr>
<td style="padding: 0 8px 10px 8px; text-align: center; height: 50px;" valign="top">
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600;">${escapeHtml(p.title)}</h3>
<td style="padding: 0 8px 10px 8px; text-align: center; height: 100%; min-height: 44px;" valign="top">
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>
</td>
</tr>
<tr height="1">
<td style="padding: 0 8px ${p.badgeCustomTemplate ? '4px' : '12px'} 8px; text-align: center;" valign="bottom">
<td style="padding: 0 8px ${p.badgeCustomTemplate ? '4px' : '12px'} 8px; text-align: center; height: 1px;" valign="bottom">
${(() => {
// 1. Ak má vernostný produkt vlastnú dynamickú šablónu cenovky
if (p.badgeCustomTemplate) {
@@ -776,7 +799,7 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
${p.showSlovakFlag ? `
<tr>
<td align="right" style="line-height: 0; padding: 0;">
<img src="${SUPABASE_ASSETS_BASE}vlajka_srdce.png" width="40" alt="SK" style="display: block; margin-top: -26px; margin-right: -10px; padding: 4px; filter: drop-shadow(1px 0 0 #fefefe) drop-shadow(-1px 0 0 #fefefe) drop-shadow(0 1px 0 #fefefe) drop-shadow(0 -1px 0 #fefefe);">
<img src="${SUPABASE_ASSETS_BASE}vlajka_srdce.png" width="40" alt="Slovenský výrobok" style="display: block; margin-top: -26px; margin-right: -10px; padding: 4px; filter: drop-shadow(1px 0 0 #fefefe) drop-shadow(-1px 0 0 #fefefe) drop-shadow(0 1px 0 #fefefe) drop-shadow(0 -1px 0 #fefefe);">
</td>
</tr>` : ''}
</table>`;
@@ -831,7 +854,7 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
${p.showSlovakFlag ? `
<tr>
<td colspan="2" align="right" style="line-height: 0; padding: 0;">
<img src="${SUPABASE_ASSETS_BASE}vlajka_srdce.png" width="40" alt="SK" style="display: block; margin-top: -26px; margin-right: -10px; padding: 4px; filter: drop-shadow(1px 0 0 #fefefe) drop-shadow(-1px 0 0 #fefefe) drop-shadow(0 1px 0 #fefefe) drop-shadow(0 -1px 0 #fefefe);">
<img src="${SUPABASE_ASSETS_BASE}vlajka_srdce.png" width="40" alt="Slovenský výrobok" style="display: block; margin-top: -26px; margin-right: -10px; padding: 4px; filter: drop-shadow(1px 0 0 #fefefe) drop-shadow(-1px 0 0 #fefefe) drop-shadow(0 1px 0 #fefefe) drop-shadow(0 -1px 0 #fefefe);">
</td>
</tr>` : ''}
<tr>
@@ -847,8 +870,8 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
</td>
</tr>
${p.badgeCustomTemplate ? `
<tr>
<td align="center" style="padding: 2px 8px 12px 8px; text-align: center;">
<tr height="1">
<td align="center" valign="bottom" style="padding: 2px 8px 12px 8px; text-align: center; height: 1px;">
${p.cardUnitPrice || p.unitPrice ? `<div style="font-size: 9px; color: #666666; font-weight: 500; font-family: Arial, sans-serif; text-align: center;">${escapeHtml(p.cardUnitPrice || p.unitPrice)}</div>` : '<div style="font-size: 9px; line-height: 9px; height: 9px;">&nbsp;</div>'}
</td>
</tr>` : ''}
+2
View File
@@ -90,6 +90,8 @@ export interface ProductsGridModuleData {
sectionTitle: string;
globalValidity: string;
products: ProductItem[];
cardBgColor?: string;
cardBorderColor?: string;
}
export interface LoyaltyClubModuleData {