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,31 @@
|
||||
// Proxy obrázkov pre náhľad produktových kariet: obrázky z iných domén idú cez rovnaký pôvod,
|
||||
// aby sa vizuál karty dal pri exporte uložiť ako obrázok (canvas nesmie byť "tainted" cez CORS).
|
||||
const MAX_BYTES = 15 * 1024 * 1024;
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const target = new URL(request.url).searchParams.get('url');
|
||||
if (!target || !/^https?:\/\//i.test(target)) {
|
||||
return Response.json({ error: 'Neplatná adresa obrázka.' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(target, { redirect: 'follow' });
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
if (!res.ok || !contentType.startsWith('image/')) {
|
||||
return Response.json({ error: `Obrázok sa nepodarilo načítať (${res.status}).` }, { status: 502 });
|
||||
}
|
||||
const body = await res.arrayBuffer();
|
||||
if (body.byteLength > MAX_BYTES) {
|
||||
return Response.json({ error: 'Obrázok je príliš veľký.' }, { status: 413 });
|
||||
}
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
'content-type': contentType,
|
||||
'cache-control': 'public, max-age=86400'
|
||||
}
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Chyba pri načítaní obrázka';
|
||||
return Response.json({ error: message }, { status: 502 });
|
||||
}
|
||||
}
|
||||
+15
-2
@@ -13,6 +13,7 @@ import { ProductImportModal } from '@/components/editor/ProductImportModal';
|
||||
import { BadgeEditorModal } from '@/components/editor/BadgeEditorModal';
|
||||
import { SavedCampaignsModal } from '@/components/editor/SavedCampaignsModal';
|
||||
import JSZip from 'jszip';
|
||||
import { prepareCardImages } from '@/lib/cardImages';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { ChevronUp, ChevronDown, Copy, Trash2, Anchor, Lock } from 'lucide-react';
|
||||
|
||||
@@ -432,8 +433,19 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
// Pred exportom sa vizuály produktových kariet uložia ako obrázky (nezmenené sa nenahrávajú znova)
|
||||
const prepareForExport = async (): Promise<NewsletterCampaign> => {
|
||||
const { campaign: prepared, failed } = await prepareCardImages(campaign);
|
||||
setCampaign((prev) => ({ ...prev, modules: prepared.modules }));
|
||||
if (failed > 0) {
|
||||
alert(`${failed} kariet produktov sa nepodarilo uložiť ako obrázok – v e-maile budú v jednoduchšej podobe.`);
|
||||
}
|
||||
return prepared;
|
||||
};
|
||||
|
||||
const handleExportZip = async () => {
|
||||
const rawHtml = renderEmailHtml(campaign);
|
||||
const prepared = await prepareForExport();
|
||||
const rawHtml = renderEmailHtml(prepared);
|
||||
let finalHtml = rawHtml;
|
||||
try {
|
||||
const res = await fetch('/api/inline-css', {
|
||||
@@ -456,7 +468,8 @@ export default function Home() {
|
||||
const handleExportBrevo = async (updateExisting = false) => {
|
||||
setIsExportingBrevo(true);
|
||||
try {
|
||||
const rawHtml = renderEmailHtml(campaign);
|
||||
const prepared = await prepareForExport();
|
||||
const rawHtml = renderEmailHtml(prepared);
|
||||
let finalHtml = rawHtml;
|
||||
try {
|
||||
const res = await fetch('/api/inline-css', {
|
||||
|
||||
Reference in new issue
Block a user