diff --git a/src/components/editor/ConfigPanel.tsx b/src/components/editor/ConfigPanel.tsx index 3447b75..cef80e3 100644 --- a/src/components/editor/ConfigPanel.tsx +++ b/src/components/editor/ConfigPanel.tsx @@ -5,6 +5,7 @@ import { NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter import { Plus, Trash2, Image as ImageIcon, Tag, Store, Globe, Palette, Check } from 'lucide-react'; import { SUPABASE_ASSETS_BASE } from '@/lib/defaultCampaign'; import { ImageUploadInput } from './ImageUploadInput'; +import { pickAlt } from '@/lib/altText'; const PRODUCT_BG_PRESETS = [ { label: 'Červená', sub: 'Fantasticky', color: '#feeaea', border: '#f5d4d4' }, @@ -143,6 +144,9 @@ export const ConfigPanel: React.FC = ({ label="Logo COOP Jednota" value={module.data.logoUrl || ''} onChange={(url) => updateData({ logoUrl: url })} + altValue={module.data.logoAlt} + onAltChange={(alt) => updateData({ logoAlt: alt })} + altPlaceholder="COOP Jednota" />
@@ -254,6 +258,9 @@ export const ConfigPanel: React.FC = ({ label="Obrázok bannera" value={module.data.imageUrl || ''} onChange={(url) => updateData({ imageUrl: url })} + altValue={module.data.altText} + onAltChange={(alt) => updateData({ altText: alt })} + altPlaceholder={pickAlt(module.data.title, module.title, 'COOP Jednota')} />
@@ -536,6 +543,13 @@ export const ConfigPanel: React.FC = ({ products[pIdx].imageUrl = url; updateData({ products }); }} + altValue={p.imageAlt} + onAltChange={(alt) => { + const products = [...module.data.products]; + products[pIdx].imageAlt = alt; + updateData({ products }); + }} + altPlaceholder={pickAlt(p.title, module.data.sectionTitle, 'Produkt')} placeholder="https://...produkt.png alebo nahrajte súbor" /> @@ -947,6 +961,9 @@ export const ConfigPanel: React.FC = ({ label="Obrázok banneru Vernostných zliav" value={module.data.headerImageUrl || ''} onChange={(url) => updateData({ headerImageUrl: url })} + altValue={module.data.headerImageAlt} + onAltChange={(alt) => updateData({ headerImageAlt: alt })} + altPlaceholder={pickAlt(module.data.title, module.data.sectionTitle, 'Vernostné zľavy')} />
@@ -1095,6 +1112,13 @@ export const ConfigPanel: React.FC = ({ products[pIdx].imageUrl = url; updateData({ products }); }} + altValue={p.imageAlt} + onAltChange={(alt) => { + const products = [...module.data.products]; + products[pIdx].imageAlt = alt; + updateData({ products }); + }} + altPlaceholder={pickAlt(p.title, module.data.sectionTitle, 'Produkt')} placeholder="https://...produkt.png alebo nahrajte súbor" /> @@ -1585,6 +1609,11 @@ export const ConfigPanel: React.FC = ({ onChange={(url) => updateData({ item1: { ...module.data.item1, imageUrl: url } })} + altValue={module.data.item1?.altText} + onAltChange={(alt) => updateData({ + item1: { ...module.data.item1, altText: alt } + })} + altPlaceholder={pickAlt(module.data.item1?.title, module.data.title, module.title)} />
@@ -1625,6 +1654,11 @@ export const ConfigPanel: React.FC = ({ onChange={(url) => updateData({ item2: { ...module.data.item2, imageUrl: url } })} + altValue={module.data.item2?.altText} + onAltChange={(alt) => updateData({ + item2: { ...module.data.item2, altText: alt } + })} + altPlaceholder={pickAlt(module.data.item2?.title, module.data.title, module.title)} />
@@ -1666,6 +1700,11 @@ export const ConfigPanel: React.FC = ({ onChange={(url) => updateData({ item1: { ...module.data.item1, imageUrl: url } })} + altValue={module.data.item1?.altText} + onAltChange={(alt) => updateData({ + item1: { ...module.data.item1, altText: alt } + })} + altPlaceholder={pickAlt(module.data.item1?.title, module.data.title, module.title)} /> @@ -1704,6 +1743,11 @@ export const ConfigPanel: React.FC = ({ onChange={(url) => updateData({ item2: { ...module.data.item2, imageUrl: url } })} + altValue={module.data.item2?.altText} + onAltChange={(alt) => updateData({ + item2: { ...module.data.item2, altText: alt } + })} + altPlaceholder={pickAlt(module.data.item2?.title, module.data.title, module.title)} /> diff --git a/src/components/editor/ImageUploadInput.tsx b/src/components/editor/ImageUploadInput.tsx index 161c1f5..bea5cbf 100644 --- a/src/components/editor/ImageUploadInput.tsx +++ b/src/components/editor/ImageUploadInput.tsx @@ -8,13 +8,20 @@ interface ImageUploadInputProps { value: string; onChange: (url: string) => void; placeholder?: string; + // Voliteľný ručný alt text; prázdny = použije sa automatický (altPlaceholder) + altValue?: string; + onAltChange?: (alt: string) => void; + altPlaceholder?: string; } export const ImageUploadInput: React.FC = ({ label = 'Obrázok', value, onChange, - placeholder = 'https://... alebo nahrajte súbor' + placeholder = 'https://... alebo nahrajte súbor', + altValue, + onAltChange, + altPlaceholder }) => { const fileInputRef = useRef(null); const [isUploading, setIsUploading] = useState(false); @@ -107,6 +114,17 @@ export const ImageUploadInput: React.FC = ({ )} + + {onAltChange && ( + onAltChange(e.target.value)} + placeholder={altPlaceholder ? `Alt text (automaticky: ${altPlaceholder})` : 'Alt text'} + title="Alt text obrázka. Ak ho nevyplníte, doplní sa automaticky." + className="w-full text-[11px] border border-gray-200 rounded-lg px-2 py-1.5 bg-white placeholder:text-gray-400" + /> + )} ); }; diff --git a/src/lib/altText.ts b/src/lib/altText.ts new file mode 100644 index 0000000..0174837 --- /dev/null +++ b/src/lib/altText.ts @@ -0,0 +1,18 @@ +// Alt text obrázka: prvý neprázdny kandidát (ručne zadaný alt, názov produktu, nadpis okna...), +// bez HTML značiek (napr.
v nadpisoch) a nadbytočných medzier +export function pickAlt(...candidates: Array): string { + for (const c of candidates) { + const clean = stripTags(c); + if (clean) return clean; + } + return ''; +} + +function stripTags(str: string | null | undefined): string { + if (!str) return ''; + return String(str) + .replace(//gi, ' ') + .replace(/<[^>]*>/g, '') + .replace(/\s+/g, ' ') + .trim(); +} diff --git a/src/lib/emailRenderer.ts b/src/lib/emailRenderer.ts index 9642add..b085426 100644 --- a/src/lib/emailRenderer.ts +++ b/src/lib/emailRenderer.ts @@ -1,6 +1,7 @@ import { NewsletterCampaign, ProductItem } from '@/types/newsletter'; import { SUPABASE_ASSETS_BASE } from './defaultCampaign'; import { renderCleanBadgeSvg } from './badgeUtils'; +import { pickAlt } from './altText'; export function renderEmailHtml(campaign: NewsletterCampaign): string { const renderedModules = campaign.modules.map((m) => renderModule(m, campaign)).join('\n'); @@ -256,7 +257,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string { - COOP Jednota + ${escapeHtml(pickAlt(m.data.logoAlt, 'COOP Jednota'))} @@ -276,7 +277,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string { - ${escapeHtml(m.data.altText || m.data.title || m.title || 'COOP Jednota')} + ${escapeHtml(pickAlt(m.data.altText, m.data.title, m.title, 'COOP Jednota'))} @@ -377,7 +378,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string { - ${escapeHtml(m.data.title || m.data.sectionTitle || 'Vernostné zľavy')} + ${escapeHtml(pickAlt(m.data.headerImageAlt, m.data.title, m.data.sectionTitle, 'Vernostné zľavy'))} @@ -416,7 +417,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string { - ${escapeHtml(it1.title || m.data.title || m.title || 'Aktuálny leták')} + ${escapeHtml(pickAlt(it1.altText, it1.title, m.data.title, m.title, 'Aktuálny leták'))} @@ -450,7 +451,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string { - ${escapeHtml(it2.title || m.data.title || m.title || 'Aktuálny leták')} + ${escapeHtml(pickAlt(it2.altText, it2.title, m.data.title, m.title, 'Aktuálny leták'))} @@ -502,7 +503,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
- ${escapeHtml(it1.title || 'Recepty')} + ${escapeHtml(pickAlt(it1.altText, it1.title, 'Recepty'))}
@@ -531,7 +532,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
- ${escapeHtml(it2.title || 'Časopis Jednota')} + ${escapeHtml(pickAlt(it2.altText, it2.title, 'Časopis Jednota'))}
@@ -656,7 +657,7 @@ function renderExactProductCard(
- ${escapeHtml(p.title || 'Cenovka')} + ${escapeHtml(pickAlt(p.title, 'Cenovka'))}
`; @@ -712,7 +713,7 @@ function renderExactProductCard( - ${escapeHtml(p.title || sectionTitle || 'Produkt')} + ${escapeHtml(pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt'))} ${renderBadgeContent(bWidth, bAlign, padTop, padBottom, marginStyle)} @@ -742,7 +743,7 @@ function renderExactProductCard( - ${escapeHtml(p.title || sectionTitle || 'Produkt')} + ${escapeHtml(pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt'))} @@ -799,7 +800,7 @@ function renderExactLoyaltyCard(p: any, validity: string): string { - ${escapeHtml(p.title || 'Produkt')} + ${escapeHtml(pickAlt(p.imageAlt, p.title, 'Produkt'))} diff --git a/src/types/newsletter.ts b/src/types/newsletter.ts index c9b6e14..239688d 100644 --- a/src/types/newsletter.ts +++ b/src/types/newsletter.ts @@ -40,6 +40,7 @@ export interface ProductItem { cardBgColor?: string; // HEX napr. "#feeaea" (fantasticky), "#fef0c7" (domace), "#dcb085" (grill) showSlovakFlag?: boolean; customBadgePngUrl?: string; // Voliteľná statická PNG cenovka + imageAlt?: string; // Ručný alt text fotky (inak názov produktu) badgeLayout?: 'below' | 'side'; // Poloha cenovky: 'below' = pod obrázkom (default), 'side' = vedľa obrázka } @@ -55,6 +56,7 @@ export interface AnnouncementModuleData { export interface HeaderModuleData { logoUrl: string; logoLink: string; + logoAlt?: string; navLinks: Array<{ label: string; url: string; variant?: 'outline' | 'filled' }>; } @@ -99,6 +101,7 @@ export interface ProductsGridModuleData { export interface LoyaltyClubModuleData { headerImageUrl: string; + headerImageAlt?: string; title: string; description: string; buttonText: string;