feat: vlastne stitky na fotke produktu, Adobe Fonts pre celu aplikaciu

- pri produkte blok Stitky na fotke: text (<br> = novy riadok), poloha X/Y,
  velkost, natocenie, farba pozadia a textu; lubovolny pocet stitkov
- stitky su nad fotkou a pod cenovkou, pismo Aptly Bold; ulozia sa aj do
  obrazka karty pri exporte
- NEXT_PUBLIC_ADOBE_FONTS_URL: odkaz na web projekt Adobe Fonts sa nacita
  v celej aplikacii (Aptly pre cenovky a stitky)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-21 12:47:42 +02:00
1 parent 332ab833e9
commit 293d9b953c
5 files changed
+131 -3

No files matched your search

+100 -1
View File
@@ -3,7 +3,7 @@
// 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 } from '@/types/newsletter';
import { BadgeTemplate, CardProduct, ProductLabel } from '@/types/newsletter';
import { ChevronDown, Trash2, Pencil, Globe, Layers } from 'lucide-react';
import { ImageUploadInput } from './ImageUploadInput';
import { pickAlt } from '@/lib/altText';
@@ -105,6 +105,72 @@ const ResetLink: React.FC<{ onClick: () => void }> = ({ onClick }) => (
</button>
);
// 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');
@@ -372,6 +438,39 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
)}
</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">