feat: reusable badge templates, dynamic price and superscript cents, size/align/padding controls, fix saving
This commit is contained in:
1 parent
f9aa2297ba
commit
5a6788b593
6 files changed
+1020
-183
No files matched your search
+73
-1
@@ -5,6 +5,7 @@ import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from
|
|||||||
import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign';
|
import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign';
|
||||||
import { renderEmailHtml, renderSingleModuleHtml } from '@/lib/emailRenderer';
|
import { renderEmailHtml, renderSingleModuleHtml } from '@/lib/emailRenderer';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { DEFAULT_BADGE_TEMPLATES } from '@/lib/badgeUtils';
|
||||||
import { TopBar } from '@/components/editor/TopBar';
|
import { TopBar } from '@/components/editor/TopBar';
|
||||||
import { ConfigPanel } from '@/components/editor/ConfigPanel';
|
import { ConfigPanel } from '@/components/editor/ConfigPanel';
|
||||||
import { ProductImportModal } from '@/components/editor/ProductImportModal';
|
import { ProductImportModal } from '@/components/editor/ProductImportModal';
|
||||||
@@ -26,6 +27,64 @@ export default function Home() {
|
|||||||
const [isBadgeEditorOpen, setIsBadgeEditorOpen] = useState(false);
|
const [isBadgeEditorOpen, setIsBadgeEditorOpen] = useState(false);
|
||||||
const [isSavedCampaignsOpen, setIsSavedCampaignsOpen] = useState(false);
|
const [isSavedCampaignsOpen, setIsSavedCampaignsOpen] = useState(false);
|
||||||
const [targetProductIdForBadge, setTargetProductIdForBadge] = useState<string | null>(null);
|
const [targetProductIdForBadge, setTargetProductIdForBadge] = useState<string | null>(null);
|
||||||
|
const [availableBadges, setAvailableBadges] = useState<BadgeTemplate[]>(DEFAULT_BADGE_TEMPLATES);
|
||||||
|
|
||||||
|
// Načítanie dostupných šablón cenoviek pre opakované použitie
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBadges = async () => {
|
||||||
|
let loaded: BadgeTemplate[] = [];
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('badges')
|
||||||
|
.select('*')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (data && !error && data.length > 0) {
|
||||||
|
loaded = data.map((d: any) => {
|
||||||
|
const isObj = d.layers_config && !Array.isArray(d.layers_config);
|
||||||
|
return {
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
svgRaw: d.svg_raw || '',
|
||||||
|
layersConfig: isObj ? (d.layers_config.layers || []) : (d.layers_config || []),
|
||||||
|
width: isObj ? (d.layers_config.width || 110) : 110,
|
||||||
|
align: isObj ? (d.layers_config.align || 'center') : 'center',
|
||||||
|
paddingTop: isObj ? (d.layers_config.paddingTop ?? 4) : 4,
|
||||||
|
paddingBottom: isObj ? (d.layers_config.paddingBottom ?? 4) : 4
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error fetching badges in page:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const local = localStorage.getItem('coop_custom_badges');
|
||||||
|
if (local) {
|
||||||
|
const parsed = JSON.parse(local);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
const existingIds = new Set(loaded.map((b) => b.id));
|
||||||
|
for (const b of parsed) {
|
||||||
|
if (!existingIds.has(b.id)) {
|
||||||
|
loaded.push(b);
|
||||||
|
existingIds.add(b.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const all = [...loaded];
|
||||||
|
const existingIds = new Set(all.map((b) => b.id));
|
||||||
|
for (const preset of DEFAULT_BADGE_TEMPLATES) {
|
||||||
|
if (!existingIds.has(preset.id)) {
|
||||||
|
all.push(preset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAvailableBadges(all);
|
||||||
|
};
|
||||||
|
fetchBadges();
|
||||||
|
}, [isBadgeEditorOpen]);
|
||||||
|
|
||||||
// Pomocné funkcie pre fixné ukotvenie hlavičky, spodných letákov, receptov a pätičky
|
// Pomocné funkcie pre fixné ukotvenie hlavičky, spodných letákov, receptov a pätičky
|
||||||
const isTopModule = (m: NewsletterModule) => m.type === 'header' || m.type === 'announcement';
|
const isTopModule = (m: NewsletterModule) => m.type === 'header' || m.type === 'announcement';
|
||||||
@@ -475,7 +534,12 @@ export default function Home() {
|
|||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
badgeId: badge.id,
|
badgeId: badge.id,
|
||||||
customBadgePngUrl: renderedPngUrl || p.customBadgePngUrl
|
badgeCustomTemplate: badge,
|
||||||
|
badgeWidth: badge.width ?? p.badgeWidth ?? 110,
|
||||||
|
badgeAlign: badge.align ?? p.badgeAlign ?? 'center',
|
||||||
|
badgePaddingTop: badge.paddingTop ?? p.badgePaddingTop ?? 4,
|
||||||
|
badgePaddingBottom: badge.paddingBottom ?? p.badgePaddingBottom ?? 4,
|
||||||
|
customBadgePngUrl: undefined // Používame živý dynamický SVG vektor, žiadna statická fotka
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
@@ -488,6 +552,13 @@ export default function Home() {
|
|||||||
products: updatedProducts
|
products: updatedProducts
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setAvailableBadges((prev) => {
|
||||||
|
if (prev.some((b) => b.id === badge.id)) {
|
||||||
|
return prev.map((b) => (b.id === badge.id ? badge : b));
|
||||||
|
}
|
||||||
|
return [badge, ...prev];
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const topCount = campaign.modules.filter((m) => isTopModule(m)).length;
|
const topCount = campaign.modules.filter((m) => isTopModule(m)).length;
|
||||||
@@ -651,6 +722,7 @@ export default function Home() {
|
|||||||
<ConfigPanel
|
<ConfigPanel
|
||||||
module={selectedModule}
|
module={selectedModule}
|
||||||
onUpdateModule={handleUpdateModule}
|
onUpdateModule={handleUpdateModule}
|
||||||
|
availableBadges={availableBadges}
|
||||||
onOpenBadgeManager={(pId) => {
|
onOpenBadgeManager={(pId) => {
|
||||||
setTargetProductIdForBadge(pId);
|
setTargetProductIdForBadge(pId);
|
||||||
setIsBadgeEditorOpen(true);
|
setIsBadgeEditorOpen(true);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import { toPng } from 'html-to-image';
|
import { DEFAULT_BADGE_TEMPLATES, renderCleanBadgeSvg, svgToCleanPng } from '@/lib/badgeUtils';
|
||||||
import { Tag, Plus, Check, Trash2, X, Upload, Eye, FileCode, Move } from 'lucide-react';
|
import { Tag, Plus, Check, Trash2, X, Upload, Eye, Move, AlignLeft, AlignCenter, AlignRight } from 'lucide-react';
|
||||||
|
|
||||||
interface BadgeEditorModalProps {
|
interface BadgeEditorModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -68,7 +68,6 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
const [badges, setBadges] = useState<BadgeTemplate[]>([]);
|
const [badges, setBadges] = useState<BadgeTemplate[]>([]);
|
||||||
const [activeBadge, setActiveBadge] = useState<BadgeTemplate | null>(null);
|
const [activeBadge, setActiveBadge] = useState<BadgeTemplate | null>(null);
|
||||||
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [isExportingPng, setIsExportingPng] = useState(false);
|
const [isExportingPng, setIsExportingPng] = useState(false);
|
||||||
const [isSavingDb, setIsSavingDb] = useState(false);
|
const [isSavingDb, setIsSavingDb] = useState(false);
|
||||||
|
|
||||||
@@ -175,7 +174,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
}
|
}
|
||||||
}, [activeBadge]);
|
}, [activeBadge]);
|
||||||
|
|
||||||
// Načítanie existujúcich šablón zo Supabase
|
// Načítanie existujúcich šablón zo Supabase + LocalStorage + Default Presets
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
loadBadgesFromSupabase();
|
loadBadgesFromSupabase();
|
||||||
@@ -183,6 +182,8 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const loadBadgesFromSupabase = async () => {
|
const loadBadgesFromSupabase = async () => {
|
||||||
|
let loaded: BadgeTemplate[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('badges')
|
.from('badges')
|
||||||
@@ -190,45 +191,81 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
.order('created_at', { ascending: false });
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
if (data && !error && data.length > 0) {
|
if (data && !error && data.length > 0) {
|
||||||
const loaded: BadgeTemplate[] = data.map((d: any) => ({
|
loaded = data.map((d: any) => {
|
||||||
|
const isObj = d.layers_config && !Array.isArray(d.layers_config);
|
||||||
|
return {
|
||||||
id: d.id,
|
id: d.id,
|
||||||
name: d.name,
|
name: d.name,
|
||||||
svgRaw: d.svg_raw || '',
|
svgRaw: d.svg_raw || '',
|
||||||
layersConfig: d.layers_config || []
|
layersConfig: isObj ? (d.layers_config.layers || []) : (d.layers_config || []),
|
||||||
}));
|
width: isObj ? (d.layers_config.width || 110) : 110,
|
||||||
setBadges(loaded);
|
align: isObj ? (d.layers_config.align || 'center') : 'center',
|
||||||
setActiveBadge(loaded[0]);
|
paddingTop: isObj ? (d.layers_config.paddingTop ?? 4) : 4,
|
||||||
if (loaded[0]?.layersConfig?.[0]) {
|
paddingBottom: isObj ? (d.layers_config.paddingBottom ?? 4) : 4
|
||||||
setSelectedLayerId(loaded[0].layersConfig[0].id);
|
};
|
||||||
}
|
});
|
||||||
} else {
|
|
||||||
// Ak je databáza prázdna, necháme čistý stav
|
|
||||||
setBadges([]);
|
|
||||||
setActiveBadge(null);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading badges:', err);
|
console.warn('Error loading badges from Supabase, checking local storage:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doplnenie z localStorage
|
||||||
|
try {
|
||||||
|
const localStr = localStorage.getItem('coop_custom_badges');
|
||||||
|
if (localStr) {
|
||||||
|
const localBadges = JSON.parse(localStr);
|
||||||
|
if (Array.isArray(localBadges)) {
|
||||||
|
const existingIds = new Set(loaded.map((b) => b.id));
|
||||||
|
for (const b of localBadges) {
|
||||||
|
if (!existingIds.has(b.id)) {
|
||||||
|
loaded.push(b);
|
||||||
|
existingIds.add(b.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Doplnenie predpripravených dizajnových šablón (DEFAULT_BADGE_TEMPLATES)
|
||||||
|
const allBadges = [...loaded];
|
||||||
|
const existingIds = new Set(allBadges.map((b) => b.id));
|
||||||
|
for (const preset of DEFAULT_BADGE_TEMPLATES) {
|
||||||
|
if (!existingIds.has(preset.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
// Vytvorenie úplne novej prázdnej šablóny cenovky
|
// Vytvorenie novej šablóny
|
||||||
const handleCreateNewTemplate = () => {
|
const handleCreateNewTemplate = () => {
|
||||||
const newBadge: BadgeTemplate = {
|
const newBadge: BadgeTemplate = {
|
||||||
id: `badge-${Date.now()}`,
|
id: `badge-${Date.now()}`,
|
||||||
name: 'Nová vlastná cenovka',
|
name: 'Nová vlastná cenovka',
|
||||||
svgRaw: `<svg width="140" height="90" viewBox="0 0 140 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
svgRaw: `<svg width="140" height="95" viewBox="0 0 140 95" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect width="140" height="90" rx="12" fill="#ffd100"/>
|
<rect width="140" height="95" rx="14" fill="#ffd100"/>
|
||||||
</svg>`,
|
</svg>`,
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
paddingTop: 4,
|
||||||
|
paddingBottom: 4,
|
||||||
layersConfig: [
|
layersConfig: [
|
||||||
{
|
{
|
||||||
id: `l-${Date.now()}-1`,
|
id: `l-${Date.now()}-1`,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: 'AKCIA',
|
text: 'AKCIA',
|
||||||
x: 70,
|
x: 70,
|
||||||
y: 30,
|
y: 24,
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
fontWeight: '900',
|
fontWeight: '900',
|
||||||
fill: '#e2001a',
|
fill: '#e2001a',
|
||||||
align: 'center'
|
align: 'center'
|
||||||
@@ -236,10 +273,10 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
{
|
{
|
||||||
id: `l-${Date.now()}-2`,
|
id: `l-${Date.now()}-2`,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: '-{zlava}',
|
text: '{cena}',
|
||||||
x: 70,
|
x: 70,
|
||||||
y: 65,
|
y: 62,
|
||||||
fontSize: 26,
|
fontSize: 42,
|
||||||
fontWeight: '900',
|
fontWeight: '900',
|
||||||
fill: '#001a33',
|
fill: '#001a33',
|
||||||
align: 'center'
|
align: 'center'
|
||||||
@@ -252,7 +289,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
setSelectedLayerId(newBadge.layersConfig[0].id);
|
setSelectedLayerId(newBadge.layersConfig[0].id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Upload vlastného SVG súboru ako pozadia
|
// Upload vlastného SVG súboru
|
||||||
const handleSvgUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSvgUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file || !activeBadge) return;
|
if (!file || !activeBadge) return;
|
||||||
@@ -261,7 +298,6 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
const content = event.target?.result as string;
|
const content = event.target?.result as string;
|
||||||
if (content.includes('<svg')) {
|
if (content.includes('<svg')) {
|
||||||
// Vyčistenie a nastavenie rozmerov
|
|
||||||
setActiveBadge({
|
setActiveBadge({
|
||||||
...activeBadge,
|
...activeBadge,
|
||||||
svgRaw: content
|
svgRaw: content
|
||||||
@@ -315,70 +351,162 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteBadgeTemplate = async (badgeId: string) => {
|
const deleteBadgeTemplate = async (badgeId: string) => {
|
||||||
|
if (confirm('Naozaj chcete zmazať túto šablónu cenovky?')) {
|
||||||
try {
|
try {
|
||||||
await supabase.from('badges').delete().eq('id', badgeId);
|
await supabase.from('badges').delete().eq('id', badgeId);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
const remaining = badges.filter((b) => b.id !== badgeId);
|
const remaining = badges.filter((b) => b.id !== badgeId);
|
||||||
setBadges(remaining);
|
setBadges(remaining);
|
||||||
setActiveBadge(remaining[0] || null);
|
setActiveBadge(remaining[0] || null);
|
||||||
|
try {
|
||||||
|
localStorage.setItem('coop_custom_badges', JSON.stringify(remaining));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dynamické nahradenie premenných: {zlava}, {cena}, {jednotkova_cena}
|
// Uloženie šablóny do Supabase + LocalStorage (garantované uchovanie)
|
||||||
const replaceVariables = (txt: string) => {
|
|
||||||
if (!txt) return '';
|
|
||||||
return txt
|
|
||||||
.replace(/{zlava}/g, currentDiscount.replace('-', ''))
|
|
||||||
.replace(/{cena}/g, `${currentPriceMain},${currentPriceSub} €`)
|
|
||||||
.replace(/{jednotkova_cena}/g, currentUnitPrice);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Uloženie šablóny do Supabase
|
|
||||||
const handleSaveToSupabase = async () => {
|
const handleSaveToSupabase = async () => {
|
||||||
if (!activeBadge) return;
|
if (!activeBadge) return;
|
||||||
setIsSavingDb(true);
|
setIsSavingDb(true);
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const payload: any = {
|
||||||
.from('badges')
|
|
||||||
.upsert([
|
|
||||||
{
|
|
||||||
id: activeBadge.id.startsWith('badge-') ? undefined : activeBadge.id,
|
|
||||||
name: activeBadge.name,
|
name: activeBadge.name,
|
||||||
svg_raw: activeBadge.svgRaw,
|
svg_raw: activeBadge.svgRaw,
|
||||||
layers_config: activeBadge.layersConfig
|
layers_config: {
|
||||||
|
layers: activeBadge.layersConfig,
|
||||||
|
width: activeBadge.width || 110,
|
||||||
|
align: activeBadge.align || 'center',
|
||||||
|
paddingTop: activeBadge.paddingTop ?? 4,
|
||||||
|
paddingBottom: activeBadge.paddingBottom ?? 4
|
||||||
}
|
}
|
||||||
])
|
};
|
||||||
.select();
|
|
||||||
|
|
||||||
if (data && data.length > 0) {
|
let savedBadge: BadgeTemplate | null = null;
|
||||||
alert('Šablóna cenovky bola úspešne uložená do databázy!');
|
const isExistingUuid = activeBadge.id && !activeBadge.id.startsWith('badge-');
|
||||||
loadBadgesFromSupabase();
|
|
||||||
|
if (isExistingUuid) {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('badges')
|
||||||
|
.update(payload)
|
||||||
|
.eq('id', activeBadge.id)
|
||||||
|
.select();
|
||||||
|
if (error) throw error;
|
||||||
|
if (data && data[0]) {
|
||||||
|
savedBadge = {
|
||||||
|
id: data[0].id,
|
||||||
|
name: data[0].name,
|
||||||
|
svgRaw: data[0].svg_raw,
|
||||||
|
layersConfig: activeBadge.layersConfig,
|
||||||
|
width: activeBadge.width,
|
||||||
|
align: activeBadge.align,
|
||||||
|
paddingTop: activeBadge.paddingTop,
|
||||||
|
paddingBottom: activeBadge.paddingBottom
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('badges')
|
||||||
|
.insert([payload])
|
||||||
|
.select();
|
||||||
|
if (error) throw error;
|
||||||
|
if (data && data[0]) {
|
||||||
|
savedBadge = {
|
||||||
|
id: data[0].id,
|
||||||
|
name: data[0].name,
|
||||||
|
svgRaw: data[0].svg_raw,
|
||||||
|
layersConfig: activeBadge.layersConfig,
|
||||||
|
width: activeBadge.width,
|
||||||
|
align: activeBadge.align,
|
||||||
|
paddingTop: activeBadge.paddingTop,
|
||||||
|
paddingBottom: activeBadge.paddingBottom
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetToSave = savedBadge || activeBadge;
|
||||||
|
const updatedList = badges.map((b) => (b.id === activeBadge.id ? targetToSave : b));
|
||||||
|
if (!updatedList.some((b) => b.id === targetToSave.id)) {
|
||||||
|
updatedList.unshift(targetToSave);
|
||||||
|
}
|
||||||
|
setBadges(updatedList);
|
||||||
|
setActiveBadge(targetToSave);
|
||||||
|
try {
|
||||||
|
localStorage.setItem('coop_custom_badges', JSON.stringify(updatedList));
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
alert('Šablóna cenovky bola úspešne uložená a je pripravená na opätovné použitie pre všetky produkty!');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(`Chyba pri ukladaní do databázy: ${err.message}`);
|
console.warn('Supabase save error, persisting to local cache:', err);
|
||||||
|
try {
|
||||||
|
const updatedList = badges.map((b) => (b.id === activeBadge.id ? activeBadge : b));
|
||||||
|
localStorage.setItem('coop_custom_badges', JSON.stringify(updatedList));
|
||||||
|
alert('Šablóna bola úspešne uložená v prehliadači.');
|
||||||
|
} catch (e) {
|
||||||
|
alert(`Chyba pri ukladaní: ${err.message}`);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSavingDb(false);
|
setIsSavingDb(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render do PNG a aplikovanie na produkt
|
// Vykreslenie vrstvy v React náhľade s centami ako horný index (sup) a BEZ znaku €
|
||||||
const handleApplyBadge = async () => {
|
const renderLayerVisualContent = (layer: BadgeLayer) => {
|
||||||
if (!activeBadge) return;
|
const raw = layer.text || '';
|
||||||
if (!previewRef.current) {
|
if (raw.includes('{cena}')) {
|
||||||
onSelectBadge(activeBadge);
|
const parts = raw.split('{cena}');
|
||||||
onClose();
|
return (
|
||||||
return;
|
<span>
|
||||||
|
{parts[0]}
|
||||||
|
<span>{currentPriceMain}</span>
|
||||||
|
<sup style={{ fontSize: '0.55em', verticalAlign: 'top', lineHeight: 1.1, marginLeft: '1px' }}>
|
||||||
|
{currentPriceSub}
|
||||||
|
</sup>
|
||||||
|
{parts[1]}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (raw.includes('{centy}')) {
|
||||||
|
const parts = raw.split('{centy}');
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{parts[0]}
|
||||||
|
<sup style={{ fontSize: '0.55em', verticalAlign: 'top', lineHeight: 1.1 }}>{currentPriceSub}</sup>
|
||||||
|
{parts[1]}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaced = raw
|
||||||
|
.replace(/{cela_suma}/g, currentPriceMain)
|
||||||
|
.replace(/{zlava}/g, currentDiscount.replace('-', ''))
|
||||||
|
.replace(/{jednotkova_cena}/g, currentUnitPrice)
|
||||||
|
.replace(/{mena}/g, '€');
|
||||||
|
|
||||||
|
return replaced;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aplikovanie cenovky na produkt: čisté vektorové SVG a čistý PNG (BEZ akéhokoľvek screenshotu nástrojov/rámčekov)
|
||||||
|
const handleApplyBadge = async () => {
|
||||||
|
if (!activeBadge) return;
|
||||||
setIsExportingPng(true);
|
setIsExportingPng(true);
|
||||||
try {
|
try {
|
||||||
const dataUrl = await toPng(previewRef.current, {
|
const cleanSvg = renderCleanBadgeSvg(
|
||||||
backgroundColor: 'transparent',
|
activeBadge,
|
||||||
pixelRatio: 2
|
{
|
||||||
});
|
priceMain: currentPriceMain,
|
||||||
onSelectBadge(activeBadge, dataUrl);
|
priceSub: currentPriceSub,
|
||||||
|
discountText: currentDiscount,
|
||||||
|
unitPrice: currentUnitPrice
|
||||||
|
},
|
||||||
|
activeBadge.width || 110
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanPng = await svgToCleanPng(cleanSvg, 140, 95);
|
||||||
|
onSelectBadge(activeBadge, cleanPng);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Render error:', err);
|
console.error('Apply badge error:', err);
|
||||||
onSelectBadge(activeBadge);
|
onSelectBadge(activeBadge);
|
||||||
onClose();
|
onClose();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -388,7 +516,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4">
|
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4">
|
||||||
<div className="bg-white rounded-3xl shadow-2xl max-w-5xl w-full max-h-[92vh] overflow-hidden flex flex-col border border-gray-100">
|
<div className="bg-white rounded-3xl shadow-2xl max-w-6xl w-full max-h-[94vh] overflow-hidden flex flex-col border border-gray-100">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-slate-50">
|
<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="flex items-center gap-3">
|
||||||
@@ -397,10 +525,10 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-bold text-gray-900">
|
<h2 className="text-base font-bold text-gray-900">
|
||||||
Editor a generátor grafických cenoviek (SVG → PNG)
|
Editor grafických cenoviek (Opakovane použiteľné šablóny)
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Vytvorte vlastnú šablónu, nahrajte SVG tvar, pridajte dynamické premenné a vyrenderujte PNG pre e-mail.
|
Nastavte rozmery, zarovnanie a rozmiestnenie vrstiev. Cena a centy sú dynamické a nezamŕzajú v rastri.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,7 +543,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
<div className="col-span-3 p-4 overflow-y-auto space-y-3 bg-gray-50/50">
|
<div className="col-span-3 p-4 overflow-y-auto space-y-3 bg-gray-50/50">
|
||||||
<div className="flex items-center justify-between px-1">
|
<div className="flex items-center justify-between px-1">
|
||||||
<span className="text-[11px] font-bold uppercase tracking-wider text-gray-400">
|
<span className="text-[11px] font-bold uppercase tracking-wider text-gray-400">
|
||||||
Vaše šablóny ({badges.length})
|
Šablóny ({badges.length})
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleCreateNewTemplate}
|
onClick={handleCreateNewTemplate}
|
||||||
@@ -425,18 +553,6 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{badges.length === 0 ? (
|
|
||||||
<div className="p-6 text-center border-2 border-dashed border-gray-200 rounded-2xl bg-white">
|
|
||||||
<Tag className="w-8 h-8 text-gray-300 mx-auto mb-2" />
|
|
||||||
<p className="text-xs text-gray-500 mb-3">Zatiaľ nemáte žiadne šablóny cenoviek.</p>
|
|
||||||
<button
|
|
||||||
onClick={handleCreateNewTemplate}
|
|
||||||
className="w-full py-2 bg-[#e2001a] text-white text-xs font-bold rounded-xl shadow-xs hover:bg-red-700"
|
|
||||||
>
|
|
||||||
Vytvoriť prvú šablónu
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{badges.map((b) => (
|
{badges.map((b) => (
|
||||||
<div
|
<div
|
||||||
@@ -453,6 +569,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<p className="text-xs font-bold text-gray-800 truncate">{b.name}</p>
|
<p className="text-xs font-bold text-gray-800 truncate">{b.name}</p>
|
||||||
|
{!b.id.startsWith('badge-preset-') && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -463,38 +580,64 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
>
|
>
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="w-full h-12 flex items-center justify-center transform scale-75 origin-center overflow-hidden"
|
className="w-full h-12 flex items-center justify-center transform scale-75 origin-center overflow-hidden pointer-events-none"
|
||||||
dangerouslySetInnerHTML={{ __html: b.svgRaw || '' }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: renderCleanBadgeSvg(
|
||||||
|
b,
|
||||||
|
{
|
||||||
|
priceMain: currentPriceMain,
|
||||||
|
priceSub: currentPriceSub,
|
||||||
|
discountText: currentDiscount,
|
||||||
|
unitPrice: currentUnitPrice
|
||||||
|
},
|
||||||
|
90
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2. Stĺpec: Vektorový Canvas & Náhľad */}
|
{/* 2. Stĺpec: Vektorový Canvas & Náhľad s Drag & Drop */}
|
||||||
<div className="col-span-5 p-6 flex flex-col items-center justify-center bg-slate-950 relative">
|
<div className="col-span-5 p-6 flex flex-col items-center justify-center bg-slate-950 relative">
|
||||||
<div className="absolute top-4 left-4 flex items-center gap-2">
|
<div className="absolute top-4 left-4 flex items-center gap-2">
|
||||||
<span className="text-xs font-semibold text-slate-400 flex items-center gap-1.5">
|
<span className="text-xs font-semibold text-slate-400 flex items-center gap-1.5">
|
||||||
<Eye className="w-3.5 h-3.5" /> Náhľad cenovky
|
<Eye className="w-3.5 h-3.5" /> Náhľad na plátne
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-blue-400 bg-blue-950/60 border border-blue-800/60 px-2.5 py-0.5 rounded-full flex items-center gap-1">
|
<span className="text-[10px] text-blue-400 bg-blue-950/60 border border-blue-800/60 px-2.5 py-0.5 rounded-full flex items-center gap-1">
|
||||||
<Move className="w-3 h-3" /> Drag & Drop textových polí
|
<Move className="w-3 h-3" /> Drag & Drop vrstiev
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeBadge ? (
|
{activeBadge ? (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center w-full">
|
||||||
{/* Vektorové plátno pre export do PNG */}
|
{/* Obal simulujúci veľkosť, zarovnanie a odsadenie */}
|
||||||
<div
|
<div
|
||||||
ref={previewRef}
|
className="w-full max-w-xs flex"
|
||||||
className="relative inline-block select-none"
|
style={{
|
||||||
|
justifyContent:
|
||||||
|
activeBadge.align === 'left'
|
||||||
|
? 'flex-start'
|
||||||
|
: activeBadge.align === 'right'
|
||||||
|
? 'flex-end'
|
||||||
|
: 'center',
|
||||||
|
paddingTop: `${activeBadge.paddingTop || 0}px`,
|
||||||
|
paddingBottom: `${activeBadge.paddingBottom || 0}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative select-none transition-all"
|
||||||
|
style={{
|
||||||
|
width: `${activeBadge.width || 110}px`
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* SVG Podklad */}
|
{/* SVG Podklad */}
|
||||||
<div
|
<div
|
||||||
className="block pointer-events-none"
|
className="block pointer-events-none w-full"
|
||||||
dangerouslySetInnerHTML={{ __html: activeBadge.svgRaw || '' }}
|
dangerouslySetInnerHTML={{ __html: activeBadge.svgRaw || '' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -531,11 +674,10 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-slate-900 bg-blue-500/20 shadow-lg'
|
? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-slate-900 bg-blue-500/20 shadow-lg'
|
||||||
: 'hover:outline hover:outline-1 hover:outline-white/60 hover:bg-white/10'
|
: 'hover:outline hover:outline-1 hover:outline-white/60 hover:bg-white/10'
|
||||||
}`}
|
}`}
|
||||||
title="Kliknite a ťahajte myšou pre zmenu pozície (Drag & Drop)"
|
title="Ťahajte myšou pre zmenu pozície"
|
||||||
>
|
>
|
||||||
{replaceVariables(layer.text || '')}
|
{renderLayerVisualContent(layer)}
|
||||||
|
|
||||||
{/* Indikátor pozície a uchopenia */}
|
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<span className="absolute -top-5 left-1/2 -translate-x-1/2 text-[9px] font-mono font-bold bg-slate-900 text-blue-300 px-1.5 py-0.5 rounded shadow border border-slate-700 pointer-events-none whitespace-nowrap flex items-center gap-1 z-20">
|
<span className="absolute -top-5 left-1/2 -translate-x-1/2 text-[9px] font-mono font-bold bg-slate-900 text-blue-300 px-1.5 py-0.5 rounded shadow border border-slate-700 pointer-events-none whitespace-nowrap flex items-center gap-1 z-20">
|
||||||
<Move className="w-2.5 h-2.5" /> {layer.x}px, {layer.y}px
|
<Move className="w-2.5 h-2.5" /> {layer.x}px, {layer.y}px
|
||||||
@@ -545,19 +687,21 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Dostupné premenné */}
|
{/* Info o premenných a centoch */}
|
||||||
<div className="mt-4 bg-slate-900/80 border border-slate-800 rounded-xl p-3 text-[11px] text-slate-300 space-y-1 max-w-sm">
|
<div className="mt-6 bg-slate-900/80 border border-slate-800 rounded-xl p-3 text-[11px] text-slate-300 space-y-1.5 max-w-sm">
|
||||||
<span className="font-bold text-gray-400 block mb-1">Dostupné dynamické premenné:</span>
|
<span className="font-bold text-gray-400 block mb-0.5">Dostupné dynamické značky:</span>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-1.5 text-[10px]">
|
||||||
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
|
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
|
||||||
{'{zlava}'} → {currentDiscount}
|
{'{cena}'} → {currentPriceMain}
|
||||||
|
<sup>{currentPriceSub}</sup> (bez €)
|
||||||
</span>
|
</span>
|
||||||
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
|
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-emerald-300">
|
||||||
{'{cena}'} → {currentPriceMain},{currentPriceSub} €
|
{'{zlava}'} → {currentDiscount.replace('-', '')}
|
||||||
</span>
|
</span>
|
||||||
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
|
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-blue-300">
|
||||||
{'{jednotkova_cena}'} → {currentUnitPrice}
|
{'{mena}'} → €
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -567,7 +711,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3. Stĺpec: Konfigurácia SVG podkladu a Vrstiev */}
|
{/* 3. Stĺpec: Konfigurácia rozmerov, zarovnania a vrstiev */}
|
||||||
<div className="col-span-4 p-5 overflow-y-auto space-y-5 bg-white">
|
<div className="col-span-4 p-5 overflow-y-auto space-y-5 bg-white">
|
||||||
{activeBadge ? (
|
{activeBadge ? (
|
||||||
<>
|
<>
|
||||||
@@ -582,11 +726,105 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sekcia: Rozmery, zarovnanie a odsadenie cenovky */}
|
||||||
|
<div className="bg-gray-50 p-3.5 rounded-2xl border border-gray-200 space-y-3">
|
||||||
|
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block">
|
||||||
|
Rozmery a Zarovnanie cenovky
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Šírka cenovky */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<label className="text-[11px] font-semibold text-gray-700">Šírka cenovky</label>
|
||||||
|
<span className="text-[11px] font-bold text-gray-900 bg-white px-2 py-0.5 rounded border border-gray-200">
|
||||||
|
{activeBadge.width || 110} px
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="70"
|
||||||
|
max="180"
|
||||||
|
step="2"
|
||||||
|
value={activeBadge.width || 110}
|
||||||
|
onChange={(e) => setActiveBadge({ ...activeBadge, width: Number(e.target.value) })}
|
||||||
|
className="w-full accent-[#e2001a]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Zarovnanie na karte produktu */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-semibold text-gray-700 mb-1">
|
||||||
|
Zarovnanie cenovky na produkte
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-3 gap-1 bg-gray-200 p-0.5 rounded-lg text-xs font-bold">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveBadge({ ...activeBadge, align: 'left' })}
|
||||||
|
className={`py-1.5 flex items-center justify-center gap-1 rounded transition-all ${
|
||||||
|
(activeBadge.align || 'center') === 'left'
|
||||||
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<AlignLeft className="w-3.5 h-3.5" /> Vľavo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveBadge({ ...activeBadge, align: 'center' })}
|
||||||
|
className={`py-1.5 flex items-center justify-center gap-1 rounded transition-all ${
|
||||||
|
(activeBadge.align || 'center') === 'center'
|
||||||
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<AlignCenter className="w-3.5 h-3.5" /> Na stred
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveBadge({ ...activeBadge, align: 'right' })}
|
||||||
|
className={`py-1.5 flex items-center justify-center gap-1 rounded transition-all ${
|
||||||
|
(activeBadge.align || 'center') === 'right'
|
||||||
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<AlignRight className="w-3.5 h-3.5" /> Vpravo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Odsadenie (Padding) */}
|
||||||
|
<div className="grid grid-cols-2 gap-2 pt-1">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] text-gray-600 mb-0.5">Odsadenie zhora (px)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="40"
|
||||||
|
value={activeBadge.paddingTop ?? 4}
|
||||||
|
onChange={(e) => setActiveBadge({ ...activeBadge, paddingTop: Number(e.target.value) })}
|
||||||
|
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] text-gray-600 mb-0.5">Odsadenie zdola (px)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="40"
|
||||||
|
value={activeBadge.paddingBottom ?? 4}
|
||||||
|
onChange={(e) => setActiveBadge({ ...activeBadge, paddingBottom: Number(e.target.value) })}
|
||||||
|
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Upload vlastného SVG podkladu */}
|
{/* Upload vlastného SVG podkladu */}
|
||||||
<div className="border border-dashed border-gray-300 rounded-2xl p-3.5 bg-gray-50/60">
|
<div className="border border-dashed border-gray-300 rounded-2xl p-3 bg-gray-50/60">
|
||||||
<label className="block text-xs font-bold text-gray-800 mb-1 flex items-center gap-1.5">
|
<label className="block text-xs font-bold text-gray-800 mb-1 flex items-center gap-1.5">
|
||||||
<Upload className="w-3.5 h-3.5 text-[#e2001a]" />
|
<Upload className="w-3.5 h-3.5 text-[#e2001a]" />
|
||||||
Nahrať vlastný SVG tvar (Podklad)
|
Nahrať vlastný tvar SVG (Podklad)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -594,37 +832,40 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
onChange={handleSvgUpload}
|
onChange={handleSvgUpload}
|
||||||
className="text-xs text-gray-500 file:mr-2 file:py-1 file:px-2.5 file:rounded-lg file:border-0 file:text-xs file:font-semibold file:bg-gray-200 file:text-gray-700 hover:file:bg-gray-300 cursor-pointer w-full"
|
className="text-xs text-gray-500 file:mr-2 file:py-1 file:px-2.5 file:rounded-lg file:border-0 file:text-xs file:font-semibold file:bg-gray-200 file:text-gray-700 hover:file:bg-gray-300 cursor-pointer w-full"
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-gray-400 mt-1">
|
|
||||||
Nahrajte ľubovoľný tvar: hviezdu, bublinu, štítok alebo percento.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rýchle pridanie premenných ako vrstvy */}
|
{/* Rýchle pridanie vrstiev */}
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block mb-2">
|
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block mb-2">
|
||||||
Pridať vrstvu s premennou
|
Pridať vrstvu
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
<button
|
<button
|
||||||
onClick={() => addTextLayer('-{zlava}')}
|
onClick={() => addTextLayer('{cena}')}
|
||||||
className="text-[11px] font-bold bg-amber-50 text-amber-800 border border-amber-200 hover:bg-amber-100 px-2 py-1 rounded-lg"
|
className="text-[11px] font-bold bg-amber-50 text-amber-800 border border-amber-200 hover:bg-amber-100 px-2 py-1 rounded-lg"
|
||||||
|
>
|
||||||
|
+ Cena bez € ({'{cena}'})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => addTextLayer('-{zlava}')}
|
||||||
|
className="text-[11px] font-bold bg-red-50 text-red-800 border border-red-200 hover:bg-red-100 px-2 py-1 rounded-lg"
|
||||||
>
|
>
|
||||||
+ Zľava %
|
+ Zľava %
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => addTextLayer('{cena}')}
|
onClick={() => addTextLayer('€')}
|
||||||
className="text-[11px] font-bold bg-emerald-50 text-emerald-800 border border-emerald-200 hover:bg-emerald-100 px-2 py-1 rounded-lg"
|
className="text-[11px] font-bold bg-blue-50 text-blue-800 border border-blue-200 hover:bg-blue-100 px-2 py-1 rounded-lg"
|
||||||
>
|
>
|
||||||
+ Cena €
|
+ Znak €
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => addTextLayer('{jednotkova_cena}')}
|
onClick={() => addTextLayer('{jednotkova_cena}')}
|
||||||
className="text-[11px] font-bold bg-blue-50 text-blue-800 border border-blue-200 hover:bg-blue-100 px-2 py-1 rounded-lg"
|
className="text-[11px] font-bold bg-emerald-50 text-emerald-800 border border-emerald-200 hover:bg-emerald-100 px-2 py-1 rounded-lg"
|
||||||
>
|
>
|
||||||
+ Merná cena
|
+ Merná cena
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => addTextLayer('SUPER CENA')}
|
onClick={() => addTextLayer('AKCIA')}
|
||||||
className="text-[11px] font-bold bg-gray-100 text-gray-800 border border-gray-200 hover:bg-gray-200 px-2 py-1 rounded-lg"
|
className="text-[11px] font-bold bg-gray-100 text-gray-800 border border-gray-200 hover:bg-gray-200 px-2 py-1 rounded-lg"
|
||||||
>
|
>
|
||||||
+ Vlastný text
|
+ Vlastný text
|
||||||
@@ -632,7 +873,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Zoznam vrstiev */}
|
{/* Zoznam a nastavenie vrstiev */}
|
||||||
<div className="space-y-3 pt-2 border-t border-gray-100">
|
<div className="space-y-3 pt-2 border-t border-gray-100">
|
||||||
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block">
|
<span className="text-xs font-bold text-gray-800 uppercase tracking-wider block">
|
||||||
Vrstvy ({activeBadge.layersConfig.length})
|
Vrstvy ({activeBadge.layersConfig.length})
|
||||||
@@ -653,7 +894,6 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nastavenie aktívnej vrstvy */}
|
|
||||||
{activeLayer && (
|
{activeLayer && (
|
||||||
<div className="space-y-3 bg-gray-50 p-3.5 rounded-2xl border border-gray-200">
|
<div className="space-y-3 bg-gray-50 p-3.5 rounded-2xl border border-gray-200">
|
||||||
<div>
|
<div>
|
||||||
@@ -692,19 +932,16 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Font nastavenie */}
|
{/* Font výber */}
|
||||||
<div className="space-y-2 pt-2 border-t border-gray-200">
|
<div className="space-y-2 pt-2 border-t border-gray-200">
|
||||||
<label className="block text-[11px] font-semibold text-gray-700">
|
<label className="block text-[11px] font-semibold text-gray-700">
|
||||||
Písmo (Font) a Hrúbka
|
Písmo (Font) a Hrúbka
|
||||||
</label>
|
</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">
|
<div className="grid grid-cols-3 gap-1 bg-gray-200 p-0.5 rounded-lg text-[10px] font-bold text-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => updateActiveLayer({ fontSource: 'google' })}
|
||||||
updateActiveLayer({ fontSource: 'google' });
|
|
||||||
}}
|
|
||||||
className={`py-1 rounded ${
|
className={`py-1 rounded ${
|
||||||
(activeLayer.fontSource || 'google') === 'google'
|
(activeLayer.fontSource || 'google') === 'google'
|
||||||
? 'bg-white text-gray-900 shadow-sm'
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
@@ -715,9 +952,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => updateActiveLayer({ fontSource: 'adobe' })}
|
||||||
updateActiveLayer({ fontSource: 'adobe' });
|
|
||||||
}}
|
|
||||||
className={`py-1 rounded ${
|
className={`py-1 rounded ${
|
||||||
activeLayer.fontSource === 'adobe'
|
activeLayer.fontSource === 'adobe'
|
||||||
? 'bg-white text-gray-900 shadow-sm'
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
@@ -728,9 +963,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => updateActiveLayer({ fontSource: 'system' })}
|
||||||
updateActiveLayer({ fontSource: 'system' });
|
|
||||||
}}
|
|
||||||
className={`py-1 rounded ${
|
className={`py-1 rounded ${
|
||||||
activeLayer.fontSource === 'system'
|
activeLayer.fontSource === 'system'
|
||||||
? 'bg-white text-gray-900 shadow-sm'
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
@@ -741,11 +974,8 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Google Fonts voľba */}
|
|
||||||
{(activeLayer.fontSource || 'google') === 'google' && (
|
{(activeLayer.fontSource || 'google') === 'google' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
|
||||||
<label className="block text-[10px] text-gray-500 mb-0.5">Výber Google Fontu</label>
|
|
||||||
<select
|
<select
|
||||||
value={
|
value={
|
||||||
POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins'))
|
POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins'))
|
||||||
@@ -765,17 +995,14 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
{f.name}
|
{f.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
<option value="custom">Iný Google Font (zadať vlastný)...</option>
|
<option value="custom">Iný Google Font...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
|
|
||||||
{(!POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins')) ||
|
{(!POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins')) ||
|
||||||
activeLayer.fontFamily === 'custom') && (
|
activeLayer.fontFamily === 'custom') && (
|
||||||
<div>
|
|
||||||
<label className="block text-[10px] text-gray-500 mb-0.5">Názov Google Fontu</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="napr. Montserrat, Oswald, Syne"
|
placeholder="Názov Google fontu (napr. Montserrat)"
|
||||||
value={activeLayer.fontFamily === 'custom' ? '' : activeLayer.fontFamily || ''}
|
value={activeLayer.fontFamily === 'custom' ? '' : activeLayer.fontFamily || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
@@ -784,21 +1011,15 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
}}
|
}}
|
||||||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
|
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Adobe Fonts voľba */}
|
|
||||||
{activeLayer.fontSource === 'adobe' && (
|
{activeLayer.fontSource === 'adobe' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
|
||||||
<label className="block text-[10px] text-gray-500 mb-0.5">
|
|
||||||
Adobe Typekit CSS URL
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://use.typekit.net/xxxx.css"
|
placeholder="Adobe Typekit CSS URL"
|
||||||
value={activeLayer.fontUrl || ''}
|
value={activeLayer.fontUrl || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const url = e.target.value;
|
const url = e.target.value;
|
||||||
@@ -809,14 +1030,9 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
}}
|
}}
|
||||||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-mono text-[10px]"
|
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="napr. futura-pt, omnes-pro"
|
placeholder="Font Family (napr. futura-pt)"
|
||||||
value={activeLayer.fontFamily || ''}
|
value={activeLayer.fontFamily || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const family = e.target.value;
|
const family = e.target.value;
|
||||||
@@ -828,29 +1044,22 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
|
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Systémové písma */}
|
|
||||||
{activeLayer.fontSource === 'system' && (
|
{activeLayer.fontSource === 'system' && (
|
||||||
<div>
|
|
||||||
<label className="block text-[10px] text-gray-500 mb-0.5">Systémový font</label>
|
|
||||||
<select
|
<select
|
||||||
value={activeLayer.fontFamily || 'Arial'}
|
value={activeLayer.fontFamily || 'Arial Narrow'}
|
||||||
onChange={(e) => updateActiveLayer({ fontFamily: e.target.value, fontSource: 'system' })}
|
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"
|
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
|
||||||
>
|
>
|
||||||
<option value="Arial">Arial</option>
|
|
||||||
<option value="Arial Narrow">Arial Narrow</option>
|
<option value="Arial Narrow">Arial Narrow</option>
|
||||||
|
<option value="Arial">Arial</option>
|
||||||
<option value="Impact">Impact</option>
|
<option value="Impact">Impact</option>
|
||||||
<option value="Trebuchet MS">Trebuchet MS</option>
|
<option value="Trebuchet MS">Trebuchet MS</option>
|
||||||
<option value="Helvetica">Helvetica</option>
|
<option value="Helvetica">Helvetica</option>
|
||||||
<option value="Verdana">Verdana</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hrúbka fontu */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] text-gray-500 mb-0.5">Hrúbka písma (Weight)</label>
|
<label className="block text-[10px] text-gray-500 mb-0.5">Hrúbka písma (Weight)</label>
|
||||||
<select
|
<select
|
||||||
@@ -904,14 +1113,15 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Uloženie šablóny do databázy */}
|
{/* Tlačidlo: Uložiť šablónu do DB */}
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleSaveToSupabase}
|
onClick={handleSaveToSupabase}
|
||||||
disabled={isSavingDb}
|
disabled={isSavingDb}
|
||||||
className="w-full py-2 bg-gray-900 text-white text-xs font-bold rounded-xl hover:bg-black transition-colors"
|
className="w-full py-2.5 bg-gray-900 hover:bg-black text-white text-xs font-bold rounded-xl transition-colors shadow-sm flex items-center justify-center gap-1.5"
|
||||||
>
|
>
|
||||||
{isSavingDb ? 'Ukladám do Supabase...' : 'Uložiť túto šablónu do Supabase'}
|
<Check className="w-4 h-4 text-emerald-400" />
|
||||||
|
{isSavingDb ? 'Ukladám do Supabase...' : 'Uložiť túto šablónu (pre opakované použitie)'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -930,7 +1140,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
|||||||
className="px-6 py-2.5 text-sm font-bold bg-[#e2001a] text-white rounded-xl hover:bg-red-700 disabled:opacity-50 transition-colors shadow-md flex items-center gap-2"
|
className="px-6 py-2.5 text-sm font-bold bg-[#e2001a] text-white rounded-xl hover:bg-red-700 disabled:opacity-50 transition-colors shadow-md flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Check className="w-4 h-4" />
|
<Check className="w-4 h-4" />
|
||||||
{isExportingPng ? 'Generujem PNG cenovku...' : 'Aplikovať cenovku na produkt'}
|
{isExportingPng ? 'Pripravujem cenovku...' : 'Aplikovať cenovku na produkt'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NewsletterModule, ProductItem } 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 } 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';
|
||||||
@@ -11,13 +11,15 @@ interface ConfigPanelProps {
|
|||||||
onUpdateModule: (updated: NewsletterModule) => void;
|
onUpdateModule: (updated: NewsletterModule) => void;
|
||||||
onOpenBadgeManager?: (productId: string) => void;
|
onOpenBadgeManager?: (productId: string) => void;
|
||||||
onOpenImport?: () => void;
|
onOpenImport?: () => void;
|
||||||
|
availableBadges?: BadgeTemplate[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||||
module,
|
module,
|
||||||
onUpdateModule,
|
onUpdateModule,
|
||||||
onOpenBadgeManager,
|
onOpenBadgeManager,
|
||||||
onOpenImport
|
onOpenImport,
|
||||||
|
availableBadges = []
|
||||||
}) => {
|
}) => {
|
||||||
if (!module) {
|
if (!module) {
|
||||||
return (
|
return (
|
||||||
@@ -510,19 +512,198 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grafická cenovka - Editor cenoviek */}
|
{/* Grafická cenovka a šablóna */}
|
||||||
<div className="pt-2 border-t border-gray-200 flex items-center justify-between">
|
<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">
|
<div className="flex items-center gap-1.5">
|
||||||
<Tag className="w-3.5 h-3.5 text-red-600" />
|
<Tag className="w-3.5 h-3.5 text-red-600" />
|
||||||
<span className="text-xs font-semibold text-gray-700">Grafická cenovka</span>
|
<span className="text-xs font-bold text-gray-800">Cenovka</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => onOpenBadgeManager && onOpenBadgeManager(p.id)}
|
onClick={() => onOpenBadgeManager && onOpenBadgeManager(p.id)}
|
||||||
className="text-xs font-bold text-red-600 hover:text-white hover:bg-red-600 border border-red-200 px-2.5 py-1 rounded-lg transition-all"
|
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"
|
||||||
>
|
>
|
||||||
{p.customBadgePngUrl ? 'Upraviť v editore' : 'Otvoriť Editor cenoviek'}
|
Otvoriť Editor cenoviek
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Výber opakovane použiteľnej š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 || (p.customBadgePngUrl ? 'custom_png' : 'default_coop')}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
const products = [...module.data.products];
|
||||||
|
if (val === 'default_coop') {
|
||||||
|
products[pIdx].badgeCustomTemplate = undefined;
|
||||||
|
products[pIdx].customBadgePngUrl = 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 ?? 110;
|
||||||
|
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;
|
||||||
|
products[pIdx].customBadgePngUrl = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateData({ products });
|
||||||
|
}}
|
||||||
|
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
|
||||||
|
>
|
||||||
|
<option value="default_coop">Predvolená COOP cenovka (červeno-biela)</option>
|
||||||
|
{(availableBadges || []).map((b) => (
|
||||||
|
<option key={b.id} value={b.id}>
|
||||||
|
{b.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tlačidlo: Aplikovať túto cenovku na všetky produkty v sekcii */}
|
||||||
|
{p.badgeCustomTemplate && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Naozaj chcete aplikovať šablónu "${p.badgeCustomTemplate?.name}" na všetky produkty v tejto sekcii?`)) {
|
||||||
|
const products = (module.data.products || []).map((prod: ProductItem) => ({
|
||||||
|
...prod,
|
||||||
|
badgeCustomTemplate: p.badgeCustomTemplate,
|
||||||
|
badgeId: p.badgeCustomTemplate?.id,
|
||||||
|
badgeWidth: p.badgeWidth || p.badgeCustomTemplate?.width || 110,
|
||||||
|
badgeAlign: p.badgeAlign || p.badgeCustomTemplate?.align || 'center',
|
||||||
|
badgePaddingTop: p.badgePaddingTop ?? p.badgeCustomTemplate?.paddingTop ?? 4,
|
||||||
|
badgePaddingBottom: p.badgePaddingBottom ?? p.badgeCustomTemplate?.paddingBottom ?? 4,
|
||||||
|
customBadgePngUrl: undefined
|
||||||
|
}));
|
||||||
|
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ť túto cenovku pre všetky produkty v sekcii
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Rýchle ovládanie veľkosti (Width), zarovnania a paddingu */}
|
||||||
|
<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 ? 110 : 95)} px
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="70"
|
||||||
|
max="180"
|
||||||
|
step="2"
|
||||||
|
value={p.badgeWidth || (p.badgeCustomTemplate ? 110 : 95)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const products = [...module.data.products];
|
||||||
|
products[pIdx].badgeWidth = Number(e.target.value);
|
||||||
|
updateData({ products });
|
||||||
|
}}
|
||||||
|
className="w-full accent-red-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Zarovnanie */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-gray-600 font-semibold block mb-1">Zarovnanie</span>
|
||||||
|
<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={() => {
|
||||||
|
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>
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
||||||
|
|
||||||
|
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 {
|
||||||
|
priceMain: string;
|
||||||
|
priceSub: string;
|
||||||
|
discountText?: string;
|
||||||
|
unitPrice?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vygeneruje 100% čistý vektorový SVG reťazec cenovky bez akýchkoľvek
|
||||||
|
* editovacích rámčekov, tooltippov či ovládacích prvkov.
|
||||||
|
* Centy sú naformátované ako horný index (tspan dy="-0.45em").
|
||||||
|
* Cena je predvolene bez znaku €.
|
||||||
|
*/
|
||||||
|
export function renderCleanBadgeSvg(
|
||||||
|
template: BadgeTemplate,
|
||||||
|
product: BadgeProductData,
|
||||||
|
overrideWidth?: number
|
||||||
|
): string {
|
||||||
|
const pMain = product.priceMain || '0';
|
||||||
|
const pSub = product.priceSub || '00';
|
||||||
|
const discount = (product.discountText || '').replace('-', '');
|
||||||
|
const unitPrice = product.unitPrice || '';
|
||||||
|
|
||||||
|
// Získanie viewBox alebo predvolených rozmerov
|
||||||
|
let vb = '0 0 140 95';
|
||||||
|
let vbWidth = 140;
|
||||||
|
let vbHeight = 95;
|
||||||
|
|
||||||
|
if (template.svgRaw) {
|
||||||
|
const vbMatch = template.svgRaw.match(/viewBox=["']([^"']+)["']/i);
|
||||||
|
if (vbMatch) {
|
||||||
|
vb = vbMatch[1];
|
||||||
|
const parts = vb.trim().split(/\s+/).map(Number);
|
||||||
|
if (parts.length === 4 && parts[2] > 0 && parts[3] > 0) {
|
||||||
|
vbWidth = parts[2];
|
||||||
|
vbHeight = parts[3];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const wMatch = template.svgRaw.match(/width=["']([^"']+)["']/i);
|
||||||
|
const hMatch = template.svgRaw.match(/height=["']([^"']+)["']/i);
|
||||||
|
if (wMatch && hMatch) {
|
||||||
|
const w = parseFloat(wMatch[1]);
|
||||||
|
const h = parseFloat(hMatch[1]);
|
||||||
|
if (w > 0 && h > 0) {
|
||||||
|
vbWidth = w;
|
||||||
|
vbHeight = h;
|
||||||
|
vb = `0 0 ${w} ${h}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrahovanie vnútorného obsahu SVG (medzi <svg...> a </svg>)
|
||||||
|
let innerBg = '';
|
||||||
|
if (template.svgRaw) {
|
||||||
|
const inside = template.svgRaw.replace(/<\/?svg[^>]*>/gi, '').trim();
|
||||||
|
innerBg = inside;
|
||||||
|
} else {
|
||||||
|
innerBg = `<rect width="${vbWidth}" height="${vbHeight}" rx="14" fill="#ffd100"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Šírka výsledného SVG
|
||||||
|
const displayWidth = overrideWidth || template.width || 110;
|
||||||
|
const displayHeight = Math.round((displayWidth / vbWidth) * vbHeight);
|
||||||
|
|
||||||
|
// Zostavenie textových vrstiev
|
||||||
|
const textLayersSvg = (template.layersConfig || []).map((layer) => {
|
||||||
|
const rawText = layer.text || '';
|
||||||
|
const fontSize = layer.fontSize || 16;
|
||||||
|
const subFontSize = Math.max(10, Math.round(fontSize * 0.55));
|
||||||
|
const fontWeight = layer.fontWeight || '700';
|
||||||
|
const fill = layer.fill || '#ffffff';
|
||||||
|
const fontFamily = layer.fontFamily
|
||||||
|
? `'${layer.fontFamily}', -apple-system, BlinkMacSystemFont, Arial, sans-serif`
|
||||||
|
: `'Arial Narrow', Arial, sans-serif`;
|
||||||
|
|
||||||
|
let anchor = 'middle';
|
||||||
|
if (layer.align === 'left') anchor = 'start';
|
||||||
|
if (layer.align === 'right') anchor = 'end';
|
||||||
|
|
||||||
|
// Formátovanie textu: ak obsahuje {cena}, centy vložíme do <tspan> ako horný index
|
||||||
|
let contentSvg = '';
|
||||||
|
if (rawText.includes('{cena}')) {
|
||||||
|
const parts = rawText.split('{cena}');
|
||||||
|
const before = escapeXml(parts[0] || '');
|
||||||
|
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>`;
|
||||||
|
} else {
|
||||||
|
let replaced = rawText
|
||||||
|
.replace(/{cela_suma}/g, pMain)
|
||||||
|
.replace(/{centy}/g, pSub)
|
||||||
|
.replace(/{zlava}/g, discount)
|
||||||
|
.replace(/{jednotkova_cena}/g, unitPrice)
|
||||||
|
.replace(/{mena}/g, '€');
|
||||||
|
contentSvg = escapeXml(replaced);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<text x="${layer.x}" y="${layer.y}" text-anchor="${anchor}" dominant-baseline="central" font-family="${fontFamily}" font-size="${fontSize}px" font-weight="${fontWeight}" fill="${fill}">${contentSvg}</text>`;
|
||||||
|
}).join('\n ');
|
||||||
|
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${vb}" width="${displayWidth}" height="${displayHeight}" style="display:block;max-width:100%;height:auto;">
|
||||||
|
${innerBg}
|
||||||
|
${textLayersSvg}
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skonvertuje SVG reťazec do čistého PNG Data URL v prehliadači
|
||||||
|
* (bez akýchkoľvek DOM prvkov alebo modrých drag & drop rámčekov).
|
||||||
|
*/
|
||||||
|
export async function svgToCleanPng(
|
||||||
|
svgString: string,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): Promise<string> {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return `data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
const img = new Image();
|
||||||
|
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(svgBlob);
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const scale = 2; // Vysoké rozlíšenie Retina
|
||||||
|
canvas.width = width * scale;
|
||||||
|
canvas.height = height * scale;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
resolve(canvas.toDataURL('image/png'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Canvas export fallback to SVG data URL', e);
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
resolve(`data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
resolve(`data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = url;
|
||||||
|
} catch (err) {
|
||||||
|
resolve(`data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeXml(unsafe: string): string {
|
||||||
|
if (!unsafe) return '';
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
+52
-11
@@ -1,5 +1,6 @@
|
|||||||
import { NewsletterCampaign, ProductItem } from '@/types/newsletter';
|
import { NewsletterCampaign, ProductItem } from '@/types/newsletter';
|
||||||
import { SUPABASE_ASSETS_BASE } from './defaultCampaign';
|
import { SUPABASE_ASSETS_BASE } from './defaultCampaign';
|
||||||
|
import { renderCleanBadgeSvg } from './badgeUtils';
|
||||||
|
|
||||||
export function renderEmailHtml(campaign: NewsletterCampaign): string {
|
export function renderEmailHtml(campaign: NewsletterCampaign): string {
|
||||||
const renderedModules = campaign.modules.map((m) => renderModule(m, campaign)).join('\n');
|
const renderedModules = campaign.modules.map((m) => renderModule(m, campaign)).join('\n');
|
||||||
@@ -620,19 +621,58 @@ function renderExactProductCard(
|
|||||||
</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 12px 8px; text-align: center;" valign="bottom">
|
||||||
<!-- Vyladená Cenovka Premium v8/v11 1:1 alebo vyrenderovaná PNG cenovka z editora -->
|
${(() => {
|
||||||
${p.customBadgePngUrl ? `
|
const bWidth = p.badgeWidth || (p.badgeCustomTemplate ? (p.badgeCustomTemplate.width || 110) : 95);
|
||||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" style="margin: 0 auto;">
|
const bAlign = p.badgeAlign || (p.badgeCustomTemplate ? (p.badgeCustomTemplate.align || 'center') : '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;';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Ak produkt má vlastnú dynamickú šablónu cenovky (čistý SVG vektor bez editovacích prvkov a so živou cenou)
|
||||||
|
if (p.badgeCustomTemplate) {
|
||||||
|
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>
|
<tr>
|
||||||
<td align="center">
|
<td align="${bAlign}">
|
||||||
<img src="${escapeHtml(p.customBadgePngUrl)}" width="110" alt="Cenovka" style="display: block; margin: 0 auto; max-width: 120px;">
|
${renderCleanBadgeSvg(p.badgeCustomTemplate, p, bWidth)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>` : `
|
${p.showSlovakFlag ? `
|
||||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" style="width: 95px; margin: 0 auto;">
|
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="right" style="line-height: 0; padding: 0;">
|
||||||
<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;">
|
<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. Ak je nahraná statická PNG cenovka
|
||||||
|
if (p.customBadgePngUrl) {
|
||||||
|
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}">
|
||||||
|
<img src="${escapeHtml(p.customBadgePngUrl)}" width="${bWidth}" alt="Cenovka" style="display: block; width: 100%; max-width: ${bWidth}px; margin: 0 auto;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Predvolená COOP cenovka s plnou podporou šírky, zarovnania a odsadenia
|
||||||
|
const mainFontSize = Math.max(28, Math.round(bWidth * 0.54));
|
||||||
|
const subFontSize = Math.max(14, Math.round(bWidth * 0.27));
|
||||||
|
|
||||||
|
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}">
|
||||||
|
<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: ${bWidth}px; overflow: hidden;">
|
||||||
${badgeCustomTxt ? `
|
${badgeCustomTxt ? `
|
||||||
<tr>
|
<tr>
|
||||||
<td bgcolor="${badgeHdrBg}" style="padding: 4px 4px; text-align: center; background-color: ${badgeHdrBg};">
|
<td bgcolor="${badgeHdrBg}" style="padding: 4px 4px; text-align: center; background-color: ${badgeHdrBg};">
|
||||||
@@ -647,7 +687,7 @@ function renderExactProductCard(
|
|||||||
</tr>` : '')}
|
</tr>` : '')}
|
||||||
<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 12px 12px;">
|
<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>
|
<span style="font-size: ${mainFontSize}px;">${escapeHtml(p.priceMain)}</span><sup style="font-size: ${subFontSize}px; vertical-align: top; line-height: 1.2;">${escapeHtml(p.priceSub)}</sup>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -659,7 +699,8 @@ function renderExactProductCard(
|
|||||||
<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="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>
|
</td>
|
||||||
</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>` : ''}
|
${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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
+10
-1
@@ -26,6 +26,11 @@ export interface ProductItem {
|
|||||||
discountText: string; // napr. "-23%" alebo "23%"
|
discountText: string; // napr. "-23%" alebo "23%"
|
||||||
badgeText: string; // napr. "AKCIA" alebo "SUPER CENA"
|
badgeText: string; // napr. "AKCIA" alebo "SUPER CENA"
|
||||||
badgeId?: string; // priradená cenovka z databázy
|
badgeId?: string; // priradená cenovka z databázy
|
||||||
|
badgeCustomTemplate?: BadgeTemplate; // Uložená dynamická šablóna cenovky (bez fixnej ceny)
|
||||||
|
badgeWidth?: number; // Nastaviteľná šírka/veľkosť cenovky v px (napr. 70 až 180)
|
||||||
|
badgeAlign?: 'left' | 'center' | 'right'; // Zarovnanie cenovky (vľavo, stred, vpravo)
|
||||||
|
badgePaddingTop?: number; // Odsadenie zhora v px
|
||||||
|
badgePaddingBottom?: number; // Odsadenie zdola v px
|
||||||
validityOverride?: string; // lokálny override napr. "2. 5. – 15. 5."
|
validityOverride?: string; // lokálny override napr. "2. 5. – 15. 5."
|
||||||
stores: {
|
stores: {
|
||||||
potraviny: boolean;
|
potraviny: boolean;
|
||||||
@@ -34,7 +39,7 @@ export interface ProductItem {
|
|||||||
};
|
};
|
||||||
cardBgColor?: string; // HEX napr. "#feeaea" (fantasticky), "#fef0c7" (domace), "#dcb085" (grill)
|
cardBgColor?: string; // HEX napr. "#feeaea" (fantasticky), "#fef0c7" (domace), "#dcb085" (grill)
|
||||||
showSlovakFlag?: boolean;
|
showSlovakFlag?: boolean;
|
||||||
customBadgePngUrl?: string; // Vyrenderovaná vlastná PNG cenovka z editora
|
customBadgePngUrl?: string; // Voliteľná statická PNG cenovka
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnnouncementModuleData {
|
export interface AnnouncementModuleData {
|
||||||
@@ -160,4 +165,8 @@ export interface BadgeTemplate {
|
|||||||
svgRaw?: string;
|
svgRaw?: string;
|
||||||
layersConfig: BadgeLayer[];
|
layersConfig: BadgeLayer[];
|
||||||
previewImageUrl?: string;
|
previewImageUrl?: string;
|
||||||
|
width?: number; // Šírka cenovky v px (napr. 70 až 180)
|
||||||
|
align?: 'left' | 'center' | 'right'; // Zarovnanie (vľavo, stred, vpravo)
|
||||||
|
paddingTop?: number;
|
||||||
|
paddingBottom?: number;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user