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>
This commit is contained in:
1 parent
139e3569d0
commit
25771afc79
9 files changed
+1009
-1756
No files matched your search
@@ -0,0 +1,71 @@
|
||||
// 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 };
|
||||
}
|
||||
Reference in new issue
Block a user