diff --git a/src/app/page.tsx b/src/app/page.tsx index ec35df3..caeb939 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,7 +5,7 @@ import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE, ensureValidityBar } from '@/lib/defaultCampaign'; import { renderEmailHtml, renderSingleModuleHtml, getCampaignValidity } from '@/lib/emailRenderer'; import { supabase } from '@/lib/supabase'; -import { DEFAULT_BADGE_TEMPLATES } from '@/lib/badgeUtils'; +import { DEFAULT_BADGE_TEMPLATES, BadgeRow, badgeFromRow } from '@/lib/badgeUtils'; import { getBrevoCampaignUrl } from '@/lib/brevo'; import { TopBar } from '@/components/editor/TopBar'; import { ConfigPanel } from '@/components/editor/ConfigPanel'; @@ -56,19 +56,7 @@ export default function Home() { .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 - }; - }); + loaded = (data as BadgeRow[]).map(badgeFromRow); } } catch (e) { console.warn('Error fetching badges in page:', e); @@ -1006,6 +994,7 @@ export default function Home() { isOpen={isImportOpen} onClose={() => setIsImportOpen(false)} onImportProducts={handleImportProducts} + availableBadges={availableBadges} /> {/* Modál Editora cenoviek (SVG -> PNG) */} diff --git a/src/components/editor/BadgeEditorModal.tsx b/src/components/editor/BadgeEditorModal.tsx index 6e17418..35679b5 100644 --- a/src/components/editor/BadgeEditorModal.tsx +++ b/src/components/editor/BadgeEditorModal.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { BadgeTemplate, BadgeLayer } from '@/types/newsletter'; import { supabase } from '@/lib/supabase'; -import { renderCleanBadgeSvg, BadgeProductData } from '@/lib/badgeUtils'; +import { renderCleanBadgeSvg, BadgeProductData, BadgeRow, badgeFromRow, normalizeBadgeCode } from '@/lib/badgeUtils'; import { Tag, Plus, @@ -45,6 +45,7 @@ function toBadgeDbPayload(badge: BadgeTemplate) { svg_raw: badge.svgRaw, layers_config: { layers: badge.layersConfig, + code: badge.code ? normalizeBadgeCode(badge.code) : null, width: badge.width || 110, align: badge.align || 'center', paddingTop: badge.paddingTop ?? 4, @@ -53,27 +54,6 @@ function toBadgeDbPayload(badge: BadgeTemplate) { }; } -interface BadgeRow { - id: string; - name: string; - svg_raw: string | null; - layers_config: BadgeLayer[] | { layers?: BadgeLayer[]; width?: number; align?: BadgeTemplate['align']; paddingTop?: number; paddingBottom?: number } | null; -} - -function fromBadgeRow(d: BadgeRow): BadgeTemplate { - const cfg = d.layers_config && !Array.isArray(d.layers_config) ? d.layers_config : null; - return { - id: d.id, - name: d.name, - svgRaw: d.svg_raw || '', - layersConfig: cfg ? cfg.layers || [] : (d.layers_config as BadgeLayer[]) || [], - width: cfg?.width || 110, - align: cfg?.align || 'center', - paddingTop: cfg?.paddingTop ?? 4, - paddingBottom: cfg?.paddingBottom ?? 4 - }; -} - function saveLocal(list: BadgeTemplate[]) { try { localStorage.setItem(LOCAL_KEY, JSON.stringify(list)); @@ -294,6 +274,10 @@ export const BadgeEditorModal: React.FC = ({ const fileRef = useRef(null); const isDirty = !!badge && JSON.stringify(badge) !== savedJson; + // Kód pre import musí byť jedinečný + const codeTaken = + !!badge?.code && + badges.some((b) => b.id !== badge.id && b.code && normalizeBadgeCode(b.code) === normalizeBadgeCode(badge.code)); const layer = badge?.layersConfig.find((l) => l.id === selectedId) || null; const vb = useMemo(() => viewBoxOf(badge?.svgRaw), [badge?.svgRaw]); const canvasWidth = Math.round(300 * zoom); @@ -324,7 +308,7 @@ export const BadgeEditorModal: React.FC = ({ let loaded: BadgeTemplate[] = []; try { const { data } = await supabase.from('badges').select('*').order('created_at', { ascending: false }); - loaded = ((data as BadgeRow[]) || []).map(fromBadgeRow); + loaded = ((data as BadgeRow[]) || []).map(badgeFromRow); } catch { // bez databázy použijeme kópiu v prehliadači } @@ -663,6 +647,21 @@ export const BadgeEditorModal: React.FC = ({ title="Názov šablóny" /> )} + {badge && ( + + )} {isDirty && Neuložené} {savedFlash && Uložené} diff --git a/src/components/editor/ProductImportModal.tsx b/src/components/editor/ProductImportModal.tsx index ecf99c6..51896d3 100644 --- a/src/components/editor/ProductImportModal.tsx +++ b/src/components/editor/ProductImportModal.tsx @@ -3,7 +3,8 @@ import React, { useState } from 'react'; import Papa from 'papaparse'; import { XMLParser } from 'fast-xml-parser'; -import { ProductItem } from '@/types/newsletter'; +import { BadgeTemplate, ProductItem } from '@/types/newsletter'; +import { findBadgeByCode, normalizeBadgeCode } from '@/lib/badgeUtils'; import { Upload, Check, AlertCircle, FileText, X } from 'lucide-react'; import { supabase } from '@/lib/supabase'; @@ -11,12 +12,14 @@ interface ImportModalProps { isOpen: boolean; onClose: () => void; onImportProducts: (products: ProductItem[]) => void; + availableBadges?: BadgeTemplate[]; // šablóny cenoviek – stĺpec s kódom určí, ktorú použiť } export const ProductImportModal: React.FC = ({ isOpen, onClose, - onImportProducts + onImportProducts, + availableBadges = [] }) => { const [file, setFile] = useState(null); const [fileType, setFileType] = useState<'csv' | 'json' | 'xml'>('csv'); @@ -32,6 +35,7 @@ export const ProductImportModal: React.FC = ({ discountText: string; imageUrl: string; validity: string; + badgeCode: string; }>({ title: '', priceMain: '', @@ -39,7 +43,8 @@ export const ProductImportModal: React.FC = ({ unitPrice: '', discountText: '', imageUrl: '', - validity: '' + validity: '', + badgeCode: '' }); const [presetName, setPresetName] = useState(''); @@ -125,6 +130,7 @@ export const ProductImportModal: React.FC = ({ if (lower.includes('zlava') || lower.includes('discount')) newMapping.discountText = h; if (lower.includes('obrazok') || lower.includes('image') || lower.includes('foto')) newMapping.imageUrl = h; if (lower.includes('platnost') || lower.includes('valid')) newMapping.validity = h; + if (lower.includes('cenovk') || lower.includes('badge') || lower.includes('sablon') || lower.includes('template')) newMapping.badgeCode = h; }); setMapping(newMapping); }; @@ -148,7 +154,11 @@ export const ProductImportModal: React.FC = ({ if (pSub.length === 1) pSub += '0'; } + // Šablóna cenovky podľa kódu (alebo názvu) zo súboru + const badge = mapping.badgeCode ? findBadgeByCode(availableBadges, row[mapping.badgeCode]) : undefined; + return { + ...(badge ? { badgeCustomTemplate: badge, badgeId: badge.id, badgeWidth: badge.width ?? 110 } : {}), id: `imported-${Date.now()}-${idx}`, title: String(row[mapping.title] || 'Nový produkt'), imageUrl: row[mapping.imageUrl] || 'https://dqexxfspkylqttrtxsww.supabase.co/storage/v1/object/public/newsletter-assets/produkt_1.png', @@ -163,6 +173,20 @@ export const ProductImportModal: React.FC = ({ }; }); + // Kódy cenoviek, ku ktorým sa nenašla šablóna + if (mapping.badgeCode) { + const unknown = [ + ...new Set( + parsedRows + .map((row) => String(row[mapping.badgeCode] || '').trim()) + .filter((v) => v && !findBadgeByCode(availableBadges, v)) + ) + ]; + if (unknown.length) { + alert(`Pre tieto kódy cenoviek sa nenašla šablóna (produkty dostanú predvolenú cenovku):\n${unknown.join(', ')}`); + } + } + onImportProducts(importedProducts); onClose(); }; @@ -268,6 +292,25 @@ export const ProductImportModal: React.FC = ({ +
+ + +

+ Kódy šablón:{' '} + {availableBadges.length + ? availableBadges.map((b) => normalizeBadgeCode(b.code || b.name)).join(', ') + : 'zatiaľ žiadne šablóny'} + {' '}(kód nastavíte v Editore cenoviek; funguje aj názov šablóny) +

+
+