Files
Coop-NL/src/lib/cardImages.ts
T
Jakub_HostacnyandClaude Opus 5.5 25771afc79 feat: produktove karty v style letaku a zjednoduseny panel produktu
Karta podla sablony coop_nws_009: fotka cez celu kartu, cenovka cez fotku
vpravo dole, stitky predajni nad cenovkou, pod tym texty.
- v editore sa vizual sklada zivo (HTML/CSS); pri exporte (ZIP/Brevo) sa ulozi
  ako JPG a do e-mailu ide ako jeden obrazok - funguje rovnako vo vsetkych
  klientoch; nezmenene karty sa znova nenahravaju
- /api/img: proxy obrazkov pre nahlad (inak by sa vizual nedal ulozit)
- novy ProductEditor pre bezne aj vernostne produkty: Texty, Fotka (zoom/posun),
  Cenovka (sablona, velkost, ceny), Dostupnost, Dalsie moznosti; zbalene po produktoch
- alt texty kariet s cenou a zlavou, mobilne styly textu, ochrana farieb
  v tmavom rezime (zelena karta, Apple Mail)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:06:48 +02:00

72 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Príprava obrázkov produktových kariet pre export (ZIP / Brevo):
// vizuál karty z náhľadu v editore (fotka + cenovka + štítky) sa uloží ako JPG, nahrá do úložiska
// a jeho adresa sa zapamätá pri produkte (cardImage). Pri ďalšom exporte sa nezmenené karty nenahrávajú znova.
import { toJpeg } from 'html-to-image';
import { CardProduct, NewsletterCampaign } from '@/types/newsletter';
import { cardContextForModule, cardVisualKey, cardVisualHeight } from './productCard';
async function uploadJpeg(dataUrl: string, fileName: string): Promise<string> {
const blob = await (await fetch(dataUrl)).blob();
const formData = new FormData();
formData.append('file', new File([blob], fileName, { type: 'image/jpeg' }));
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
if (!data.success || !data.url) {
throw new Error(data.error || 'Nahratie obrázka karty zlyhalo');
}
return data.url;
}
export async function prepareCardImages(
campaign: NewsletterCampaign,
onProgress?: (done: number, total: number) => void
): Promise<{ campaign: NewsletterCampaign; failed: number }> {
// Karty, ktorých obrázok chýba alebo je zastaraný
const jobs: Array<{ mIdx: number; pIdx: number; key: string; bg: string; width: number }> = [];
campaign.modules.forEach((m, mIdx) => {
const isCards = m.type === 'products_grid' || (m.type === 'loyalty_club' && m.data?.enabled !== false);
if (!isCards) return;
const ctx = cardContextForModule(m, 'preview');
(m.data.products || []).forEach((p: CardProduct, pIdx: number) => {
const key = cardVisualKey(p, ctx);
if (p.cardImage?.key !== key) {
jobs.push({ mIdx, pIdx, key, bg: p.cardBgColor || ctx.cardBg, width: ctx.cardWidth });
}
});
});
const modules = campaign.modules.map((m) => ({ ...m, data: { ...m.data, products: m.data.products ? [...m.data.products] : m.data.products } }));
let failed = 0;
for (let i = 0; i < jobs.length; i++) {
const job = jobs[i];
onProgress?.(i, jobs.length);
const node = document.querySelector<HTMLElement>(`[data-card-visual="${job.key}"]`);
if (!node) {
failed++;
continue;
}
try {
// Presná veľkosť karty v e-maile (náhľad môže byť v užšom okne)
const height = cardVisualHeight(job.width);
const dataUrl = await toJpeg(node, {
pixelRatio: 2,
quality: 0.9,
backgroundColor: job.bg,
width: job.width,
height,
style: { width: `${job.width}px`, height: `${height}px` },
// obrázky idú cez /api/img?url=… – bez parametrov by si html-to-image pomýlil rôzne obrázky
includeQueryParams: true
});
const product = modules[job.mIdx].data.products[job.pIdx];
const url = await uploadJpeg(dataUrl, `karta-${product.id}.jpg`);
modules[job.mIdx].data.products[job.pIdx] = { ...product, cardImage: { key: job.key, url } };
} catch (err) {
console.warn('Obrázok karty sa nepodarilo pripraviť:', err);
failed++;
}
}
onProgress?.(jobs.length, jobs.length);
return { campaign: { ...campaign, modules }, failed };
}