feat: uprava produktovej fotky - zoom, posun a orezanie
- posuvniky Velkost / Vodorovne / Zvisle pri produkte aj vernostnom produkte - upravena fotka sa vykresli ako pozadie bunky (background-size/position, funguje aj v Gmaile), co presahuje ramik sa oreze - pri cenovke vedla obrazka je fotka pozadim celeho riadku a da sa zasunut pod cenovku; Outlook (mso) dostane povodny obrazok - neupravene produkty sa vykresluju presne ako doteraz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d72293a2e2
commit
9c2fb60fd6
3 files changed
+188
-13
No files matched your search
@@ -27,6 +27,72 @@ interface ConfigPanelProps {
|
||||
availableBadges?: BadgeTemplate[];
|
||||
}
|
||||
|
||||
// Pomer strán obrázka (výška / šírka); pri chybe načítania 1
|
||||
const loadImageAspect = (url: string): Promise<number> =>
|
||||
new Promise((resolve) => {
|
||||
if (!url) return resolve(1);
|
||||
const img = new window.Image();
|
||||
img.onload = () => resolve(img.naturalWidth ? img.naturalHeight / img.naturalWidth : 1);
|
||||
img.onerror = () => resolve(1);
|
||||
img.src = url;
|
||||
});
|
||||
|
||||
// Úprava produktovej fotky: zoom a posun (čo presahuje rámik, sa oreže)
|
||||
const ProductImageAdjust: React.FC<{
|
||||
product: ProductItem;
|
||||
onChange: (patch: Partial<ProductItem>) => void;
|
||||
}> = ({ product, onChange }) => {
|
||||
const scale = product.imageScale ?? 1;
|
||||
const offsetX = product.imageOffsetX || 0;
|
||||
const offsetY = product.imageOffsetY || 0;
|
||||
const isAdjusted = scale !== 1 || offsetX !== 0 || offsetY !== 0;
|
||||
|
||||
const update = async (patch: Partial<ProductItem>) => {
|
||||
// Pomer strán fotky potrebujeme pre presný výpočet pozadia; zistíme ho pri prvej úprave
|
||||
if (!product.imageAspect && product.imageUrl) {
|
||||
patch = { ...patch, imageAspect: await loadImageAspect(product.imageUrl) };
|
||||
}
|
||||
onChange(patch);
|
||||
};
|
||||
|
||||
const slider = (label: string, value: number, display: string, min: number, max: number, step: number, key: keyof ProductItem) => (
|
||||
<div className="grid grid-cols-[64px_1fr_44px] items-center gap-2">
|
||||
<span className="text-[10px] text-gray-600 font-semibold">{label}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(e) => update({ [key]: Number(e.target.value) } as Partial<ProductItem>)}
|
||||
className="w-full h-1 accent-red-600 cursor-pointer"
|
||||
/>
|
||||
<span className="text-[10px] font-mono text-gray-700 text-right">{display}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-2.5 rounded-xl border border-gray-200 bg-gray-50 space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-bold text-gray-700 uppercase tracking-wider">Úprava fotky</span>
|
||||
{isAdjusted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ imageScale: undefined, imageOffsetX: undefined, imageOffsetY: undefined })}
|
||||
className="text-[10px] font-bold text-gray-500 hover:text-red-600"
|
||||
>
|
||||
Obnoviť
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{slider('Veľkosť', scale, `${Math.round(scale * 100)} %`, 0.5, 2.5, 0.05, 'imageScale')}
|
||||
{slider('Vodorovne', offsetX, `${offsetX} px`, -150, 150, 1, 'imageOffsetX')}
|
||||
{slider('Zvisle', offsetY, `${offsetY} px`, -120, 120, 1, 'imageOffsetY')}
|
||||
<p className="text-[10px] text-gray-500">Čo presahuje rámik, sa oreže. Pri polohe cenovky „vedľa obrázka“ sa dá produkt zasunúť pod cenovku.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Jednoduchý HTML editor: vlastné HTML nahradí modul vygenerovaný z formulára
|
||||
const CustomHtmlEditor: React.FC<{
|
||||
module: NewsletterModule;
|
||||
@@ -640,6 +706,7 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
onChange={(url) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].imageUrl = url;
|
||||
products[pIdx].imageAspect = undefined; // nová fotka = nový pomer strán
|
||||
updateData({ products });
|
||||
}}
|
||||
altValue={p.imageAlt}
|
||||
@@ -652,6 +719,15 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
placeholder="https://...produkt.png alebo nahrajte súbor"
|
||||
/>
|
||||
|
||||
<ProductImageAdjust
|
||||
product={p}
|
||||
onChange={(patch) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx] = { ...products[pIdx], ...patch };
|
||||
updateData({ products });
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Platnosť akcie pre tento produkt (override)</label>
|
||||
<input
|
||||
@@ -1270,6 +1346,7 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
onChange={(url) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx].imageUrl = url;
|
||||
products[pIdx].imageAspect = undefined; // nová fotka = nový pomer strán
|
||||
updateData({ products });
|
||||
}}
|
||||
altValue={p.imageAlt}
|
||||
@@ -1282,6 +1359,15 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
placeholder="https://...produkt.png alebo nahrajte súbor"
|
||||
/>
|
||||
|
||||
<ProductImageAdjust
|
||||
product={p}
|
||||
onChange={(patch) => {
|
||||
const products = [...module.data.products];
|
||||
products[pIdx] = { ...products[pIdx], ...patch };
|
||||
updateData({ products });
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Platnosť akcie pre tento produkt (override)</label>
|
||||
<input
|
||||
|
||||
Reference in new issue
Block a user