Feat: adjustable superscript index, loyalty badges, centered unit price, global badge apply, brevo url fix
This commit is contained in:
1 parent
245e3f8471
commit
4e4e5e5af3
10 files changed
+461
-212
No files matched your search
@@ -63,8 +63,8 @@ export async function POST(req: NextRequest) {
|
|||||||
success: true,
|
success: true,
|
||||||
updated: true,
|
updated: true,
|
||||||
campaignId: Number(existingCampaignId),
|
campaignId: Number(existingCampaignId),
|
||||||
brevoUrl: `https://app.brevo.com/campaigns/email/edit/${existingCampaignId}`,
|
brevoUrl: 'https://app.brevo.com/campaigns',
|
||||||
brevoListingUrl: 'https://app.brevo.com/campaigns/listing',
|
brevoListingUrl: 'https://app.brevo.com/campaigns',
|
||||||
message: 'Kampaň bola úspešne aktualizovaná v Brevo!'
|
message: 'Kampaň bola úspešne aktualizovaná v Brevo!'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -113,8 +113,8 @@ export async function POST(req: NextRequest) {
|
|||||||
success: true,
|
success: true,
|
||||||
updated: false,
|
updated: false,
|
||||||
campaignId: data.id,
|
campaignId: data.id,
|
||||||
brevoUrl: `https://app.brevo.com/campaigns/email/edit/${data.id}`,
|
brevoUrl: 'https://app.brevo.com/campaigns',
|
||||||
brevoListingUrl: 'https://app.brevo.com/campaigns/listing',
|
brevoListingUrl: 'https://app.brevo.com/campaigns',
|
||||||
message: 'Kampaň bola úspešne vytvorená v Brevo!'
|
message: 'Kampaň bola úspešne vytvorená v Brevo!'
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export async function GET(req: NextRequest) {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
subject: data.subject,
|
subject: data.subject,
|
||||||
brevoUrl: `https://app.brevo.com/campaigns/email/edit/${data.id}`,
|
brevoUrl: 'https://app.brevo.com/campaigns',
|
||||||
brevoListingUrl: 'https://app.brevo.com/campaigns/listing'
|
brevoListingUrl: 'https://app.brevo.com/campaigns'
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Brevo verify error:', err);
|
console.error('Brevo verify error:', err);
|
||||||
|
|||||||
+37
-8
@@ -144,7 +144,7 @@ export default function Home() {
|
|||||||
modules: enforceAnchors(dbItem.data.modules)
|
modules: enforceAnchors(dbItem.data.modules)
|
||||||
});
|
});
|
||||||
if (dbItem.brevo_campaign_id) {
|
if (dbItem.brevo_campaign_id) {
|
||||||
setBrevoUrl(`https://app.brevo.com/campaigns/email/edit/${dbItem.brevo_campaign_id}`);
|
setBrevoUrl('https://app.brevo.com/campaigns');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,14 +522,14 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Vybraný produkt pre editor cenoviek
|
// Vybraný produkt pre editor cenoviek
|
||||||
let targetProduct: ProductItem | null = null;
|
let targetProduct: any = null;
|
||||||
if (targetProductIdForBadge && selectedModule?.data?.products) {
|
if (targetProductIdForBadge && selectedModule?.data?.products) {
|
||||||
targetProduct = selectedModule.data.products.find((p: any) => p.id === targetProductIdForBadge) || null;
|
targetProduct = selectedModule.data.products.find((p: any) => p.id === targetProductIdForBadge) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleApplyBadgeToProduct = (badge: BadgeTemplate, renderedPngUrl?: string) => {
|
const handleApplyBadgeToProduct = (badge: BadgeTemplate, renderedPngUrl?: string) => {
|
||||||
if (!targetProductIdForBadge || !selectedModule) return;
|
if (!targetProductIdForBadge || !selectedModule) return;
|
||||||
const updatedProducts = (selectedModule.data.products || []).map((p: ProductItem) => {
|
const updatedProducts = (selectedModule.data.products || []).map((p: any) => {
|
||||||
if (p.id === targetProductIdForBadge) {
|
if (p.id === targetProductIdForBadge) {
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
@@ -561,6 +561,34 @@ export default function Home() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Hromadná aplikácia cenovky na všetky produkty v celej kampani (všetky sekcie + vernostné zľavy)
|
||||||
|
const handleApplyBadgeToAll = (badge: BadgeTemplate) => {
|
||||||
|
setCampaign((prev) => ({
|
||||||
|
...prev,
|
||||||
|
modules: prev.modules.map((m) => {
|
||||||
|
if (m.type === 'products_grid' || m.type === 'loyalty_club') {
|
||||||
|
return {
|
||||||
|
...m,
|
||||||
|
data: {
|
||||||
|
...m.data,
|
||||||
|
products: (m.data.products || []).map((prod: any) => ({
|
||||||
|
...prod,
|
||||||
|
badgeId: badge.id,
|
||||||
|
badgeCustomTemplate: badge,
|
||||||
|
badgeWidth: badge.width ?? prod.badgeWidth ?? 110,
|
||||||
|
badgeAlign: badge.align ?? prod.badgeAlign ?? 'center',
|
||||||
|
badgePaddingTop: badge.paddingTop ?? prod.badgePaddingTop ?? 4,
|
||||||
|
badgePaddingBottom: badge.paddingBottom ?? prod.badgePaddingBottom ?? 4,
|
||||||
|
customBadgePngUrl: undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const topCount = campaign.modules.filter((m) => isTopModule(m)).length;
|
const topCount = campaign.modules.filter((m) => isTopModule(m)).length;
|
||||||
const firstBottomIdx = campaign.modules.findIndex((m) => isBottomModule(m));
|
const firstBottomIdx = campaign.modules.findIndex((m) => isBottomModule(m));
|
||||||
|
|
||||||
@@ -722,6 +750,7 @@ export default function Home() {
|
|||||||
<ConfigPanel
|
<ConfigPanel
|
||||||
module={selectedModule}
|
module={selectedModule}
|
||||||
onUpdateModule={handleUpdateModule}
|
onUpdateModule={handleUpdateModule}
|
||||||
|
onApplyBadgeToAll={handleApplyBadgeToAll}
|
||||||
availableBadges={availableBadges}
|
availableBadges={availableBadges}
|
||||||
onOpenBadgeManager={(pId) => {
|
onOpenBadgeManager={(pId) => {
|
||||||
setTargetProductIdForBadge(pId);
|
setTargetProductIdForBadge(pId);
|
||||||
@@ -744,10 +773,10 @@ export default function Home() {
|
|||||||
isOpen={isBadgeEditorOpen}
|
isOpen={isBadgeEditorOpen}
|
||||||
onClose={() => setIsBadgeEditorOpen(false)}
|
onClose={() => setIsBadgeEditorOpen(false)}
|
||||||
onSelectBadge={handleApplyBadgeToProduct}
|
onSelectBadge={handleApplyBadgeToProduct}
|
||||||
currentDiscount={targetProduct?.discountText || '25%'}
|
currentDiscount={targetProduct?.discountText || targetProduct?.cardDiscount || '25%'}
|
||||||
currentPriceMain={targetProduct?.priceMain || '1'}
|
currentPriceMain={targetProduct?.priceMain || targetProduct?.cardPriceMain || '1'}
|
||||||
currentPriceSub={targetProduct?.priceSub || '99'}
|
currentPriceSub={targetProduct?.priceSub || targetProduct?.cardPriceSub || '99'}
|
||||||
currentUnitPrice={targetProduct?.unitPrice || '1kg = 4,91 €'}
|
currentUnitPrice={targetProduct?.unitPrice || targetProduct?.cardUnitPrice || '1kg = 4,91 €'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modál Moje newslettre (Ukladanie, duplikácia, vytváranie a prepínanie) */}
|
{/* Modál Moje newslettre (Ukladanie, duplikácia, vytváranie a prepínanie) */}
|
||||||
@@ -759,7 +788,7 @@ export default function Home() {
|
|||||||
setCampaign({ ...selected, modules: enforceAnchors(selected.modules) });
|
setCampaign({ ...selected, modules: enforceAnchors(selected.modules) });
|
||||||
setSelectedModuleId(selected.modules[1]?.id || selected.modules[0]?.id || null);
|
setSelectedModuleId(selected.modules[1]?.id || selected.modules[0]?.id || null);
|
||||||
if (selected.brevoCampaignId) {
|
if (selected.brevoCampaignId) {
|
||||||
setBrevoUrl(`https://app.brevo.com/campaigns/email/edit/${selected.brevoCampaignId}`);
|
setBrevoUrl('https://app.brevo.com/campaigns');
|
||||||
} else {
|
} else {
|
||||||
setBrevoUrl(null);
|
setBrevoUrl(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,20 +226,11 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
// Doplnenie predpripravených dizajnových šablón (DEFAULT_BADGE_TEMPLATES)
|
setBadges(loaded);
|
||||||
const allBadges = [...loaded];
|
if (!activeBadge && loaded.length > 0) {
|
||||||
const existingIds = new Set(allBadges.map((b) => b.id));
|
setActiveBadge(loaded[0]);
|
||||||
for (const preset of DEFAULT_BADGE_TEMPLATES) {
|
if (loaded[0].layersConfig?.[0]) {
|
||||||
if (!existingIds.has(preset.id)) {
|
setSelectedLayerId(loaded[0].layersConfig[0].id);
|
||||||
allBadges.push(preset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setBadges(allBadges);
|
|
||||||
if (!activeBadge && allBadges.length > 0) {
|
|
||||||
setActiveBadge(allBadges[0]);
|
|
||||||
if (allBadges[0].layersConfig?.[0]) {
|
|
||||||
setSelectedLayerId(allBadges[0].layersConfig[0].id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -452,15 +443,28 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
// Vykreslenie vrstvy v React náhľade s centami ako horný index (sup) a BEZ znaku €
|
// Vykreslenie vrstvy v React náhľade s centami ako horný index (sup) a BEZ znaku €
|
||||||
const renderLayerVisualContent = (layer: BadgeLayer) => {
|
const renderLayerVisualContent = (layer: BadgeLayer) => {
|
||||||
const raw = layer.text || '';
|
const raw = layer.text || '';
|
||||||
|
const baseFontSize = layer.fontSize || 16;
|
||||||
|
const subFontSize = layer.indexFontSize ? `${layer.indexFontSize}px` : `${Math.max(10, Math.round(baseFontSize * 0.55))}px`;
|
||||||
|
const subY = layer.indexOffsetY !== undefined ? `${layer.indexOffsetY}px` : '-10px';
|
||||||
|
const subX = layer.indexOffsetX !== undefined ? `${layer.indexOffsetX}px` : '2px';
|
||||||
|
|
||||||
if (raw.includes('{cena}')) {
|
if (raw.includes('{cena}')) {
|
||||||
const parts = raw.split('{cena}');
|
const parts = raw.split('{cena}');
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{parts[0]}
|
{parts[0]}
|
||||||
<span>{currentPriceMain}</span>
|
<span>{currentPriceMain}</span>
|
||||||
<sup style={{ fontSize: '0.55em', verticalAlign: 'top', lineHeight: 1.1, marginLeft: '1px' }}>
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
fontSize: subFontSize,
|
||||||
|
transform: `translate(${subX}, ${subY})`,
|
||||||
|
fontWeight: layer.fontWeight || '700',
|
||||||
|
lineHeight: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
{currentPriceSub}
|
{currentPriceSub}
|
||||||
</sup>
|
</span>
|
||||||
{parts[1]}
|
{parts[1]}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -471,7 +475,17 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{parts[0]}
|
{parts[0]}
|
||||||
<sup style={{ fontSize: '0.55em', verticalAlign: 'top', lineHeight: 1.1 }}>{currentPriceSub}</sup>
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
fontSize: subFontSize,
|
||||||
|
transform: `translate(${subX}, ${subY})`,
|
||||||
|
fontWeight: layer.fontWeight || '700',
|
||||||
|
lineHeight: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentPriceSub}
|
||||||
|
</span>
|
||||||
{parts[1]}
|
{parts[1]}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -1101,6 +1115,74 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Nastavenie veľkosti a pozície horného indexu (centov) */}
|
||||||
|
{(activeLayer.text?.includes('{cena}') || activeLayer.text?.includes('{centy}')) && (
|
||||||
|
<div className="pt-2.5 pb-1 border-t border-blue-200/80 bg-blue-50/70 p-3 rounded-xl border border-blue-100 space-y-2.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-[11px] font-bold text-blue-900">
|
||||||
|
Nastavenie horného indexu (Centov)
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-blue-700 bg-blue-100 px-1.5 py-0.5 rounded font-mono font-bold">
|
||||||
|
{activeLayer.indexFontSize ?? Math.max(10, Math.round((activeLayer.fontSize || 16) * 0.55))}px
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Veľkosť indexu */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center text-[10px] text-gray-600 mb-0.5">
|
||||||
|
<span>Veľkosť písma indexu</span>
|
||||||
|
<span className="font-bold text-gray-800">
|
||||||
|
{activeLayer.indexFontSize ?? Math.max(10, Math.round((activeLayer.fontSize || 16) * 0.55))} px
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="8"
|
||||||
|
max="40"
|
||||||
|
value={activeLayer.indexFontSize ?? Math.max(10, Math.round((activeLayer.fontSize || 16) * 0.55))}
|
||||||
|
onChange={(e) => updateActiveLayer({ indexFontSize: Number(e.target.value) })}
|
||||||
|
className="w-full accent-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vertikálny posun (Hore / Dole) */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center text-[10px] text-gray-600 mb-0.5">
|
||||||
|
<span>Vertikálna pozícia (Posun Hore / Dole)</span>
|
||||||
|
<span className="font-bold text-gray-800">
|
||||||
|
{activeLayer.indexOffsetY ?? -10} px
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="-30"
|
||||||
|
max="20"
|
||||||
|
value={activeLayer.indexOffsetY ?? -10}
|
||||||
|
onChange={(e) => updateActiveLayer({ indexOffsetY: Number(e.target.value) })}
|
||||||
|
className="w-full accent-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Horizontálna medzera (Vľavo / Vpravo) */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center text-[10px] text-gray-600 mb-0.5">
|
||||||
|
<span>Horizontálna medzera (Vľavo / Vpravo)</span>
|
||||||
|
<span className="font-bold text-gray-800">
|
||||||
|
{activeLayer.indexOffsetX ?? 2} px
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="-10"
|
||||||
|
max="25"
|
||||||
|
value={activeLayer.indexOffsetX ?? 2}
|
||||||
|
onChange={(e) => updateActiveLayer({ indexOffsetX: Number(e.target.value) })}
|
||||||
|
className="w-full accent-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="pt-1 flex justify-between items-center">
|
<div className="pt-1 flex justify-between items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => removeLayer(activeLayer.id)}
|
onClick={() => removeLayer(activeLayer.id)}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter';
|
import { NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter';
|
||||||
import { Plus, Trash2, Image as ImageIcon, Tag, Store } from 'lucide-react';
|
import { Plus, Trash2, Image as ImageIcon, Tag, Store, Globe } from 'lucide-react';
|
||||||
import { SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign';
|
import { SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign';
|
||||||
import { ImageUploadInput } from './ImageUploadInput';
|
import { ImageUploadInput } from './ImageUploadInput';
|
||||||
|
|
||||||
interface ConfigPanelProps {
|
interface ConfigPanelProps {
|
||||||
module: NewsletterModule | null;
|
module: NewsletterModule | null;
|
||||||
onUpdateModule: (updated: NewsletterModule) => void;
|
onUpdateModule: (updated: NewsletterModule) => void;
|
||||||
|
onApplyBadgeToAll?: (badge: BadgeTemplate) => void;
|
||||||
onOpenBadgeManager?: (productId: string) => void;
|
onOpenBadgeManager?: (productId: string) => void;
|
||||||
onOpenImport?: () => void;
|
onOpenImport?: () => void;
|
||||||
availableBadges?: BadgeTemplate[];
|
availableBadges?: BadgeTemplate[];
|
||||||
@@ -17,6 +18,7 @@ interface ConfigPanelProps {
|
|||||||
export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||||
module,
|
module,
|
||||||
onUpdateModule,
|
onUpdateModule,
|
||||||
|
onApplyBadgeToAll,
|
||||||
onOpenBadgeManager,
|
onOpenBadgeManager,
|
||||||
onOpenImport,
|
onOpenImport,
|
||||||
availableBadges = []
|
availableBadges = []
|
||||||
@@ -567,8 +569,9 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tlačidlo: Aplikovať túto cenovku na všetky produkty v sekcii */}
|
{/* Tlačidlá aplikácie cenovky */}
|
||||||
{p.badgeCustomTemplate && (
|
{p.badgeCustomTemplate && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -588,8 +591,29 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
}}
|
}}
|
||||||
className="w-full py-1.5 text-[11px] font-bold text-slate-700 bg-slate-100 hover:bg-slate-200 border border-slate-300 rounded-lg transition-colors text-center"
|
className="w-full py-1.5 text-[11px] font-bold text-slate-700 bg-slate-100 hover:bg-slate-200 border border-slate-300 rounded-lg transition-colors text-center"
|
||||||
>
|
>
|
||||||
Použiť túto cenovku pre všetky produkty v sekcii
|
Použiť pre všetky produkty v tejto sekcii
|
||||||
</button>
|
</button>
|
||||||
|
{onApplyBadgeToAll && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Naozaj chcete aplikovať šablónu "${p.badgeCustomTemplate?.name}" na VŠETKY produkty v celom newsletteri (vrátane vernostných zliav)?`)) {
|
||||||
|
onApplyBadgeToAll({
|
||||||
|
...p.badgeCustomTemplate!,
|
||||||
|
width: p.badgeWidth || p.badgeCustomTemplate?.width || 110,
|
||||||
|
align: p.badgeAlign || p.badgeCustomTemplate?.align || 'center',
|
||||||
|
paddingTop: p.badgePaddingTop ?? p.badgeCustomTemplate?.paddingTop ?? 4,
|
||||||
|
paddingBottom: p.badgePaddingBottom ?? p.badgeCustomTemplate?.paddingBottom ?? 4
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full py-1.5 text-[11px] font-bold text-red-700 bg-red-50 hover:bg-red-100 border border-red-200 rounded-lg transition-colors text-center flex items-center justify-center gap-1.5"
|
||||||
|
>
|
||||||
|
<Globe className="w-3 h-3 text-red-600" />
|
||||||
|
<span>Použiť v celom newsletteri</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Rýchle ovládanie veľkosti (Width), zarovnania a paddingu */}
|
{/* Rýchle ovládanie veľkosti (Width), zarovnania a paddingu */}
|
||||||
@@ -1005,6 +1029,222 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
Zobraziť slovenskú vlajku / srdce
|
Zobraziť slovenskú vlajku / srdce
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Grafická cenovka a šablóna pre vernostné produkty */}
|
||||||
|
<div className="pt-2 border-t border-gray-200 space-y-2.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Tag className="w-3.5 h-3.5 text-red-600" />
|
||||||
|
<span className="text-xs font-bold text-gray-800">Cenovka</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onOpenBadgeManager && onOpenBadgeManager(p.id)}
|
||||||
|
className="text-[11px] font-bold text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100 border border-red-200 px-2.5 py-1 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Otvoriť Editor cenoviek
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Výber šablóny */}
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">
|
||||||
|
Šablóna cenovky
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={p.badgeCustomTemplate?.id || 'default_loyalty'}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
const products = [...module.data.products];
|
||||||
|
if (val === 'default_loyalty') {
|
||||||
|
products[pIdx].badgeCustomTemplate = undefined;
|
||||||
|
products[pIdx].badgeId = undefined;
|
||||||
|
} else {
|
||||||
|
const found = (availableBadges || []).find((b) => b.id === val);
|
||||||
|
if (found) {
|
||||||
|
products[pIdx].badgeCustomTemplate = found;
|
||||||
|
products[pIdx].badgeId = found.id;
|
||||||
|
products[pIdx].badgeWidth = found.width ?? products[pIdx].badgeWidth ?? 120;
|
||||||
|
products[pIdx].badgeAlign = found.align ?? products[pIdx].badgeAlign ?? 'center';
|
||||||
|
products[pIdx].badgePaddingTop = found.paddingTop ?? products[pIdx].badgePaddingTop ?? 4;
|
||||||
|
products[pIdx].badgePaddingBottom = found.paddingBottom ?? products[pIdx].badgePaddingBottom ?? 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateData({ products });
|
||||||
|
}}
|
||||||
|
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
|
||||||
|
>
|
||||||
|
<option value="default_loyalty">Predvolený Dual sticker (Bez karty / S kartou)</option>
|
||||||
|
{(availableBadges || []).map((b) => (
|
||||||
|
<option key={b.id} value={b.id}>
|
||||||
|
{b.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tlačidlá aplikácie cenovky */}
|
||||||
|
{p.badgeCustomTemplate && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Naozaj chcete aplikovať šablónu "${p.badgeCustomTemplate?.name}" na všetky vernostné produkty?`)) {
|
||||||
|
const products = (module.data.products || []).map((prod: any) => ({
|
||||||
|
...prod,
|
||||||
|
badgeCustomTemplate: p.badgeCustomTemplate,
|
||||||
|
badgeId: p.badgeCustomTemplate?.id,
|
||||||
|
badgeWidth: p.badgeWidth || p.badgeCustomTemplate?.width || 120,
|
||||||
|
badgeAlign: p.badgeAlign || p.badgeCustomTemplate?.align || 'center',
|
||||||
|
badgePaddingTop: p.badgePaddingTop ?? p.badgeCustomTemplate?.paddingTop ?? 4,
|
||||||
|
badgePaddingBottom: p.badgePaddingBottom ?? p.badgeCustomTemplate?.paddingBottom ?? 4
|
||||||
|
}));
|
||||||
|
updateData({ products });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full py-1.5 text-[11px] font-bold text-slate-700 bg-slate-100 hover:bg-slate-200 border border-slate-300 rounded-lg transition-colors text-center"
|
||||||
|
>
|
||||||
|
Použiť pre všetky produkty v tejto sekcii
|
||||||
|
</button>
|
||||||
|
{onApplyBadgeToAll && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Naozaj chcete aplikovať šablónu "${p.badgeCustomTemplate?.name}" na VŠETKY produkty v celom newsletteri?`)) {
|
||||||
|
onApplyBadgeToAll({
|
||||||
|
...p.badgeCustomTemplate!,
|
||||||
|
width: p.badgeWidth || p.badgeCustomTemplate?.width || 120,
|
||||||
|
align: p.badgeAlign || p.badgeCustomTemplate?.align || 'center',
|
||||||
|
paddingTop: p.badgePaddingTop ?? p.badgeCustomTemplate?.paddingTop ?? 4,
|
||||||
|
paddingBottom: p.badgePaddingBottom ?? p.badgeCustomTemplate?.paddingBottom ?? 4
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full py-1.5 text-[11px] font-bold text-red-700 bg-red-50 hover:bg-red-100 border border-red-200 rounded-lg transition-colors text-center flex items-center justify-center gap-1.5"
|
||||||
|
>
|
||||||
|
<Globe className="w-3 h-3 text-red-600" />
|
||||||
|
<span>Použiť v celom newsletteri</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Rýchle ovládanie veľkosti (Width), zarovnania a paddingu pre vernostnú cenovku */}
|
||||||
|
{p.badgeCustomTemplate && (
|
||||||
|
<div className="bg-gray-50 p-2.5 rounded-xl border border-gray-200 space-y-2">
|
||||||
|
{/* Šírka */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center text-[10px] text-gray-600 mb-0.5">
|
||||||
|
<span className="font-semibold">Veľkosť cenovky (Šírka)</span>
|
||||||
|
<span className="font-bold text-gray-800 bg-white px-1.5 py-0.2 rounded border border-gray-200">
|
||||||
|
{p.badgeWidth || (p.badgeCustomTemplate ? 120 : 140)} px
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="80"
|
||||||
|
max="180"
|
||||||
|
step="2"
|
||||||
|
value={p.badgeWidth || (p.badgeCustomTemplate ? 120 : 140)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const products = [...module.data.products];
|
||||||
|
products[pIdx].badgeWidth = Number(e.target.value);
|
||||||
|
updateData({ products });
|
||||||
|
}}
|
||||||
|
className="w-full accent-red-600 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Zarovnanie */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-gray-600 font-semibold block mb-1">
|
||||||
|
Zarovnanie cenovky
|
||||||
|
</span>
|
||||||
|
<div className="grid grid-cols-3 gap-1 bg-gray-200/70 p-0.5 rounded-lg text-xs font-semibold text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const products = [...module.data.products];
|
||||||
|
products[pIdx].badgeAlign = 'left';
|
||||||
|
updateData({ products });
|
||||||
|
}}
|
||||||
|
className={`py-1 rounded transition-all ${
|
||||||
|
(p.badgeAlign || 'center') === 'left'
|
||||||
|
? 'bg-white text-gray-900 shadow-xs'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Vľavo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const products = [...module.data.products];
|
||||||
|
products[pIdx].badgeAlign = 'center';
|
||||||
|
updateData({ products });
|
||||||
|
}}
|
||||||
|
className={`py-1 rounded transition-all ${
|
||||||
|
(p.badgeAlign || 'center') === 'center'
|
||||||
|
? 'bg-white text-gray-900 shadow-xs'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Na stred
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const products = [...module.data.products];
|
||||||
|
products[pIdx].badgeAlign = 'right';
|
||||||
|
updateData({ products });
|
||||||
|
}}
|
||||||
|
className={`py-1 rounded transition-all ${
|
||||||
|
(p.badgeAlign || 'center') === 'right'
|
||||||
|
? 'bg-white text-gray-900 shadow-xs'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Vpravo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Padding zhora a zdola */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] text-gray-600 font-semibold block">Odsadenie zhora (px)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="40"
|
||||||
|
value={p.badgePaddingTop ?? (p.badgeCustomTemplate?.paddingTop ?? 4)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const products = [...module.data.products];
|
||||||
|
products[pIdx].badgePaddingTop = Number(e.target.value);
|
||||||
|
updateData({ products });
|
||||||
|
}}
|
||||||
|
className="w-full text-xs border border-gray-300 rounded-lg p-1 bg-white font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] text-gray-600 font-semibold block">Odsadenie zdola (px)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="40"
|
||||||
|
value={p.badgePaddingBottom ?? (p.badgeCustomTemplate?.paddingBottom ?? 4)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const products = [...module.data.products];
|
||||||
|
products[pIdx].badgePaddingBottom = Number(e.target.value);
|
||||||
|
updateData({ products });
|
||||||
|
}}
|
||||||
|
className="w-full text-xs border border-gray-300 rounded-lg p-1 bg-white font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ export const SavedCampaignsModal: React.FC<SavedCampaignsModalProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
{camp.brevo_campaign_id && (
|
{camp.brevo_campaign_id && (
|
||||||
<a
|
<a
|
||||||
href={`https://app.brevo.com/campaigns/email/edit/${camp.brevo_campaign_id}`}
|
href="https://app.brevo.com/campaigns"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-600 hover:underline flex items-center gap-1 font-semibold"
|
className="text-blue-600 hover:underline flex items-center gap-1 font-semibold"
|
||||||
|
|||||||
@@ -214,14 +214,14 @@ export const TopBar: React.FC<TopBarProps> = ({
|
|||||||
<div className="flex items-center gap-1.5 bg-emerald-50 border border-emerald-200 p-1 rounded-xl">
|
<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 úpravu v Brevo */}
|
||||||
<a
|
<a
|
||||||
href={`https://app.brevo.com/campaigns/email/edit/${campaign.brevoCampaignId}`}
|
href="https://app.brevo.com/campaigns"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
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"
|
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"
|
||||||
title={`Kampaň #${campaign.brevoCampaignId} existuje v Brevo (${brevoCampaignStatus || 'draft'}). Kliknutím ju otvoríte v editore Brevo.`}
|
title={`Kampaň #${campaign.brevoCampaignId} je v Brevo (${brevoCampaignStatus || 'draft'}). Kliknutím otvoríte zoznam kampaní v Brevo.`}
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-3.5 h-3.5" />
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
<span>Upraviť v Brevo</span>
|
<span>Otvoriť v Brevo</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/* Tlačidlo na aktualizáciu existujúcej kampane */}
|
{/* Tlačidlo na aktualizáciu existujúcej kampane */}
|
||||||
@@ -244,17 +244,6 @@ export const TopBar: React.FC<TopBarProps> = ({
|
|||||||
>
|
>
|
||||||
<RefreshCw className="w-3 h-3" />
|
<RefreshCw className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Odkaz na celkový zoznam kampaní */}
|
|
||||||
<a
|
|
||||||
href={brevoListingUrl || 'https://app.brevo.com/campaigns/listing'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-[10px] font-bold text-emerald-700 hover:underline px-1.5 hidden md:inline"
|
|
||||||
title="Prehľad všetkých kampaní v Brevo"
|
|
||||||
>
|
|
||||||
Zoznam
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Kampaň v Brevo nie je overená / neexistuje: Zobrazujeme tlačidlo na prvotné odoslanie */
|
/* Kampaň v Brevo nie je overená / neexistuje: Zobrazujeme tlačidlo na prvotné odoslanie */
|
||||||
|
|||||||
+13
-158
@@ -1,159 +1,7 @@
|
|||||||
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
||||||
|
|
||||||
export const DEFAULT_BADGE_TEMPLATES: BadgeTemplate[] = [
|
// Vymazané všetky preddefinované šablóny — v systéme sú výhradne vlastné cenovky vytvorené používateľom
|
||||||
{
|
export const DEFAULT_BADGE_TEMPLATES: BadgeTemplate[] = [];
|
||||||
id: 'badge-preset-yellow-akcia',
|
|
||||||
name: 'COOP Akcia (Žltá)',
|
|
||||||
svgRaw: `<svg width="140" height="95" viewBox="0 0 140 95" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="140" height="95" rx="14" fill="#ffd100"/>
|
|
||||||
<rect x="15" y="8" width="110" height="24" rx="6" fill="#e2001a"/>
|
|
||||||
</svg>`,
|
|
||||||
width: 110,
|
|
||||||
align: 'center',
|
|
||||||
paddingTop: 4,
|
|
||||||
paddingBottom: 4,
|
|
||||||
layersConfig: [
|
|
||||||
{
|
|
||||||
id: 'l-akcia-1',
|
|
||||||
type: 'text',
|
|
||||||
text: 'AKCIA',
|
|
||||||
x: 70,
|
|
||||||
y: 20,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '900',
|
|
||||||
fontFamily: 'Poppins',
|
|
||||||
fill: '#ffffff',
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'l-akcia-2',
|
|
||||||
type: 'text',
|
|
||||||
text: '{cena}',
|
|
||||||
x: 70,
|
|
||||||
y: 62,
|
|
||||||
fontSize: 42,
|
|
||||||
fontWeight: '900',
|
|
||||||
fontFamily: 'Arial Narrow',
|
|
||||||
fill: '#001a33',
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'badge-preset-red-zlava',
|
|
||||||
name: 'COOP Zľava (Červená)',
|
|
||||||
svgRaw: `<svg width="140" height="95" viewBox="0 0 140 95" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="140" height="95" rx="14" fill="#e2001a"/>
|
|
||||||
<rect x="12" y="8" width="116" height="22" rx="5" fill="#ffffff" fill-opacity="0.2"/>
|
|
||||||
</svg>`,
|
|
||||||
width: 110,
|
|
||||||
align: 'center',
|
|
||||||
paddingTop: 4,
|
|
||||||
paddingBottom: 4,
|
|
||||||
layersConfig: [
|
|
||||||
{
|
|
||||||
id: 'l-zlava-1',
|
|
||||||
type: 'text',
|
|
||||||
text: 'ZĽAVA -{zlava}',
|
|
||||||
x: 70,
|
|
||||||
y: 19,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '900',
|
|
||||||
fontFamily: 'Poppins',
|
|
||||||
fill: '#ffffff',
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'l-zlava-2',
|
|
||||||
type: 'text',
|
|
||||||
text: '{cena}',
|
|
||||||
x: 70,
|
|
||||||
y: 62,
|
|
||||||
fontSize: 42,
|
|
||||||
fontWeight: '900',
|
|
||||||
fontFamily: 'Arial Narrow',
|
|
||||||
fill: '#ffd100',
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'badge-preset-blue-supercena',
|
|
||||||
name: 'Super Cena (Tmavomodrá)',
|
|
||||||
svgRaw: `<svg width="140" height="95" viewBox="0 0 140 95" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="140" height="95" rx="14" fill="#001a33"/>
|
|
||||||
<rect x="0" y="0" width="140" height="30" rx="14" fill="#003399"/>
|
|
||||||
</svg>`,
|
|
||||||
width: 110,
|
|
||||||
align: 'center',
|
|
||||||
paddingTop: 4,
|
|
||||||
paddingBottom: 4,
|
|
||||||
layersConfig: [
|
|
||||||
{
|
|
||||||
id: 'l-super-1',
|
|
||||||
type: 'text',
|
|
||||||
text: 'SUPER CENA',
|
|
||||||
x: 70,
|
|
||||||
y: 16,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '900',
|
|
||||||
fontFamily: 'Poppins',
|
|
||||||
fill: '#ffffff',
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'l-super-2',
|
|
||||||
type: 'text',
|
|
||||||
text: '{cena}',
|
|
||||||
x: 70,
|
|
||||||
y: 62,
|
|
||||||
fontSize: 42,
|
|
||||||
fontWeight: '900',
|
|
||||||
fontFamily: 'Arial Narrow',
|
|
||||||
fill: '#ffd100',
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'badge-preset-green-cerstve',
|
|
||||||
name: 'Bio & Čerstvé (Zelená)',
|
|
||||||
svgRaw: `<svg width="140" height="95" viewBox="0 0 140 95" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="140" height="95" rx="14" fill="#005239"/>
|
|
||||||
<rect x="15" y="8" width="110" height="22" rx="6" fill="#003d2b"/>
|
|
||||||
</svg>`,
|
|
||||||
width: 110,
|
|
||||||
align: 'center',
|
|
||||||
paddingTop: 4,
|
|
||||||
paddingBottom: 4,
|
|
||||||
layersConfig: [
|
|
||||||
{
|
|
||||||
id: 'l-green-1',
|
|
||||||
type: 'text',
|
|
||||||
text: 'ČERSTVÉ',
|
|
||||||
x: 70,
|
|
||||||
y: 19,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '900',
|
|
||||||
fontFamily: 'Poppins',
|
|
||||||
fill: '#a3e635',
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'l-green-2',
|
|
||||||
type: 'text',
|
|
||||||
text: '{cena}',
|
|
||||||
x: 70,
|
|
||||||
y: 62,
|
|
||||||
fontSize: 42,
|
|
||||||
fontWeight: '900',
|
|
||||||
fontFamily: 'Arial Narrow',
|
|
||||||
fill: '#ffffff',
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface BadgeProductData {
|
export interface BadgeProductData {
|
||||||
priceMain: string;
|
priceMain: string;
|
||||||
@@ -224,7 +72,10 @@ export function renderCleanBadgeSvg(
|
|||||||
const textLayersSvg = (template.layersConfig || []).map((layer) => {
|
const textLayersSvg = (template.layersConfig || []).map((layer) => {
|
||||||
const rawText = layer.text || '';
|
const rawText = layer.text || '';
|
||||||
const fontSize = layer.fontSize || 16;
|
const fontSize = layer.fontSize || 16;
|
||||||
const subFontSize = Math.max(10, Math.round(fontSize * 0.55));
|
const subFontSize = layer.indexFontSize || Math.max(10, Math.round(fontSize * 0.55));
|
||||||
|
const subDy = layer.indexOffsetY !== undefined ? `${layer.indexOffsetY}px` : '-0.45em';
|
||||||
|
const subDx = layer.indexOffsetX !== undefined ? `${layer.indexOffsetX}px` : '1px';
|
||||||
|
const revertDy = layer.indexOffsetY !== undefined ? `${-layer.indexOffsetY}px` : '0.45em';
|
||||||
const fontWeight = layer.fontWeight || '700';
|
const fontWeight = layer.fontWeight || '700';
|
||||||
const fill = layer.fill || '#ffffff';
|
const fill = layer.fill || '#ffffff';
|
||||||
const fontFamily = layer.fontFamily
|
const fontFamily = layer.fontFamily
|
||||||
@@ -235,17 +86,21 @@ export function renderCleanBadgeSvg(
|
|||||||
if (layer.align === 'left') anchor = 'start';
|
if (layer.align === 'left') anchor = 'start';
|
||||||
if (layer.align === 'right') anchor = 'end';
|
if (layer.align === 'right') anchor = 'end';
|
||||||
|
|
||||||
// Formátovanie textu: ak obsahuje {cena}, centy vložíme do <tspan> ako horný index
|
// Formátovanie textu: ak obsahuje {cena} alebo {centy}, centy vložíme do <tspan> ako horný index
|
||||||
let contentSvg = '';
|
let contentSvg = '';
|
||||||
if (rawText.includes('{cena}')) {
|
if (rawText.includes('{cena}')) {
|
||||||
const parts = rawText.split('{cena}');
|
const parts = rawText.split('{cena}');
|
||||||
const before = escapeXml(parts[0] || '');
|
const before = escapeXml(parts[0] || '');
|
||||||
const after = escapeXml(parts[1] || '');
|
const after = escapeXml(parts[1] || '');
|
||||||
contentSvg = `${before}${escapeXml(pMain)}<tspan font-size="${subFontSize}px" dy="-0.45em" font-weight="${fontWeight}">${escapeXml(pSub)}</tspan><tspan dy="0.45em">${after}</tspan>`;
|
contentSvg = `${before}${escapeXml(pMain)}<tspan font-size="${subFontSize}px" dx="${subDx}" dy="${subDy}" font-weight="${fontWeight}">${escapeXml(pSub)}</tspan>${after ? `<tspan dy="${revertDy}">${after}</tspan>` : ''}`;
|
||||||
|
} else if (rawText.includes('{centy}')) {
|
||||||
|
const parts = rawText.split('{centy}');
|
||||||
|
const before = escapeXml(parts[0] || '');
|
||||||
|
const after = escapeXml(parts[1] || '');
|
||||||
|
contentSvg = `${before}<tspan font-size="${subFontSize}px" dx="${subDx}" dy="${subDy}" font-weight="${fontWeight}">${escapeXml(pSub)}</tspan>${after ? `<tspan dy="${revertDy}">${after}</tspan>` : ''}`;
|
||||||
} else {
|
} else {
|
||||||
let replaced = rawText
|
let replaced = rawText
|
||||||
.replace(/{cela_suma}/g, pMain)
|
.replace(/{cela_suma}/g, pMain)
|
||||||
.replace(/{centy}/g, pSub)
|
|
||||||
.replace(/{zlava}/g, discount)
|
.replace(/{zlava}/g, discount)
|
||||||
.replace(/{jednotkova_cena}/g, unitPrice)
|
.replace(/{jednotkova_cena}/g, unitPrice)
|
||||||
.replace(/{mena}/g, '€');
|
.replace(/{mena}/g, '€');
|
||||||
|
|||||||
@@ -620,7 +620,7 @@ function renderExactProductCard(
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr height="1">
|
<tr height="1">
|
||||||
<td style="padding: 0 8px 12px 8px; text-align: center;" valign="bottom">
|
<td style="padding: 0 8px 4px 8px; text-align: center;" valign="bottom">
|
||||||
${(() => {
|
${(() => {
|
||||||
const bWidth = p.badgeWidth || (p.badgeCustomTemplate ? (p.badgeCustomTemplate.width || 110) : 95);
|
const bWidth = p.badgeWidth || (p.badgeCustomTemplate ? (p.badgeCustomTemplate.width || 110) : 95);
|
||||||
const bAlign = p.badgeAlign || (p.badgeCustomTemplate ? (p.badgeCustomTemplate.align || 'center') : 'center');
|
const bAlign = p.badgeAlign || (p.badgeCustomTemplate ? (p.badgeCustomTemplate.align || 'center') : 'center');
|
||||||
@@ -701,7 +701,11 @@ function renderExactProductCard(
|
|||||||
</tr>` : ''}
|
</tr>` : ''}
|
||||||
</table>`;
|
</table>`;
|
||||||
})()}
|
})()}
|
||||||
${p.unitPrice ? `<div style="font-size: 9px; color: #666666; margin-top: 4px; font-weight: 500; font-family: Arial, sans-serif;">${escapeHtml(p.unitPrice)}</div>` : ''}
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 2px 8px 12px 8px; text-align: center;">
|
||||||
|
${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;"> </div>'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>`;
|
</table>`;
|
||||||
@@ -739,8 +743,47 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr height="1">
|
<tr height="1">
|
||||||
<td style="padding: 0 8px 12px 8px; text-align: center;" valign="bottom">
|
<td style="padding: 0 8px ${p.badgeCustomTemplate ? '4px' : '12px'} 8px; text-align: center;" valign="bottom">
|
||||||
<!-- Dual Sticker Block (Bez karty / S kartou) 1:1 -->
|
${(() => {
|
||||||
|
// 1. Ak má vernostný produkt vlastnú dynamickú šablónu cenovky
|
||||||
|
if (p.badgeCustomTemplate) {
|
||||||
|
const bWidth = p.badgeWidth || p.badgeCustomTemplate.width || 120;
|
||||||
|
const bAlign = p.badgeAlign || p.badgeCustomTemplate.align || 'center';
|
||||||
|
const padTop = p.badgePaddingTop ?? (p.badgeCustomTemplate.paddingTop ?? 0);
|
||||||
|
const padBottom = p.badgePaddingBottom ?? (p.badgeCustomTemplate.paddingBottom ?? 0);
|
||||||
|
|
||||||
|
let marginStyle = 'margin: 0 auto;';
|
||||||
|
if (bAlign === 'left') {
|
||||||
|
marginStyle = 'margin: 0 auto 0 0; margin-right: auto; margin-left: 0;';
|
||||||
|
} else if (bAlign === 'right') {
|
||||||
|
marginStyle = 'margin: 0 0 0 auto; margin-left: auto; margin-right: 0;';
|
||||||
|
}
|
||||||
|
|
||||||
|
const loyaltyProductData = {
|
||||||
|
priceMain: p.cardPriceMain || p.priceMain || '0',
|
||||||
|
priceSub: p.cardPriceSub || p.priceSub || '00',
|
||||||
|
discountText: p.cardDiscount || p.discountText || '',
|
||||||
|
unitPrice: p.cardUnitPrice || p.unitPrice || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
|
<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}">
|
||||||
|
${renderCleanBadgeSvg(p.badgeCustomTemplate, loyaltyProductData, bWidth)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
${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);">
|
||||||
|
</td>
|
||||||
|
</tr>` : ''}
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Predvolený Dual Sticker Block (Bez karty / S kartou) 1:1
|
||||||
|
return `
|
||||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: 0 auto; width: 140px;">
|
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: 0 auto; width: 140px;">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
@@ -799,9 +842,16 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
|
|||||||
<div style="font-size: 9px; color: #e2001a; font-weight: 800; font-family: Arial, sans-serif;">${escapeHtml(p.cardUnitPrice)}</div>
|
<div style="font-size: 9px; color: #e2001a; font-weight: 800; font-family: Arial, sans-serif;">${escapeHtml(p.cardUnitPrice)}</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>`;
|
||||||
|
})()}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
${p.badgeCustomTemplate ? `
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 2px 8px 12px 8px; text-align: center;">
|
||||||
|
${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;"> </div>'}
|
||||||
|
</td>
|
||||||
|
</tr>` : ''}
|
||||||
</table>`;
|
</table>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,10 @@ export interface BadgeLayer {
|
|||||||
fontUrl?: string;
|
fontUrl?: string;
|
||||||
fill: string;
|
fill: string;
|
||||||
align?: 'left' | 'center' | 'right';
|
align?: 'left' | 'center' | 'right';
|
||||||
|
// Nastavenia horného indexu (centov)
|
||||||
|
indexFontSize?: number; // Veľkosť horného indexu v px
|
||||||
|
indexOffsetY?: number; // Vertikálny posun horného indexu v px (posun hore / dole)
|
||||||
|
indexOffsetX?: number; // Horizontálny posun horného indexu v px (medzera)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BadgeTemplate {
|
export interface BadgeTemplate {
|
||||||
|
|||||||
Reference in new issue
Block a user