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[];
|
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
|
// Jednoduchý HTML editor: vlastné HTML nahradí modul vygenerovaný z formulára
|
||||||
const CustomHtmlEditor: React.FC<{
|
const CustomHtmlEditor: React.FC<{
|
||||||
module: NewsletterModule;
|
module: NewsletterModule;
|
||||||
@@ -640,6 +706,7 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
onChange={(url) => {
|
onChange={(url) => {
|
||||||
const products = [...module.data.products];
|
const products = [...module.data.products];
|
||||||
products[pIdx].imageUrl = url;
|
products[pIdx].imageUrl = url;
|
||||||
|
products[pIdx].imageAspect = undefined; // nová fotka = nový pomer strán
|
||||||
updateData({ products });
|
updateData({ products });
|
||||||
}}
|
}}
|
||||||
altValue={p.imageAlt}
|
altValue={p.imageAlt}
|
||||||
@@ -652,6 +719,15 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
placeholder="https://...produkt.png alebo nahrajte súbor"
|
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>
|
<div>
|
||||||
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Platnosť akcie pre tento produkt (override)</label>
|
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Platnosť akcie pre tento produkt (override)</label>
|
||||||
<input
|
<input
|
||||||
@@ -1270,6 +1346,7 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
onChange={(url) => {
|
onChange={(url) => {
|
||||||
const products = [...module.data.products];
|
const products = [...module.data.products];
|
||||||
products[pIdx].imageUrl = url;
|
products[pIdx].imageUrl = url;
|
||||||
|
products[pIdx].imageAspect = undefined; // nová fotka = nový pomer strán
|
||||||
updateData({ products });
|
updateData({ products });
|
||||||
}}
|
}}
|
||||||
altValue={p.imageAlt}
|
altValue={p.imageAlt}
|
||||||
@@ -1282,6 +1359,15 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
placeholder="https://...produkt.png alebo nahrajte súbor"
|
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>
|
<div>
|
||||||
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Platnosť akcie pre tento produkt (override)</label>
|
<label className="text-[10px] text-gray-500 font-semibold block mb-0.5">Platnosť akcie pre tento produkt (override)</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
+98
-13
@@ -337,7 +337,7 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
|||||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 12px;">
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 12px;">
|
||||||
<tr>
|
<tr>
|
||||||
<td width="100%" valign="top">
|
<td width="100%" valign="top">
|
||||||
${renderExactProductCard(p, globalValidity, cardBg, cardBorder, badgeHdrBg, badgeHdrColor, badgePrcBg, badgePrcColor, badgeCustomTxt, m.data.sectionTitle)}
|
${renderExactProductCard(p, globalValidity, cardBg, cardBorder, badgeHdrBg, badgeHdrColor, badgePrcBg, badgePrcColor, badgeCustomTxt, m.data.sectionTitle, 600)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>`;
|
</table>`;
|
||||||
@@ -630,6 +630,56 @@ function renderModule(m: any, campaign: NewsletterCampaign): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Úprava produktovej fotky (zoom / posun / orezanie)
|
||||||
|
// Upravená fotka sa vykreslí ako pozadie bunky (background-size / background-position
|
||||||
|
// fungujú aj v Gmaile, ktorý transform/position odstraňuje); čo presahuje rámik, sa oreže.
|
||||||
|
// Outlook pre Windows (mso) pozadia nevie, dostane pôvodný <img>.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function hasImageAdjust(p: any): boolean {
|
||||||
|
return (p.imageScale ?? 1) !== 1 || (p.imageOffsetX || 0) !== 0 || (p.imageOffsetY || 0) !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obsah vkladaný do podmieneného komentára pre Outlook nesmie obsahovať vlastné komentáre (napr. zo SVG cenovky)
|
||||||
|
function stripHtmlComments(html: string): string {
|
||||||
|
return html.replace(/<!--[\s\S]*?-->/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Štýl pozadia: fotka so základnou šírkou baseW, vycentrovaná na (centerX, centerY) rámika, plus zoom a posun
|
||||||
|
function adjustedImageBgStyle(p: any, baseW: number, centerX: number, centerY: number): string {
|
||||||
|
const scale = p.imageScale ?? 1;
|
||||||
|
const aspect = p.imageAspect || 1;
|
||||||
|
const w = Math.round(baseW * scale);
|
||||||
|
const h = Math.round(w * aspect);
|
||||||
|
const x = Math.round(centerX - w / 2 + (p.imageOffsetX || 0));
|
||||||
|
const y = Math.round(centerY - h / 2 + (p.imageOffsetY || 0));
|
||||||
|
return `background-image: url('${escapeHtml(p.imageUrl)}'); background-repeat: no-repeat; background-size: ${w}px ${h}px; background-position: ${x}px ${y}px;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Riadok s fotkou pod sebou (rozloženie "pod obrázkom" a vernostné karty)
|
||||||
|
function renderStackedImageRow(p: any, imgHtml: string, cellStyle: string, cardWidth: number, maxImgW: number, alt: string): string {
|
||||||
|
if (!hasImageAdjust(p)) {
|
||||||
|
return `
|
||||||
|
<tr height="160">
|
||||||
|
<td height="160" style="${cellStyle}" valign="middle">
|
||||||
|
${imgHtml}
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
const baseW = Math.min(maxImgW, cardWidth - 16);
|
||||||
|
const baseH = Math.round(baseW * (p.imageAspect || 1));
|
||||||
|
const frameH = Math.max(160, baseH + 15);
|
||||||
|
return `
|
||||||
|
<!--[if mso]>
|
||||||
|
<tr><td height="160" style="${cellStyle}" valign="middle">${imgHtml}</td></tr>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<tr height="${frameH}">
|
||||||
|
<td height="${frameH}" role="img" aria-label="${escapeHtml(alt)}" style="height: ${frameH}px; line-height: ${frameH}px; font-size: 0; ${adjustedImageBgStyle(p, baseW, cardWidth / 2, baseH / 2)}"> </td>
|
||||||
|
</tr>
|
||||||
|
<!--<![endif]-->`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderExactProductCard(
|
function renderExactProductCard(
|
||||||
p: ProductItem,
|
p: ProductItem,
|
||||||
globalValidity: string,
|
globalValidity: string,
|
||||||
@@ -640,7 +690,8 @@ function renderExactProductCard(
|
|||||||
badgePrcBg: string,
|
badgePrcBg: string,
|
||||||
badgePrcColor: string,
|
badgePrcColor: string,
|
||||||
badgeCustomTxt: string | null,
|
badgeCustomTxt: string | null,
|
||||||
sectionTitle?: string
|
sectionTitle?: string,
|
||||||
|
cardWidth: number = 294 // nominálna šírka karty v px (2 stĺpce = 294, 1 stĺpec = 600)
|
||||||
): string {
|
): string {
|
||||||
const validity = p.validityOverride || globalValidity;
|
const validity = p.validityOverride || globalValidity;
|
||||||
const effectiveCardBg = p.cardBgColor || cardBg || '#feeaea';
|
const effectiveCardBg = p.cardBgColor || cardBg || '#feeaea';
|
||||||
@@ -726,15 +777,36 @@ function renderExactProductCard(
|
|||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
${(() => {
|
||||||
<td width="58%" height="160" style="padding: 8px 4px 8px 8px; text-align: center; height: 160px;" valign="middle">
|
const alt = pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt');
|
||||||
<img src="${escapeHtml(p.imageUrl)}" width="180" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt'))}" style="display: block; width: 100%; max-width: 180px; margin: 0 auto; border-radius: 8px;">
|
const imgCell = `<td width="58%" height="160" style="padding: 8px 4px 8px 8px; text-align: center; height: 160px;" valign="middle">
|
||||||
</td>
|
<img src="${escapeHtml(p.imageUrl)}" width="180" alt="${escapeHtml(alt)}" style="display: block; width: 100%; max-width: 180px; margin: 0 auto; border-radius: 8px;">
|
||||||
<td width="42%" style="padding: 8px 8px 8px 4px; text-align: center;" valign="middle">
|
</td>`;
|
||||||
|
const badgeCell = `<td width="42%" style="padding: 8px 8px 8px 4px; text-align: center;" valign="middle">
|
||||||
${renderBadgeContent(bWidth, bAlign, padTop, padBottom, marginStyle)}
|
${renderBadgeContent(bWidth, bAlign, padTop, padBottom, marginStyle)}
|
||||||
${p.unitPrice ? `<div style="font-size: 8px; color: #666666; font-weight: 500; font-family: Arial, sans-serif; text-align: center; margin-top: 4px;">${escapeHtml(p.unitPrice)}</div>` : ''}
|
${p.unitPrice ? `<div style="font-size: 8px; color: #666666; font-weight: 500; font-family: Arial, sans-serif; text-align: center; margin-top: 4px;">${escapeHtml(p.unitPrice)}</div>` : ''}
|
||||||
|
</td>`;
|
||||||
|
if (!hasImageAdjust(p)) {
|
||||||
|
return `<tr>${imgCell}${badgeCell}</tr>`;
|
||||||
|
}
|
||||||
|
// Upravená fotka: pozadie celého riadku, cenovka je nad ňou (produkt sa dá zasunúť pod cenovku)
|
||||||
|
const baseW = Math.min(180, Math.round(cardWidth * 0.58) - 12);
|
||||||
|
const frameH = Math.max(160, Math.round(baseW * (p.imageAspect || 1)) + 16);
|
||||||
|
return `
|
||||||
|
<!--[if mso]><tr>${imgCell}${stripHtmlComments(badgeCell)}</tr><![endif]-->
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" height="${frameH}" role="img" aria-label="${escapeHtml(alt)}" style="height: ${frameH}px; ${adjustedImageBgStyle(p, baseW, Math.round(cardWidth * 0.29), frameH / 2)}">
|
||||||
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="${frameH}">
|
||||||
|
<tr>
|
||||||
|
<td width="58%" height="${frameH}" style="font-size: 0; line-height: 0;"> </td>
|
||||||
|
${badgeCell}
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<!--<![endif]-->`;
|
||||||
|
})()}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" height="54" style="padding: 0 8px 10px 8px; text-align: center; height: 54px; min-height: 54px;" valign="top">
|
<td colspan="2" height="54" style="padding: 0 8px 10px 8px; text-align: center; height: 54px; min-height: 54px;" valign="top">
|
||||||
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>
|
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>
|
||||||
@@ -756,11 +828,14 @@ function renderExactProductCard(
|
|||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr height="160">
|
${renderStackedImageRow(
|
||||||
<td height="160" style="padding: 0 8px 15px 8px; text-align: center; height: 160px;" valign="middle">
|
p,
|
||||||
<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt'))}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">
|
`<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt'))}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">`,
|
||||||
</td>
|
'padding: 0 8px 15px 8px; text-align: center; height: 160px;',
|
||||||
</tr>
|
cardWidth,
|
||||||
|
220,
|
||||||
|
pickAlt(p.imageAlt, p.title, sectionTitle, 'Produkt')
|
||||||
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td height="54" style="padding: 0 8px 10px 8px; text-align: center; height: 54px; min-height: 54px;" valign="top">
|
<td height="54" style="padding: 0 8px 10px 8px; text-align: center; height: 54px; min-height: 54px;" valign="top">
|
||||||
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>
|
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>
|
||||||
@@ -813,11 +888,21 @@ function renderExactLoyaltyCard(p: any, validity: string): string {
|
|||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
${hasImageAdjust(p)
|
||||||
|
? renderStackedImageRow(
|
||||||
|
p,
|
||||||
|
`<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, 'Produkt'))}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">`,
|
||||||
|
'padding: 0 8px 15px 8px; text-align: center; height: 160px;',
|
||||||
|
294,
|
||||||
|
220,
|
||||||
|
pickAlt(p.imageAlt, p.title, 'Produkt')
|
||||||
|
)
|
||||||
|
: `
|
||||||
<tr height="1">
|
<tr height="1">
|
||||||
<td style="padding: 0 8px 15px 8px; text-align: center; height: 160px;" valign="middle">
|
<td style="padding: 0 8px 15px 8px; text-align: center; height: 160px;" valign="middle">
|
||||||
<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, 'Produkt'))}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">
|
<img src="${escapeHtml(p.imageUrl)}" width="220" alt="${escapeHtml(pickAlt(p.imageAlt, p.title, 'Produkt'))}" style="display: block; width: 100%; max-width: 220px; margin: 0 auto; border-radius: 8px;">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>`}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 0 8px 10px 8px; text-align: center; height: 100%; min-height: 44px;" valign="top">
|
<td style="padding: 0 8px 10px 8px; text-align: center; height: 100%; min-height: 44px;" valign="top">
|
||||||
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>
|
<h3 class="force-text-dark" style="margin: 0; font-size: 15px; color: #1a1a1a; font-weight: 600; line-height: 1.25;">${escapeHtml(p.title)}</h3>
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export interface ProductItem {
|
|||||||
showSlovakFlag?: boolean;
|
showSlovakFlag?: boolean;
|
||||||
customBadgePngUrl?: string; // Voliteľná statická PNG cenovka
|
customBadgePngUrl?: string; // Voliteľná statická PNG cenovka
|
||||||
imageAlt?: string; // Ručný alt text fotky (inak názov produktu)
|
imageAlt?: string; // Ručný alt text fotky (inak názov produktu)
|
||||||
|
imageScale?: number; // Zoom fotky (1 = 100 %)
|
||||||
|
imageOffsetX?: number; // Posun fotky vodorovne v px (+ doprava)
|
||||||
|
imageOffsetY?: number; // Posun fotky zvisle v px (+ nadol)
|
||||||
|
imageAspect?: number; // Pomer strán fotky výška/šírka (pre výpočet pozadia)
|
||||||
badgeLayout?: 'below' | 'side'; // Poloha cenovky: 'below' = pod obrázkom (default), 'side' = vedľa obrázka
|
badgeLayout?: 'below' | 'side'; // Poloha cenovky: 'below' = pod obrázkom (default), 'side' = vedľa obrázka
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user