From 293d9b953c0bbd1d0eb28afe320775649613bb95 Mon Sep 17 00:00:00 2001 From: Jandl-hostacny Date: Wed, 21 Oct 2026 12:47:42 +0200 Subject: [PATCH] feat: vlastne stitky na fotke produktu, Adobe Fonts pre celu aplikaciu - pri produkte blok Stitky na fotke: text (
= novy riadok), poloha X/Y, velkost, natocenie, farba pozadia a textu; lubovolny pocet stitkov - stitky su nad fotkou a pod cenovkou, pismo Aptly Bold; ulozia sa aj do obrazka karty pri exporte - NEXT_PUBLIC_ADOBE_FONTS_URL: odkaz na web projekt Adobe Fonts sa nacita v celej aplikacii (Aptly pre cenovky a stitky) Co-Authored-By: Claude Opus 5.5 --- .env.example | 5 ++ src/app/layout.tsx | 2 + src/components/editor/ProductEditor.tsx | 101 +++++++++++++++++++++++- src/lib/productCard.ts | 13 ++- src/types/newsletter.ts | 13 +++ 5 files changed, 131 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 532f804..bc3789c 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,11 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY= # service_role kľúč – len na serveri (nahrávanie obrázkov), nikdy ho nedávajte do kódu pre prehliadač SUPABASE_SERVICE_ROLE_KEY= +# Adobe Fonts (voliteľné): odkaz na CSS web projektu z fonts.adobe.com (Web Projects → Embed), +# napr. https://use.typekit.net/abc1234.css – načíta písma ako Aptly pre cenovky a štítky. +# V Adobe projekte treba povoliť domény (localhost aj produkčnú). +NEXT_PUBLIC_ADOBE_FONTS_URL= + # Brevo (SMTP & API → API Keys) BREVO_API_KEY= BREVO_SENDER_NAME=COOP Jednota diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3068336..7e6e156 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -26,6 +26,8 @@ export default function RootLayout({ children }: LayoutProps<"/">) { {/* Písmo e-mailu (Poppins) – náhľad v editore aj na stránke s pripomienkami */} + {/* Adobe Fonts (napr. Aptly pre cenovky a štítky) – odkaz na web projekt z fonts.adobe.com */} + {process.env.NEXT_PUBLIC_ADOBE_FONTS_URL && } {children} diff --git a/src/components/editor/ProductEditor.tsx b/src/components/editor/ProductEditor.tsx index 31cfdc8..ff4c922 100644 --- a/src/components/editor/ProductEditor.tsx +++ b/src/components/editor/ProductEditor.tsx @@ -3,7 +3,7 @@ // Editor jedného produktu (bežné aj vernostné produkty) – zbalený po produktoch, len to, čo treba: // texty, fotka (zoom/posun), cenovka (šablóna, veľkosť, ceny), dostupnosť a ďalšie možnosti. import React, { useEffect, useRef } from 'react'; -import { BadgeTemplate, CardProduct } from '@/types/newsletter'; +import { BadgeTemplate, CardProduct, ProductLabel } from '@/types/newsletter'; import { ChevronDown, Trash2, Pencil, Globe, Layers } from 'lucide-react'; import { ImageUploadInput } from './ImageUploadInput'; import { pickAlt } from '@/lib/altText'; @@ -105,6 +105,72 @@ const ResetLink: React.FC<{ onClick: () => void }> = ({ onClick }) => ( ); +// Nastavenie jedného vlastného štítka na fotke +const LABEL_COLORS: Array<[string, string]> = [ + ['#e2001a', 'Červená'], + ['#ffde00', 'Žltá'], + ['#003399', 'Modrá'], + ['#000000', 'Čierna'], + ['#ffffff', 'Biela'] +]; + +const LabelEditor: React.FC<{ + label: ProductLabel; + onChange: (patch: Partial) => void; + onRemove: () => void; +}> = ({ label, onChange, onRemove }) => { + const swatches = (key: 'bg' | 'color') => ( +
+ {LABEL_COLORS.map(([c, name]) => ( +
+ ); + return ( +
+
+ onChange({ text: e.target.value })} + placeholder="Text štítka (
= nový riadok)" + className="flex-1 min-w-0 text-xs font-bold uppercase border border-gray-300 rounded-lg px-2 py-1" + /> + +
+ onChange({ x: v })} /> + onChange({ y: v })} /> + onChange({ size: v })} /> + onChange({ rotate: v })} /> +
+
+ Pozadie + {swatches('bg')} +
+
+ Text + {swatches('color')} +
+
+
+ ); +}; + // Šablóna používa ceny s kartou → zobrazíme aj polia s kartou const usesCardPrice = (t?: BadgeTemplate) => !!t && JSON.stringify(t.layersConfig || []).includes('_s_kartou'); @@ -372,6 +438,39 @@ export const ProductEditor: React.FC = ({ )} + {/* VLASTNÉ ŠTÍTKY NA FOTKE */} + + onChange({ + labels: [ + ...(p.labels || []), + { id: `lbl-${Date.now()}`, text: 'NOVINKA', x: 30, y: 20, size: 12, bg: '#e2001a', color: '#ffffff', rotate: 0 } + ] + }) + } + className="text-[10px] font-bold text-red-700 hover:text-red-900" + > + + Pridať štítok + + } + > + {(p.labels || []).length === 0 && ( +

Napr. „PODIEL MÄSA 92 %“ alebo „NOVINKA“ priamo na fotke produktu.

+ )} + {(p.labels || []).map((l, i) => ( + onChange({ labels: (p.labels || []).map((x, j) => (j === i ? { ...x, ...patch } : x)) })} + onRemove={() => onChange({ labels: (p.labels || []).filter((_, j) => j !== i) })} + /> + ))} +
+ {/* ĎALŠIE MOŽNOSTI */}
diff --git a/src/lib/productCard.ts b/src/lib/productCard.ts index 834c027..a22e197 100644 --- a/src/lib/productCard.ts +++ b/src/lib/productCard.ts @@ -5,7 +5,7 @@ // V editore (mode 'preview') sa vizuál skladá živo z HTML/CSS. Pri exporte (ZIP/Brevo) sa vizuál z náhľadu // uloží ako JPG (src/lib/cardImages.ts) a do e-mailu ide ako jeden obrázok – rovnako ako karta_produkt_*.jpg // v šablóne. Vďaka tomu karta vyzerá rovnako vo všetkých klientoch (Gmail, Outlook, mobil). -import { BadgeTemplate, CardProduct, NewsletterModule } from '@/types/newsletter'; +import { BadgeTemplate, CardProduct, NewsletterModule, ProductLabel } from '@/types/newsletter'; import { renderCleanBadgeSvg, BadgeProductData } from './badgeUtils'; import { SUPABASE_ASSETS_BASE } from './defaultCampaign'; import { pickAlt } from './altText'; @@ -207,7 +207,8 @@ export function cardVisualKey(p: CardProduct, ctx: CardContext): string { p.priceMain, p.priceSub, p.discountText, p.badgeText, p.unitPrice, p.cardPriceMain, p.cardPriceSub, p.cardDiscount, p.cardUnitPrice, p.noCardPriceMain, p.noCardPriceSub, p.noCardDiscount, p.noCardUnitPrice, - p.stores, p.storesOffsetX ?? DEFAULT_STORES_OFFSET_X, p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y, p.storesSize ?? DEFAULT_STORES_SIZE + p.stores, p.storesOffsetX ?? DEFAULT_STORES_OFFSET_X, p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y, p.storesSize ?? DEFAULT_STORES_SIZE, + p.labels || [] ]; const str = JSON.stringify(relevant); let h1 = 5381; @@ -220,6 +221,13 @@ export function cardVisualKey(p: CardProduct, ctx: CardContext): string { return `cv-${(h1 >>> 0).toString(36)}${(h2 >>> 0).toString(36)}`; } +// Vlastný štítok na fotke (nad fotkou, pod cenovkou) – štýl ako „PODIEL MÄSA 92 %“ z letáku, písmo Aptly Bold (Adobe Fonts) +function renderLabel(l: ProductLabel): string { + if (!l.text) return ''; + const size = l.size || 12; + return `
${escapeWithBreaks(l.text)}
`; +} + // Vizuál karty v náhľade: fotka (zoom/posun), cenovka vpravo dole, štítky nad cenovkou // Viditeľné pozadie cenovky v SVG môže mať okolo seba priehľadný okraj. Štítky predajní ukotvujeme // k pravému hornému rohu samotného pozadia, preto jeho hranice odmeriame v prehliadači (getBBox). @@ -277,6 +285,7 @@ function renderVisualPreview(p: CardProduct, ctx: CardContext, key: string): str return `
${p.imageUrl ? `` : ''} + ${(p.labels || []).map(renderLabel).join('')}
${stores ? `
${stores}
` : ''} ${renderBadge(p, ctx, bw)} diff --git a/src/types/newsletter.ts b/src/types/newsletter.ts index b9053ce..b26be85 100644 --- a/src/types/newsletter.ts +++ b/src/types/newsletter.ts @@ -50,6 +50,7 @@ export interface ProductItem { storesOffsetX?: number; // posun štítkov predajní voči cenovke (+ doprava) storesOffsetY?: number; // posun štítkov predajní voči hornej hrane cenovky (+ vyššie) storesSize?: number; // výška jedného štítka predajne v px (nezávislá od veľkosti cenovky) + labels?: ProductLabel[]; // vlastné štítky na fotke (napr. „PODIEL MÄSA 92 %“) imageScale?: number; // Zoom fotky (1 = 100 %) imageOffsetX?: number; // Posun fotky vodorovne v px (+ doprava) imageOffsetY?: number; // Posun fotky zvisle v px (+ nadol) @@ -67,6 +68,18 @@ export interface CardProduct extends ProductItem { noCardUnitPrice?: string; } +// Vlastný štítok na fotke produktu +export interface ProductLabel { + id: string; + text: string; //
= nový riadok + x: number; // stred štítka, % šírky fotky (0–100) + y: number; // stred štítka, % výšky fotky (0–100) + size: number; // veľkosť písma v px + bg: string; // farba pozadia + color: string; // farba textu + rotate?: number; // natočenie v stupňoch +} + export interface AnnouncementModuleData { tipBadge: string; text: string;