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:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-19 17:06:48 +02:00
1 parent 139e3569d0
commit 25771afc79
9 files changed
+1009 -1756

No files matched your search

+31
View File
@@ -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 });
}
}