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:
1 parent
22a9a51d00
commit
3bd93fde20
6 files changed
+138
-43
No files matched your search
@@ -835,6 +835,9 @@ export default function Home() {
|
||||
currentPriceMain={targetProduct?.priceMain || targetProduct?.cardPriceMain || '1'}
|
||||
currentPriceSub={targetProduct?.priceSub || targetProduct?.cardPriceSub || '99'}
|
||||
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) */}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
|
||||
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';
|
||||
|
||||
// Riadok tabuľky badges v Supabase pre danú šablónu
|
||||
@@ -34,6 +34,9 @@ interface BadgeEditorModalProps {
|
||||
currentPriceMain?: string;
|
||||
currentPriceSub?: string;
|
||||
currentUnitPrice?: string;
|
||||
currentCardPriceMain?: string;
|
||||
currentCardPriceSub?: string;
|
||||
currentCardDiscount?: string;
|
||||
}
|
||||
|
||||
export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
@@ -43,7 +46,10 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
currentDiscount = '25%',
|
||||
currentPriceMain = '1',
|
||||
currentPriceSub = '99',
|
||||
currentUnitPrice = '1kg = 4,91 €'
|
||||
currentUnitPrice = '1kg = 4,91 €',
|
||||
currentCardPriceMain = '1',
|
||||
currentCardPriceSub = '49',
|
||||
currentCardDiscount = '30%'
|
||||
}) => {
|
||||
const POPULAR_FONTS = [
|
||||
{ name: 'Poppins', source: 'google' as const },
|
||||
@@ -391,7 +397,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
const addTextLayer = (presetText: string = 'Text') => {
|
||||
if (!activeBadge) return;
|
||||
const newL: BadgeLayer = {
|
||||
id: `l-${Date.now()}`,
|
||||
id: `l-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
type: 'text',
|
||||
text: presetText,
|
||||
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 €
|
||||
// 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 raw = layer.text || '';
|
||||
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 subX = layer.indexOffsetX !== undefined ? `${layer.indexOffsetX}px` : '2px';
|
||||
|
||||
if (raw.includes('{cena}')) {
|
||||
const parts = raw.split('{cena}');
|
||||
const priceMatch = raw.match(BADGE_PRICE_TOKEN_RE);
|
||||
if (priceMatch) {
|
||||
const [tokMain, tokSub] = badgePriceParts(priceMatch[1], previewProduct);
|
||||
const idx = priceMatch.index || 0;
|
||||
return (
|
||||
<span>
|
||||
{parts[0]}
|
||||
<span>{currentPriceMain}</span>
|
||||
{replaceBadgeTextVars(raw.slice(0, idx), previewProduct)}
|
||||
<span>{tokMain}</span>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
@@ -560,9 +579,9 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
lineHeight: 1
|
||||
}}
|
||||
>
|
||||
{currentPriceSub}
|
||||
{tokSub}
|
||||
</span>
|
||||
{parts[1]}
|
||||
{replaceBadgeTextVars(raw.slice(idx + priceMatch[0].length), previewProduct)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -588,13 +607,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const replaced = raw
|
||||
.replace(/{cela_suma}/g, currentPriceMain)
|
||||
.replace(/{zlava}/g, currentDiscount.replace('-', ''))
|
||||
.replace(/{jednotkova_cena}/g, currentUnitPrice)
|
||||
.replace(/{mena}/g, '€');
|
||||
|
||||
return replaced;
|
||||
return replaceBadgeTextVars(raw, previewProduct);
|
||||
};
|
||||
|
||||
// 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 {
|
||||
const cleanSvg = renderCleanBadgeSvg(
|
||||
activeBadge,
|
||||
{
|
||||
priceMain: currentPriceMain,
|
||||
priceSub: currentPriceSub,
|
||||
discountText: currentDiscount,
|
||||
unitPrice: currentUnitPrice
|
||||
},
|
||||
previewProduct,
|
||||
activeBadge.width || 110
|
||||
);
|
||||
|
||||
@@ -709,12 +717,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderCleanBadgeSvg(
|
||||
b,
|
||||
{
|
||||
priceMain: currentPriceMain,
|
||||
priceSub: currentPriceSub,
|
||||
discountText: currentDiscount,
|
||||
unitPrice: currentUnitPrice
|
||||
},
|
||||
previewProduct,
|
||||
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">
|
||||
{'{zlava}'} → {currentDiscount.replace('-', '')}
|
||||
</span>
|
||||
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-amber-300">
|
||||
{'{cena_s_kartou}'} → {currentCardPriceMain}
|
||||
<sup>{currentCardPriceSub}</sup>
|
||||
</span>
|
||||
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-emerald-300">
|
||||
{'{zlava_s_kartou}'} → {currentCardDiscount.replace('-', '')}
|
||||
</span>
|
||||
<span className="bg-slate-800 px-2 py-0.5 rounded font-mono text-blue-300">
|
||||
{'{mena}'} → €
|
||||
</span>
|
||||
@@ -1032,6 +1042,18 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
>
|
||||
+ Zľava %
|
||||
</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
|
||||
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"
|
||||
|
||||
@@ -656,6 +656,52 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
</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 */}
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500 block mb-1">Dostupnosť (Piktogramy)</label>
|
||||
|
||||
+32
-14
@@ -8,6 +8,30 @@ export interface BadgeProductData {
|
||||
priceSub: string;
|
||||
discountText?: 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,
|
||||
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';
|
||||
@@ -88,23 +109,20 @@ export function renderCleanBadgeSvg(
|
||||
|
||||
// Formátovanie textu: ak obsahuje {cena} alebo {centy}, 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" dx="${subDx}" dy="${subDy}" font-weight="${fontWeight}">${escapeXml(pSub)}</tspan>${after ? `<tspan dy="${revertDy}">${after}</tspan>` : ''}`;
|
||||
const priceMatch = rawText.match(BADGE_PRICE_TOKEN_RE);
|
||||
if (priceMatch) {
|
||||
const [tokMain, tokSub] = badgePriceParts(priceMatch[1], product);
|
||||
const idx = priceMatch.index || 0;
|
||||
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}')) {
|
||||
const parts = rawText.split('{centy}');
|
||||
const before = escapeXml(parts[0] || '');
|
||||
const after = escapeXml(parts[1] || '');
|
||||
contentSvg = `${before}<tspan font-size="${subFontSize}px" dx="${subDx}" dy="${subDy}" font-weight="${fontWeight}">${escapeXml(pSub)}</tspan>${after ? `<tspan dy="${revertDy}">${after}</tspan>` : ''}`;
|
||||
} else {
|
||||
let replaced = rawText
|
||||
.replace(/{cela_suma}/g, pMain)
|
||||
.replace(/{zlava}/g, discount)
|
||||
.replace(/{jednotkova_cena}/g, unitPrice)
|
||||
.replace(/{mena}/g, '€');
|
||||
contentSvg = escapeXml(replaced);
|
||||
contentSvg = escapeXml(replaceBadgeTextVars(rawText, product));
|
||||
}
|
||||
|
||||
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>`;
|
||||
|
||||
@@ -830,7 +830,10 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
|
||||
priceMain: p.cardPriceMain || p.priceMain || '0',
|
||||
priceSub: p.cardPriceSub || p.priceSub || '00',
|
||||
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 `
|
||||
|
||||
@@ -24,6 +24,9 @@ export interface ProductItem {
|
||||
priceSub: string; // napr. "99" alebo "89"
|
||||
unitPrice: string; // napr. "1kg = 4,91 €"
|
||||
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"
|
||||
badgeId?: string; // priradená cenovka z databázy
|
||||
badgeCustomTemplate?: BadgeTemplate; // Uložená dynamická šablóna cenovky (bez fixnej ceny)
|
||||
|
||||
Reference in new issue
Block a user