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>
72 lines
3.1 KiB
TypeScript
72 lines
3.1 KiB
TypeScript
// 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 };
|
||
}
|