feat: premenne {cena_s_kartou} a {zlava_s_kartou} v cenovkach (uloha 12)

- bezne produkty maju volitelne polia Cena s kartou / Zlava s kartou,
  vernostne produkty pouzivaju svoje existujuce hodnoty
- editor cenoviek: tlacidla na pridanie vrstiev a nahlad premennych
- jedinecne ID novych vrstiev (rychle pridanie viacerych vrstiev)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-19 10:30:32 +02:00
1 parent 22a9a51d00
commit 3bd93fde20
6 files changed
+138 -43

No files matched your search

+3
View File
@@ -835,6 +835,9 @@ export default function Home() {
currentPriceMain={targetProduct?.priceMain || targetProduct?.cardPriceMain || '1'} currentPriceMain={targetProduct?.priceMain || targetProduct?.cardPriceMain || '1'}
currentPriceSub={targetProduct?.priceSub || targetProduct?.cardPriceSub || '99'} currentPriceSub={targetProduct?.priceSub || targetProduct?.cardPriceSub || '99'}
currentUnitPrice={targetProduct?.unitPrice || targetProduct?.cardUnitPrice || '1kg = 4,91 €'} currentUnitPrice={targetProduct?.unitPrice || targetProduct?.cardUnitPrice || '1kg = 4,91 €'}
currentCardPriceMain={targetProduct?.cardPriceMain || undefined}
currentCardPriceSub={targetProduct?.cardPriceSub || undefined}
currentCardDiscount={targetProduct?.cardDiscount || undefined}
/> />
{/* Modál Moje newslettre (Ukladanie, duplikácia, vytváranie a prepínanie) */} {/* Modál Moje newslettre (Ukladanie, duplikácia, vytváranie a prepínanie) */}
+50 -28
View File
@@ -3,7 +3,7 @@
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 { DEFAULT_BADGE_TEMPLATES, renderCleanBadgeSvg, svgToCleanPng } from '@/lib/badgeUtils'; import { DEFAULT_BADGE_TEMPLATES, renderCleanBadgeSvg, svgToCleanPng, BADGE_PRICE_TOKEN_RE, badgePriceParts, replaceBadgeTextVars } from '@/lib/badgeUtils';
import { Tag, Plus, Check, Trash2, X, Upload, Eye, Move, AlignLeft, AlignCenter, AlignRight, ZoomIn, ZoomOut, Copy } from 'lucide-react'; import { Tag, Plus, Check, Trash2, X, Upload, Eye, Move, AlignLeft, AlignCenter, AlignRight, ZoomIn, ZoomOut, Copy } from 'lucide-react';
// Riadok tabuľky badges v Supabase pre danú šablónu // Riadok tabuľky badges v Supabase pre danú šablónu
@@ -34,6 +34,9 @@ interface BadgeEditorModalProps {
currentPriceMain?: string; currentPriceMain?: string;
currentPriceSub?: string; currentPriceSub?: string;
currentUnitPrice?: string; currentUnitPrice?: string;
currentCardPriceMain?: string;
currentCardPriceSub?: string;
currentCardDiscount?: string;
} }
export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
@@ -43,7 +46,10 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
currentDiscount = '25%', currentDiscount = '25%',
currentPriceMain = '1', currentPriceMain = '1',
currentPriceSub = '99', currentPriceSub = '99',
currentUnitPrice = '1kg = 4,91 €' currentUnitPrice = '1kg = 4,91 €',
currentCardPriceMain = '1',
currentCardPriceSub = '49',
currentCardDiscount = '30%'
}) => { }) => {
const POPULAR_FONTS = [ const POPULAR_FONTS = [
{ name: 'Poppins', source: 'google' as const }, { name: 'Poppins', source: 'google' as const },
@@ -391,7 +397,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
const addTextLayer = (presetText: string = 'Text') => { const addTextLayer = (presetText: string = 'Text') => {
if (!activeBadge) return; if (!activeBadge) return;
const newL: BadgeLayer = { const newL: BadgeLayer = {
id: `l-${Date.now()}`, id: `l-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
type: 'text', type: 'text',
text: presetText, text: presetText,
x: 70, x: 70,
@@ -538,6 +544,17 @@ 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 €
// Ceny produktu, pre ktorý sa cenovka upravuje (náhľad premenných)
const previewProduct = {
priceMain: currentPriceMain,
priceSub: currentPriceSub,
discountText: currentDiscount,
unitPrice: currentUnitPrice,
cardPriceMain: currentCardPriceMain,
cardPriceSub: currentCardPriceSub,
cardDiscount: currentCardDiscount
};
const renderLayerVisualContent = (layer: BadgeLayer) => { const renderLayerVisualContent = (layer: BadgeLayer) => {
const raw = layer.text || ''; const raw = layer.text || '';
const baseFontSize = layer.fontSize || 16; const baseFontSize = layer.fontSize || 16;
@@ -545,12 +562,14 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
const subY = layer.indexOffsetY !== undefined ? `${layer.indexOffsetY}px` : '-10px'; const subY = layer.indexOffsetY !== undefined ? `${layer.indexOffsetY}px` : '-10px';
const subX = layer.indexOffsetX !== undefined ? `${layer.indexOffsetX}px` : '2px'; const subX = layer.indexOffsetX !== undefined ? `${layer.indexOffsetX}px` : '2px';
if (raw.includes('{cena}')) { const priceMatch = raw.match(BADGE_PRICE_TOKEN_RE);
const parts = raw.split('{cena}'); if (priceMatch) {
const [tokMain, tokSub] = badgePriceParts(priceMatch[1], previewProduct);
const idx = priceMatch.index || 0;
return ( return (
<span> <span>
{parts[0]} {replaceBadgeTextVars(raw.slice(0, idx), previewProduct)}
<span>{currentPriceMain}</span> <span>{tokMain}</span>
<span <span
style={{ style={{
display: 'inline-block', display: 'inline-block',
@@ -560,9 +579,9 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
lineHeight: 1 lineHeight: 1
}} }}
> >
{currentPriceSub} {tokSub}
</span> </span>
{parts[1]} {replaceBadgeTextVars(raw.slice(idx + priceMatch[0].length), previewProduct)}
</span> </span>
); );
} }
@@ -588,13 +607,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
); );
} }
const replaced = raw return replaceBadgeTextVars(raw, previewProduct);
.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) // Aplikovanie cenovky na produkt: čisté vektorové SVG a čistý PNG (BEZ akéhokoľvek screenshotu nástrojov/rámčekov)
@@ -604,12 +617,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
try { try {
const cleanSvg = renderCleanBadgeSvg( const cleanSvg = renderCleanBadgeSvg(
activeBadge, activeBadge,
{ previewProduct,
priceMain: currentPriceMain,
priceSub: currentPriceSub,
discountText: currentDiscount,
unitPrice: currentUnitPrice
},
activeBadge.width || 110 activeBadge.width || 110
); );
@@ -709,12 +717,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: renderCleanBadgeSvg( __html: renderCleanBadgeSvg(
b, b,
{ previewProduct,
priceMain: currentPriceMain,
priceSub: currentPriceSub,
discountText: currentDiscount,
unitPrice: currentUnitPrice
},
90 90
) )
}} }}
@@ -876,6 +879,13 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-emerald-300"> <span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-emerald-300">
{'{zlava}'} &rarr; {currentDiscount.replace('-', '')} {'{zlava}'} &rarr; {currentDiscount.replace('-', '')}
</span> </span>
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
{'{cena_s_kartou}'} &rarr; {currentCardPriceMain}
<sup>{currentCardPriceSub}</sup>
</span>
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-emerald-300">
{'{zlava_s_kartou}'} &rarr; {currentCardDiscount.replace('-', '')}
</span>
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-blue-300"> <span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-blue-300">
{'{mena}'} &rarr; € {'{mena}'} &rarr; €
</span> </span>
@@ -1032,6 +1042,18 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
> >
+ Zľava % + Zľava %
</button> </button>
<button
onClick={() => addTextLayer('{cena_s_kartou}')}
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 s kartou
</button>
<button
onClick={() => addTextLayer('-{zlava_s_kartou}')}
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 s kartou
</button>
<button <button
onClick={() => addTextLayer('€')} onClick={() => addTextLayer('€')}
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-blue-50 text-blue-800 border border-blue-200 hover:bg-blue-100 px-2 py-1 rounded-lg"
+46
View File
@@ -656,6 +656,52 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
</div> </div>
</div> </div>
{/* Cena s kartou (voliteľné) – premenné {cena_s_kartou} a {zlava_s_kartou} v cenovke */}
<div className="grid grid-cols-3 gap-2 p-2 rounded-lg bg-amber-50/60 border border-amber-100">
<div>
<label className="text-[10px] text-gray-500">S kartou (€)</label>
<input
type="text"
value={p.cardPriceMain || ''}
onChange={(e) => {
const products = [...module.data.products];
products[pIdx].cardPriceMain = e.target.value;
updateData({ products });
}}
placeholder="—"
className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold"
/>
</div>
<div>
<label className="text-[10px] text-gray-500">Centy</label>
<input
type="text"
value={p.cardPriceSub || ''}
onChange={(e) => {
const products = [...module.data.products];
products[pIdx].cardPriceSub = e.target.value;
updateData({ products });
}}
placeholder="—"
className="w-full text-xs border border-gray-300 rounded p-1 bg-white font-bold"
/>
</div>
<div>
<label className="text-[10px] text-gray-500">Zľava s kartou</label>
<input
type="text"
value={p.cardDiscount || ''}
onChange={(e) => {
const products = [...module.data.products];
products[pIdx].cardDiscount = e.target.value;
updateData({ products });
}}
placeholder="—"
className="w-full text-xs border border-gray-300 rounded p-1 bg-white"
/>
</div>
</div>
{/* Predajne */} {/* Predajne */}
<div> <div>
<label className="text-[10px] text-gray-500 block mb-1">Dostupnosť (Piktogramy)</label> <label className="text-[10px] text-gray-500 block mb-1">Dostupnosť (Piktogramy)</label>
+32 -14
View File
@@ -8,6 +8,30 @@ export interface BadgeProductData {
priceSub: string; priceSub: string;
discountText?: string; discountText?: string;
unitPrice?: string; unitPrice?: string;
cardPriceMain?: string; // cena s kartou → {cena_s_kartou}
cardPriceSub?: string;
cardDiscount?: string; // zľava s kartou → {zlava_s_kartou}
}
// Premenné ceny s centami v hornom indexe: {cena} a {cena_s_kartou}
export const BADGE_PRICE_TOKEN_RE = /\{(cena|cena_s_kartou)\}/;
// Nahradenie jednoduchých textových premenných (bez horného indexu)
export function replaceBadgeTextVars(text: string, product: BadgeProductData): string {
return text
.replace(/{cela_suma}/g, product.priceMain || '0')
.replace(/{zlava_s_kartou}/g, (product.cardDiscount || '').replace('-', ''))
.replace(/{zlava}/g, (product.discountText || '').replace('-', ''))
.replace(/{jednotkova_cena}/g, product.unitPrice || '')
.replace(/{mena}/g, '€');
}
// Celé eurá a centy pre danú cenovú premennú
export function badgePriceParts(token: string, product: BadgeProductData): [string, string] {
if (token === 'cena_s_kartou') {
return [product.cardPriceMain || '', product.cardPriceSub || ''];
}
return [product.priceMain || '0', product.priceSub || '00'];
} }
/** /**
@@ -21,10 +45,7 @@ export function renderCleanBadgeSvg(
product: BadgeProductData, product: BadgeProductData,
overrideWidth?: number overrideWidth?: number
): string { ): string {
const pMain = product.priceMain || '0';
const pSub = product.priceSub || '00'; const pSub = product.priceSub || '00';
const discount = (product.discountText || '').replace('-', '');
const unitPrice = product.unitPrice || '';
// Získanie viewBox alebo predvolených rozmerov // Získanie viewBox alebo predvolených rozmerov
let vb = '0 0 140 95'; let vb = '0 0 140 95';
@@ -88,23 +109,20 @@ export function renderCleanBadgeSvg(
// Formátovanie textu: ak obsahuje {cena} alebo {centy}, 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}')) { const priceMatch = rawText.match(BADGE_PRICE_TOKEN_RE);
const parts = rawText.split('{cena}'); if (priceMatch) {
const before = escapeXml(parts[0] || ''); const [tokMain, tokSub] = badgePriceParts(priceMatch[1], product);
const after = escapeXml(parts[1] || ''); const idx = priceMatch.index || 0;
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>` : ''}`; const before = escapeXml(replaceBadgeTextVars(rawText.slice(0, idx), product));
const after = escapeXml(replaceBadgeTextVars(rawText.slice(idx + priceMatch[0].length), product));
contentSvg = `${before}${escapeXml(tokMain)}<tspan font-size="${subFontSize}px" dx="${subDx}" dy="${subDy}" font-weight="${fontWeight}">${escapeXml(tokSub)}</tspan>${after ? `<tspan dy="${revertDy}">${after}</tspan>` : ''}`;
} else if (rawText.includes('{centy}')) { } else if (rawText.includes('{centy}')) {
const parts = rawText.split('{centy}'); const parts = rawText.split('{centy}');
const before = escapeXml(parts[0] || ''); const before = escapeXml(parts[0] || '');
const after = escapeXml(parts[1] || ''); 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>` : ''}`; 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 contentSvg = escapeXml(replaceBadgeTextVars(rawText, product));
.replace(/{cela_suma}/g, pMain)
.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>`; 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>`;
+4 -1
View File
@@ -830,7 +830,10 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
priceMain: p.cardPriceMain || p.priceMain || '0', priceMain: p.cardPriceMain || p.priceMain || '0',
priceSub: p.cardPriceSub || p.priceSub || '00', priceSub: p.cardPriceSub || p.priceSub || '00',
discountText: p.cardDiscount || p.discountText || '', discountText: p.cardDiscount || p.discountText || '',
unitPrice: p.cardUnitPrice || p.unitPrice || '' unitPrice: p.cardUnitPrice || p.unitPrice || '',
cardPriceMain: p.cardPriceMain,
cardPriceSub: p.cardPriceSub,
cardDiscount: p.cardDiscount
}; };
return ` return `
+3
View File
@@ -24,6 +24,9 @@ export interface ProductItem {
priceSub: string; // napr. "99" alebo "89" priceSub: string; // napr. "99" alebo "89"
unitPrice: string; // napr. "1kg = 4,91 €" unitPrice: string; // napr. "1kg = 4,91 €"
discountText: string; // napr. "-23%" alebo "23%" discountText: string; // napr. "-23%" alebo "23%"
cardPriceMain?: string; // cena s kartou (vernostná), celé eurá
cardPriceSub?: string; // cena s kartou, centy
cardDiscount?: string; // zľava s kartou, napr. "30%"
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) badgeCustomTemplate?: BadgeTemplate; // Uložená dynamická šablóna cenovky (bez fixnej ceny)