626 lines
27 KiB
TypeScript
626 lines
27 KiB
TypeScript
'use client';
|
||
|
||
// Editor jedného produktu (bežné aj vernostné produkty) – zbalený po produktoch, len to, čo treba:
|
||
// texty, fotka (zoom/posun), cenovka (šablóna, veľkosť, ceny), dostupnosť a ďalšie možnosti.
|
||
import React, { useEffect, useRef } from 'react';
|
||
import { BadgeTemplate, CardProduct, ProductLabel, ProductPhoto } from '@/types/newsletter';
|
||
import { ChevronDown, ChevronUp, Trash2, Pencil, Globe, Layers } from 'lucide-react';
|
||
import { ImageUploadInput } from './ImageUploadInput';
|
||
import { pickAlt } from '@/lib/altText';
|
||
import { DEFAULT_STORES_OFFSET_X, DEFAULT_STORES_OFFSET_Y, DEFAULT_STORES_SIZE, MAIN_PHOTO, badgeWidthFor, photoStack, productCardAlt } from '@/lib/productCard';
|
||
|
||
export const PRODUCT_BG_PRESETS = [
|
||
{ label: 'Červená', sub: 'Fantasticky', color: '#feeaea', border: '#f5d4d4' },
|
||
{ label: 'Žltá', sub: 'Domáce', color: '#fef0c7', border: '#e6d49a' },
|
||
{ label: 'Hnedá', sub: 'Gril', color: '#dcb085', border: '#c49a6c' },
|
||
{ label: 'Sivá', sub: 'Neutrálna', color: '#e6e1d9', border: '#d5cfc5' },
|
||
{ label: 'Zelená', sub: 'Bio', color: '#e3f0e4', border: '#c8e6c9' },
|
||
{ label: 'Biela', sub: 'Čistá', color: '#ffffff', border: '#e5e7eb' }
|
||
];
|
||
|
||
type Variant = 'standard' | 'loyalty';
|
||
// Textové polia cien produktu (bežné aj vernostné)
|
||
type TextKey =
|
||
| 'priceMain' | 'priceSub' | 'discountText'
|
||
| 'cardPriceMain' | 'cardPriceSub' | 'cardDiscount' | 'cardUnitPrice'
|
||
| 'noCardPriceMain' | 'noCardPriceSub' | 'noCardDiscount' | 'noCardUnitPrice';
|
||
type Patch = Record<string, unknown>;
|
||
|
||
interface ProductEditorProps {
|
||
product: CardProduct;
|
||
index: number;
|
||
variant: Variant;
|
||
availableBadges: BadgeTemplate[];
|
||
open: boolean; // harmonika: otvorený je vždy len jeden produkt
|
||
onToggle: () => void;
|
||
onChange: (patch: Patch) => void;
|
||
onDelete?: () => void;
|
||
onOpenBadgeEditor?: () => void;
|
||
onApplyBadgeToSection?: () => void;
|
||
onApplyBadgeToAll?: () => void;
|
||
onApplyStoresOffsetToAll?: (x: number, y: number, size: number) => void; // poloha a veľkosť štítkov pre všetky produkty
|
||
}
|
||
|
||
const inputCls = 'w-full text-xs border border-gray-300 rounded-lg px-2 py-1.5 bg-white';
|
||
|
||
const Field: React.FC<{ label: string; children: React.ReactNode; className?: string }> = ({ label, children, className }) => (
|
||
<label className={`block ${className || ''}`}>
|
||
<span className="text-[10px] text-gray-500 font-semibold block mb-0.5">{label}</span>
|
||
{children}
|
||
</label>
|
||
);
|
||
|
||
const TextInput: React.FC<{ value?: string; onChange: (v: string) => void; placeholder?: string; bold?: boolean }> = ({
|
||
value,
|
||
onChange,
|
||
placeholder,
|
||
bold
|
||
}) => (
|
||
<input
|
||
type="text"
|
||
value={value || ''}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
placeholder={placeholder}
|
||
className={`${inputCls} ${bold ? 'font-bold' : ''}`}
|
||
/>
|
||
);
|
||
|
||
const Slider: React.FC<{
|
||
label: string;
|
||
value: number;
|
||
display: string;
|
||
min: number;
|
||
max: number;
|
||
step: number;
|
||
onChange: (v: number) => void;
|
||
}> = ({ label, value, display, min, max, step, onChange }) => (
|
||
<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) => onChange(Number(e.target.value))}
|
||
className="w-full h-1 accent-red-600 cursor-pointer"
|
||
/>
|
||
<span className="text-[10px] font-mono text-gray-700 text-right">{display}</span>
|
||
</div>
|
||
);
|
||
|
||
const Block: React.FC<{ title: string; action?: React.ReactNode; children: React.ReactNode }> = ({ title, action, children }) => (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-[10px] font-bold text-gray-700 uppercase tracking-wider">{title}</span>
|
||
{action}
|
||
</div>
|
||
{children}
|
||
</div>
|
||
);
|
||
|
||
const ResetLink: React.FC<{ onClick: () => void }> = ({ onClick }) => (
|
||
<button type="button" onClick={onClick} className="text-[10px] font-bold text-gray-400 hover:text-red-600">
|
||
Obnoviť
|
||
</button>
|
||
);
|
||
|
||
// Poradie fotiek ako vrstvy: navrchu zoznamu je fotka najviac vpredu
|
||
const PhotoOrderList: React.FC<{ product: CardProduct; onChange: (order: string[]) => void }> = ({ product, onChange }) => {
|
||
const stack = photoStack(product); // odzadu dopredu
|
||
const extras = product.extraPhotos || [];
|
||
const move = (id: string, dir: 1 | -1) => {
|
||
const i = stack.indexOf(id);
|
||
const j = i + dir;
|
||
if (j < 0 || j >= stack.length) return;
|
||
const next = [...stack];
|
||
[next[i], next[j]] = [next[j], next[i]];
|
||
onChange(next);
|
||
};
|
||
const front = [...stack].reverse();
|
||
return (
|
||
<div className="rounded-lg border border-gray-200 bg-white p-2 space-y-1">
|
||
<span className="text-[10px] font-bold text-gray-600">Poradie fotiek (hore = vpredu)</span>
|
||
{front.map((id, k) => {
|
||
const isMain = id === MAIN_PHOTO;
|
||
const extraIdx = extras.findIndex((f) => f.id === id);
|
||
const url = isMain ? product.imageUrl : extras[extraIdx]?.url;
|
||
return (
|
||
<div key={id} className={`flex items-center gap-2 rounded-md px-1.5 py-1 ${isMain ? 'bg-red-50' : 'bg-gray-50'}`}>
|
||
<span className="w-7 h-7 rounded bg-white border border-gray-200 overflow-hidden flex items-center justify-center shrink-0">
|
||
{url && (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img src={url} alt="" className="w-full h-full object-contain" />
|
||
)}
|
||
</span>
|
||
<span className="flex-1 text-[11px] font-semibold text-gray-800">{isMain ? 'Hlavná fotka' : `Fotka ${extraIdx + 2}`}</span>
|
||
<button type="button" disabled={k === 0} onClick={() => move(id, 1)} className="p-0.5 text-gray-500 hover:text-gray-900 disabled:opacity-25" title="Dopredu">
|
||
<ChevronUp className="w-3.5 h-3.5" />
|
||
</button>
|
||
<button type="button" disabled={k === front.length - 1} onClick={() => move(id, -1)} className="p-0.5 text-gray-500 hover:text-gray-900 disabled:opacity-25" title="Dozadu">
|
||
<ChevronDown className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Ďalšia fotka v karte (bundle): obrázok, poloha, veľkosť
|
||
const ExtraPhotoEditor: React.FC<{
|
||
index: number;
|
||
photo: ProductPhoto;
|
||
onChange: (patch: Partial<ProductPhoto>) => void;
|
||
onRemove: () => void;
|
||
}> = ({ index, photo, onChange, onRemove }) => (
|
||
<div className="rounded-lg border border-gray-200 bg-white p-2 space-y-1.5">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-[10px] font-bold text-gray-600">Fotka {index + 2}</span>
|
||
<button type="button" onClick={onRemove} className="text-gray-300 hover:text-red-600 p-0.5" title="Odstrániť fotku">
|
||
<Trash2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
<ImageUploadInput label="" value={photo.url} onChange={(url) => onChange({ url })} />
|
||
<Slider label="Vodorovne" value={photo.x} display={`${photo.x} %`} min={0} max={100} step={1} onChange={(v) => onChange({ x: v })} />
|
||
<Slider label="Zvisle" value={photo.y} display={`${photo.y} %`} min={0} max={100} step={1} onChange={(v) => onChange({ y: v })} />
|
||
<Slider label="Veľkosť" value={photo.scale} display={`${Math.round(photo.scale * 100)} %`} min={0.2} max={2} step={0.05} onChange={(v) => onChange({ scale: v })} />
|
||
</div>
|
||
);
|
||
|
||
// Nastavenie jedného vlastného štítka na fotke
|
||
const LABEL_COLORS: Array<[string, string]> = [
|
||
['#e2001a', 'Červená'],
|
||
['#ffde00', 'Žltá'],
|
||
['#003399', 'Modrá'],
|
||
['#000000', 'Čierna'],
|
||
['#ffffff', 'Biela']
|
||
];
|
||
|
||
const LabelEditor: React.FC<{
|
||
label: ProductLabel;
|
||
onChange: (patch: Partial<ProductLabel>) => void;
|
||
onRemove: () => void;
|
||
}> = ({ label, onChange, onRemove }) => {
|
||
const swatches = (key: 'bg' | 'color') => (
|
||
<div className="flex items-center gap-1">
|
||
{LABEL_COLORS.map(([c, name]) => (
|
||
<button
|
||
key={c}
|
||
type="button"
|
||
title={name}
|
||
onClick={() => onChange({ [key]: c })}
|
||
className={`w-4 h-4 rounded-full border ${label[key]?.toLowerCase() === c ? 'ring-2 ring-red-500 border-white' : 'border-black/20'}`}
|
||
style={{ backgroundColor: c }}
|
||
/>
|
||
))}
|
||
<input
|
||
type="color"
|
||
value={label[key] || '#ffffff'}
|
||
onChange={(e) => onChange({ [key]: e.target.value })}
|
||
className="w-4 h-4 rounded cursor-pointer border border-gray-300 p-0"
|
||
title="Vlastná farba"
|
||
/>
|
||
</div>
|
||
);
|
||
return (
|
||
<div className="rounded-lg border border-gray-200 bg-white p-2 space-y-1.5">
|
||
<div className="flex items-center gap-1.5">
|
||
<input
|
||
value={label.text}
|
||
onChange={(e) => onChange({ text: e.target.value })}
|
||
placeholder="Text štítka (<br> = nový riadok)"
|
||
className="flex-1 min-w-0 text-xs font-bold uppercase border border-gray-300 rounded-lg px-2 py-1"
|
||
/>
|
||
<button type="button" onClick={onRemove} className="text-gray-300 hover:text-red-600 p-0.5" title="Zmazať štítok">
|
||
<Trash2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
<Slider label="Vodorovne" value={label.x} display={`${label.x} %`} min={0} max={100} step={1} onChange={(v) => onChange({ x: v })} />
|
||
<Slider label="Zvisle" value={label.y} display={`${label.y} %`} min={0} max={100} step={1} onChange={(v) => onChange({ y: v })} />
|
||
<Slider label="Veľkosť" value={label.size} display={`${label.size} px`} min={6} max={40} step={1} onChange={(v) => onChange({ size: v })} />
|
||
<Slider label="Natočenie" value={label.rotate || 0} display={`${label.rotate || 0}°`} min={-45} max={45} step={1} onChange={(v) => onChange({ rotate: v })} />
|
||
<div className="grid grid-cols-2 gap-2 pt-0.5">
|
||
<div className="space-y-1">
|
||
<span className="text-[10px] font-semibold text-gray-600">Pozadie</span>
|
||
{swatches('bg')}
|
||
</div>
|
||
<div className="space-y-1">
|
||
<span className="text-[10px] font-semibold text-gray-600">Text</span>
|
||
{swatches('color')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Šablóna používa ceny s kartou → zobrazíme aj polia s kartou
|
||
const usesCardPrice = (t?: BadgeTemplate) => !!t && JSON.stringify(t.layersConfig || []).includes('_s_kartou');
|
||
|
||
export const ProductEditor: React.FC<ProductEditorProps> = ({
|
||
product: p,
|
||
index,
|
||
variant,
|
||
availableBadges,
|
||
open,
|
||
onToggle,
|
||
onChange,
|
||
onDelete,
|
||
onOpenBadgeEditor,
|
||
onApplyBadgeToSection,
|
||
onApplyBadgeToAll,
|
||
onApplyStoresOffsetToAll
|
||
}) => {
|
||
// Po otvorení (aj klikom na produkt v náhľade) posunieme panel na tento produkt
|
||
const rootRef = useRef<HTMLDivElement>(null);
|
||
useEffect(() => {
|
||
if (open) rootRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||
}, [open]);
|
||
const isLoyalty = variant === 'loyalty';
|
||
const set = (key: string) => (v: unknown) => onChange({ [key]: v });
|
||
|
||
const scale = p.imageScale ?? 1;
|
||
const offsetX = p.imageOffsetX || 0;
|
||
const offsetY = p.imageOffsetY || 0;
|
||
const storesX = p.storesOffsetX ?? DEFAULT_STORES_OFFSET_X;
|
||
const storesY = p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y;
|
||
const storesSize = p.storesSize ?? DEFAULT_STORES_SIZE;
|
||
const hasStores = !!(p.stores?.potraviny || p.stores?.supermarket || p.stores?.tempo);
|
||
const badgeWidth = badgeWidthFor(p, variant);
|
||
|
||
const selectBadge = (id: string) => {
|
||
if (id === 'default') {
|
||
onChange({ badgeCustomTemplate: undefined, badgeId: undefined, customBadgePngUrl: undefined, badgeWidth: undefined });
|
||
return;
|
||
}
|
||
const found = availableBadges.find((b) => b.id === id);
|
||
if (found) {
|
||
onChange({ badgeCustomTemplate: found, badgeId: found.id, badgeWidth: found.width ?? 110, customBadgePngUrl: undefined });
|
||
}
|
||
};
|
||
|
||
const text = (key: TextKey) => p[key] as string | undefined;
|
||
|
||
const priceRow = (label: string, mainKey: TextKey, subKey: TextKey, discountKey: TextKey, unitKey?: TextKey) => (
|
||
<div className="space-y-1">
|
||
<span className="text-[10px] font-semibold text-gray-600">{label}</span>
|
||
<div className={`grid ${unitKey ? 'grid-cols-[1fr_1fr_1fr]' : 'grid-cols-3'} gap-1.5`}>
|
||
<Field label="€">
|
||
<TextInput value={text(mainKey)} onChange={set(mainKey)} bold />
|
||
</Field>
|
||
<Field label="Centy">
|
||
<TextInput value={text(subKey)} onChange={set(subKey)} bold />
|
||
</Field>
|
||
<Field label="Zľava">
|
||
<TextInput value={text(discountKey)} onChange={set(discountKey)} placeholder="25%" />
|
||
</Field>
|
||
</div>
|
||
{unitKey && (
|
||
<Field label="Jednotková cena">
|
||
<TextInput value={text(unitKey)} onChange={set(unitKey)} placeholder="jednotková cena 9,43 EUR/kg" />
|
||
</Field>
|
||
)}
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<div
|
||
ref={rootRef}
|
||
className={`rounded-xl border bg-gray-50 overflow-hidden scroll-mt-2 ${open ? 'border-red-400 ring-1 ring-red-200' : 'border-gray-200'}`}
|
||
>
|
||
{/* Hlavička produktu – klik rozbalí / zbalí */}
|
||
<div className="flex items-center gap-2 px-2.5 py-2 bg-white">
|
||
<button type="button" onClick={onToggle} className="flex-1 flex items-center gap-2 min-w-0 text-left">
|
||
<span className="w-8 h-8 rounded-md bg-gray-100 border border-gray-200 shrink-0 overflow-hidden flex items-center justify-center">
|
||
{p.imageUrl && (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img src={p.imageUrl} alt="" className="w-full h-full object-contain" />
|
||
)}
|
||
</span>
|
||
<span className="text-xs font-bold text-gray-900 truncate">{pickAlt(p.title) || `Produkt ${index + 1}`}</span>
|
||
<ChevronDown className={`w-4 h-4 text-gray-400 shrink-0 ml-auto transition-transform ${open ? 'rotate-180' : ''}`} />
|
||
</button>
|
||
{onDelete && (
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
if (confirm(`Zmazať produkt "${pickAlt(p.title) || index + 1}"?`)) onDelete();
|
||
}}
|
||
className="text-gray-300 hover:text-red-600 p-1"
|
||
title="Zmazať produkt"
|
||
>
|
||
<Trash2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{open && (
|
||
<div className="p-3 space-y-4 border-t border-gray-200">
|
||
{/* TEXTY */}
|
||
<Block title="Texty">
|
||
<Field label="Názov (Enter = nový riadok)">
|
||
<textarea
|
||
rows={2}
|
||
value={p.title || ''}
|
||
onChange={(e) => onChange({ title: e.target.value })}
|
||
className={`${inputCls} font-bold resize-y`}
|
||
/>
|
||
</Field>
|
||
<div className="grid grid-cols-2 gap-1.5">
|
||
<Field label="Gramáž / množstvo">
|
||
<TextInput value={p.quantity} onChange={set('quantity')} placeholder="35 g" />
|
||
</Field>
|
||
{isLoyalty ? (
|
||
<Field label="Poznámka">
|
||
<TextInput value={p.note} onChange={set('note')} placeholder="Ilustračné foto" />
|
||
</Field>
|
||
) : (
|
||
<Field label="Jednotková cena">
|
||
<TextInput value={p.unitPrice} onChange={set('unitPrice')} placeholder="jednotková cena 9,43 EUR/kg" />
|
||
</Field>
|
||
)}
|
||
</div>
|
||
{!isLoyalty && (
|
||
<Field label="Poznámka">
|
||
<TextInput value={p.note} onChange={set('note')} placeholder="Ilustračné foto" />
|
||
</Field>
|
||
)}
|
||
</Block>
|
||
|
||
{/* FOTKA */}
|
||
<Block
|
||
title="Fotka"
|
||
action={(scale !== 1 || offsetX !== 0 || offsetY !== 0) && (
|
||
<ResetLink onClick={() => onChange({ imageScale: undefined, imageOffsetX: undefined, imageOffsetY: undefined })} />
|
||
)}
|
||
>
|
||
<ImageUploadInput
|
||
label=""
|
||
value={p.imageUrl || ''}
|
||
onChange={set('imageUrl')}
|
||
altValue={p.imageAlt}
|
||
onAltChange={set('imageAlt')}
|
||
altPlaceholder={productCardAlt({ ...p, imageAlt: undefined }, variant)}
|
||
/>
|
||
<Slider label="Veľkosť" value={scale} display={`${Math.round(scale * 100)} %`} min={0.4} max={2.5} step={0.05} onChange={set('imageScale')} />
|
||
<Slider label="Vodorovne" value={offsetX} display={`${offsetX}`} min={-150} max={150} step={1} onChange={set('imageOffsetX')} />
|
||
<Slider label="Zvisle" value={offsetY} display={`${offsetY}`} min={-120} max={120} step={1} onChange={set('imageOffsetY')} />
|
||
|
||
{/* Ďalšie fotky (bundle) */}
|
||
{(p.extraPhotos || []).map((f, i) => (
|
||
<ExtraPhotoEditor
|
||
key={f.id}
|
||
index={i}
|
||
photo={f}
|
||
onChange={(patch) => onChange({ extraPhotos: (p.extraPhotos || []).map((x, j) => (j === i ? { ...x, ...patch } : x)) })}
|
||
onRemove={() =>
|
||
onChange({
|
||
extraPhotos: (p.extraPhotos || []).filter((_, j) => j !== i),
|
||
photoOrder: photoStack(p).filter((id) => id !== f.id)
|
||
})
|
||
}
|
||
/>
|
||
))}
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const id = `ph-${Date.now()}`;
|
||
onChange({
|
||
extraPhotos: [...(p.extraPhotos || []), { id, url: '', x: 62, y: 52, scale: 0.75 }],
|
||
photoOrder: [...photoStack(p), id] // nová fotka ide dopredu
|
||
});
|
||
}}
|
||
className="w-full text-[10px] font-bold text-gray-600 border border-dashed border-gray-300 hover:border-gray-400 hover:bg-white rounded-lg py-1.5"
|
||
>
|
||
+ Pridať ďalšiu fotku (bundle)
|
||
</button>
|
||
|
||
{(p.extraPhotos || []).length > 0 && <PhotoOrderList product={p} onChange={(photoOrder) => onChange({ photoOrder })} />}
|
||
</Block>
|
||
|
||
{/* CENOVKA */}
|
||
<Block title="Cenovka">
|
||
<div className="flex items-center gap-1.5">
|
||
<select
|
||
value={p.customBadgePngUrl ? 'png' : p.badgeCustomTemplate?.id || 'default'}
|
||
onChange={(e) => selectBadge(e.target.value)}
|
||
className={`${inputCls} font-medium`}
|
||
>
|
||
<option value="default">{isLoyalty ? 'Predvolená (bez karty / s kartou)' : 'Predvolená COOP cenovka'}</option>
|
||
{availableBadges.map((b) => (
|
||
<option key={b.id} value={b.id}>
|
||
{b.name}
|
||
</option>
|
||
))}
|
||
{p.customBadgePngUrl && <option value="png">Vlastná PNG cenovka</option>}
|
||
</select>
|
||
{onOpenBadgeEditor && (
|
||
<button
|
||
type="button"
|
||
onClick={onOpenBadgeEditor}
|
||
className="shrink-0 p-1.5 rounded-lg border border-gray-300 bg-white text-gray-600 hover:text-red-600 hover:border-red-300"
|
||
title="Editor cenoviek (vytvoriť / upraviť šablóny)"
|
||
>
|
||
<Pencil className="w-3.5 h-3.5" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<Slider label="Veľkosť" value={badgeWidth} display={`${badgeWidth} px`} min={60} max={200} step={1} onChange={set('badgeWidth')} />
|
||
|
||
{isLoyalty ? (
|
||
<>
|
||
{priceRow('S kartou', 'cardPriceMain', 'cardPriceSub', 'cardDiscount', 'cardUnitPrice')}
|
||
{priceRow('Bez karty', 'noCardPriceMain', 'noCardPriceSub', 'noCardDiscount', 'noCardUnitPrice')}
|
||
</>
|
||
) : (
|
||
<>
|
||
{priceRow('Cena', 'priceMain', 'priceSub', 'discountText')}
|
||
{usesCardPrice(p.badgeCustomTemplate) && priceRow('S kartou', 'cardPriceMain', 'cardPriceSub', 'cardDiscount')}
|
||
</>
|
||
)}
|
||
|
||
{p.badgeCustomTemplate && (onApplyBadgeToSection || onApplyBadgeToAll) && (
|
||
<div className="flex flex-wrap gap-1.5 pt-1">
|
||
{onApplyBadgeToSection && (
|
||
<button
|
||
type="button"
|
||
onClick={onApplyBadgeToSection}
|
||
className="flex items-center gap-1 text-[10px] font-bold text-gray-700 bg-white border border-gray-300 hover:bg-gray-100 px-2 py-1 rounded-lg"
|
||
>
|
||
<Layers className="w-3 h-3" /> Použiť v celej sekcii
|
||
</button>
|
||
)}
|
||
{onApplyBadgeToAll && (
|
||
<button
|
||
type="button"
|
||
onClick={onApplyBadgeToAll}
|
||
className="flex items-center gap-1 text-[10px] font-bold text-red-700 bg-red-50 border border-red-200 hover:bg-red-100 px-2 py-1 rounded-lg"
|
||
>
|
||
<Globe className="w-3 h-3" /> Použiť v celom newsletteri
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</Block>
|
||
|
||
{/* DOSTUPNOSŤ */}
|
||
<Block
|
||
title="Dostupnosť"
|
||
action={hasStores && (storesX !== DEFAULT_STORES_OFFSET_X || storesY !== DEFAULT_STORES_OFFSET_Y || storesSize !== DEFAULT_STORES_SIZE) && (
|
||
<ResetLink onClick={() => onChange({ storesOffsetX: undefined, storesOffsetY: undefined, storesSize: undefined })} />
|
||
)}
|
||
>
|
||
<div className="grid grid-cols-3 gap-1.5">
|
||
{(
|
||
[
|
||
['potraviny', 'Potraviny', 'bg-[#e2001a]'],
|
||
['supermarket', 'Supermarket', 'bg-[#f9890e]'],
|
||
['tempo', 'Tempo', 'bg-[#005239]']
|
||
] as const
|
||
).map(([key, label, color]) => {
|
||
const on = !!p.stores?.[key];
|
||
return (
|
||
<button
|
||
key={key}
|
||
type="button"
|
||
onClick={() => onChange({ stores: { ...(p.stores || {}), [key]: !on } })}
|
||
className={`text-[10px] font-bold py-1.5 rounded-lg border transition-colors ${
|
||
on ? `${color} text-white border-transparent` : 'bg-white text-gray-400 border-gray-200'
|
||
}`}
|
||
>
|
||
{label}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
{hasStores && (
|
||
<>
|
||
<Slider label="Štítky ↔" value={storesX} display={`${storesX}`} min={-80} max={80} step={1} onChange={set('storesOffsetX')} />
|
||
<Slider label="Štítky ↕" value={storesY} display={`${storesY}`} min={-40} max={60} step={1} onChange={set('storesOffsetY')} />
|
||
<Slider label="Veľkosť" value={storesSize} display={`${storesSize} px`} min={8} max={30} step={1} onChange={set('storesSize')} />
|
||
{onApplyStoresOffsetToAll && (
|
||
<button
|
||
type="button"
|
||
onClick={() => onApplyStoresOffsetToAll(storesX, storesY, storesSize)}
|
||
className="flex items-center gap-1 text-[10px] font-bold text-red-700 bg-red-50 border border-red-200 hover:bg-red-100 px-2 py-1 rounded-lg"
|
||
>
|
||
<Globe className="w-3 h-3" /> Polohu a veľkosť štítkov použiť na všetky produkty
|
||
</button>
|
||
)}
|
||
</>
|
||
)}
|
||
</Block>
|
||
|
||
{/* VLASTNÉ ŠTÍTKY NA FOTKE */}
|
||
<Block
|
||
title="Štítky na fotke"
|
||
action={
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
onChange({
|
||
labels: [
|
||
...(p.labels || []),
|
||
{ id: `lbl-${Date.now()}`, text: 'NOVINKA', x: 30, y: 20, size: 12, bg: '#e2001a', color: '#ffffff', rotate: 0 }
|
||
]
|
||
})
|
||
}
|
||
className="text-[10px] font-bold text-red-700 hover:text-red-900"
|
||
>
|
||
+ Pridať štítok
|
||
</button>
|
||
}
|
||
>
|
||
{(p.labels || []).length === 0 && (
|
||
<p className="text-[10px] text-gray-400">Napr. „PODIEL MÄSA 92 %“ alebo „NOVINKA“ priamo na fotke produktu.</p>
|
||
)}
|
||
{(p.labels || []).map((l, i) => (
|
||
<LabelEditor
|
||
key={l.id}
|
||
label={l}
|
||
onChange={(patch) => onChange({ labels: (p.labels || []).map((x, j) => (j === i ? { ...x, ...patch } : x)) })}
|
||
onRemove={() => onChange({ labels: (p.labels || []).filter((_, j) => j !== i) })}
|
||
/>
|
||
))}
|
||
</Block>
|
||
|
||
{/* ĎALŠIE MOŽNOSTI */}
|
||
<details className="group">
|
||
<summary className="text-[10px] font-bold text-gray-500 uppercase tracking-wider cursor-pointer select-none hover:text-gray-800">
|
||
Ďalšie možnosti
|
||
</summary>
|
||
<div className="pt-3 space-y-3">
|
||
<Field label="Farba karty">
|
||
<div className="flex items-center gap-1.5 flex-wrap">
|
||
{PRODUCT_BG_PRESETS.map((preset) => (
|
||
<button
|
||
key={preset.color}
|
||
type="button"
|
||
title={preset.label}
|
||
onClick={() => onChange({ cardBgColor: preset.color })}
|
||
className={`w-6 h-6 rounded-full border ${
|
||
(p.cardBgColor || '').toLowerCase() === preset.color ? 'ring-2 ring-red-500 border-white' : 'border-black/15'
|
||
}`}
|
||
style={{ backgroundColor: preset.color }}
|
||
/>
|
||
))}
|
||
<input
|
||
type="color"
|
||
value={p.cardBgColor || '#feeaea'}
|
||
onChange={(e) => onChange({ cardBgColor: e.target.value })}
|
||
className="w-6 h-6 rounded cursor-pointer border border-gray-300 p-0 bg-white"
|
||
title="Vlastná farba"
|
||
/>
|
||
</div>
|
||
</Field>
|
||
|
||
<Field label="Vlastná platnosť (len pre tento produkt)">
|
||
<TextInput value={p.validityOverride} onChange={set('validityOverride')} placeholder="napr. 2. 5. – 9. 5. 2026" />
|
||
</Field>
|
||
|
||
<div className="space-y-1.5">
|
||
<ImageUploadInput
|
||
label="Vlastná PNG cenovka (výnimka)"
|
||
value={p.customBadgePngUrl || ''}
|
||
onChange={(url) => onChange({ customBadgePngUrl: url || undefined })}
|
||
placeholder="Celá cenovka ako PNG – prekryje šablónu"
|
||
/>
|
||
{p.customBadgePngUrl && (
|
||
<button
|
||
type="button"
|
||
onClick={() => onChange({ customBadgePngUrl: undefined })}
|
||
className="w-full py-1 text-[10px] font-bold text-amber-800 bg-amber-50 hover:bg-amber-100 border border-amber-200 rounded-lg"
|
||
>
|
||
Odstrániť PNG a vrátiť šablónu
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|