feat: anchor header/footer, editable header buttons, campaign management, image uploads, badge font customization, and brevo export fix

This commit is contained in:
Jakub_Hostacny committed 2026-09-24 11:42:45 +02:00
1 parent 3dc5778155
commit 2b4d3643cb
10 files changed
+1819 -178

No files matched your search

+230 -2
View File
@@ -25,6 +25,46 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
currentPriceSub = '99',
currentUnitPrice = '1kg = 4,91 €'
}) => {
const POPULAR_FONTS = [
{ name: 'Poppins', source: 'google' as const },
{ name: 'Montserrat', source: 'google' as const },
{ name: 'Oswald', source: 'google' as const },
{ name: 'Bebas Neue', source: 'google' as const },
{ name: 'Anton', source: 'google' as const },
{ name: 'Roboto Condensed', source: 'google' as const },
{ name: 'Barlow Semi Condensed', source: 'google' as const },
{ name: 'Inter', source: 'google' as const },
{ name: 'Open Sans', source: 'google' as const },
{ name: 'Arial Narrow', source: 'system' as const },
{ name: 'Impact', source: 'system' as const }
];
const loadFontDynamically = (fontFamily: string, source: 'system' | 'google' | 'adobe' = 'google', url?: string) => {
if (typeof document === 'undefined') return;
if (source === 'adobe' && url) {
const existing = document.querySelector(`link[data-adobe-font="${url}"]`);
if (!existing) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
link.setAttribute('data-adobe-font', url);
document.head.appendChild(link);
}
} else if (source === 'google' && fontFamily) {
const clean = fontFamily.trim().replace(/\s+/g, '+');
const href = `https://fonts.googleapis.com/css2?family=${clean}:wght@400;600;700;800;900&display=swap`;
const existing = document.querySelector(`link[data-google-font="${clean}"]`);
if (!existing) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.setAttribute('data-google-font', clean);
document.head.appendChild(link);
}
}
};
const [badges, setBadges] = useState<BadgeTemplate[]>([]);
const [activeBadge, setActiveBadge] = useState<BadgeTemplate | null>(null);
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
@@ -32,6 +72,17 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
const [isExportingPng, setIsExportingPng] = useState(false);
const [isSavingDb, setIsSavingDb] = useState(false);
// Načítanie písiem pre všetky vrstvy aktívnej cenovky
useEffect(() => {
if (activeBadge?.layersConfig) {
activeBadge.layersConfig.forEach((l) => {
if (l.fontFamily) {
loadFontDynamically(l.fontFamily, l.fontSource || 'google', l.fontUrl);
}
});
}
}, [activeBadge]);
// Načítanie existujúcich šablón zo Supabase
useEffect(() => {
if (isOpen) {
@@ -366,9 +417,11 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
top: `${layer.y}px`,
transform: 'translate(-50%, -50%)',
fontSize: `${layer.fontSize}px`,
fontWeight: layer.fontWeight as any,
fontWeight: (layer.fontWeight || '700') as any,
color: layer.fill,
fontFamily: 'Poppins, Arial, sans-serif',
fontFamily: layer.fontFamily
? `'${layer.fontFamily}', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`
: 'Poppins, Arial, sans-serif',
whiteSpace: 'nowrap',
cursor: 'pointer'
}}
@@ -530,6 +583,181 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
</div>
</div>
{/* Font nastavenie */}
<div className="space-y-2 pt-2 border-t border-gray-200">
<label className="block text-[11px] font-semibold text-gray-700">
Písmo (Font) a Hrúbka
</label>
{/* Zdroj písma */}
<div className="grid grid-cols-3 gap-1 bg-gray-200 p-0.5 rounded-lg text-[10px] font-bold text-center">
<button
type="button"
onClick={() => {
updateActiveLayer({ fontSource: 'google' });
}}
className={`py-1 rounded ${
(activeLayer.fontSource || 'google') === 'google'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Google
</button>
<button
type="button"
onClick={() => {
updateActiveLayer({ fontSource: 'adobe' });
}}
className={`py-1 rounded ${
activeLayer.fontSource === 'adobe'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Adobe
</button>
<button
type="button"
onClick={() => {
updateActiveLayer({ fontSource: 'system' });
}}
className={`py-1 rounded ${
activeLayer.fontSource === 'system'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Systém
</button>
</div>
{/* Google Fonts voľba */}
{(activeLayer.fontSource || 'google') === 'google' && (
<div className="space-y-2">
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">Výber Google Fontu</label>
<select
value={
POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins'))
? activeLayer.fontFamily || 'Poppins'
: 'custom'
}
onChange={(e) => {
if (e.target.value !== 'custom') {
loadFontDynamically(e.target.value, 'google');
updateActiveLayer({ fontFamily: e.target.value, fontSource: 'google' });
}
}}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
>
{POPULAR_FONTS.filter((f) => f.source === 'google').map((f) => (
<option key={f.name} value={f.name}>
{f.name}
</option>
))}
<option value="custom">Iný Google Font (zadať vlastný)...</option>
</select>
</div>
{(!POPULAR_FONTS.some((f) => f.name === (activeLayer.fontFamily || 'Poppins')) ||
activeLayer.fontFamily === 'custom') && (
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">Názov Google Fontu</label>
<input
type="text"
placeholder="napr. Montserrat, Oswald, Syne"
value={activeLayer.fontFamily === 'custom' ? '' : activeLayer.fontFamily || ''}
onChange={(e) => {
const val = e.target.value;
loadFontDynamically(val, 'google');
updateActiveLayer({ fontFamily: val, fontSource: 'google' });
}}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
/>
</div>
)}
</div>
)}
{/* Adobe Fonts voľba */}
{activeLayer.fontSource === 'adobe' && (
<div className="space-y-2">
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">
Adobe Typekit CSS URL
</label>
<input
type="text"
placeholder="https://use.typekit.net/xxxx.css"
value={activeLayer.fontUrl || ''}
onChange={(e) => {
const url = e.target.value;
if (activeLayer.fontFamily) {
loadFontDynamically(activeLayer.fontFamily, 'adobe', url);
}
updateActiveLayer({ fontUrl: url, fontSource: 'adobe' });
}}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-mono text-[10px]"
/>
</div>
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">
Názov rodiny písma (Font Family)
</label>
<input
type="text"
placeholder="napr. futura-pt, omnes-pro"
value={activeLayer.fontFamily || ''}
onChange={(e) => {
const family = e.target.value;
if (activeLayer.fontUrl) {
loadFontDynamically(family, 'adobe', activeLayer.fontUrl);
}
updateActiveLayer({ fontFamily: family, fontSource: 'adobe' });
}}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
/>
</div>
</div>
)}
{/* Systémové písma */}
{activeLayer.fontSource === 'system' && (
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">Systémový font</label>
<select
value={activeLayer.fontFamily || 'Arial'}
onChange={(e) => updateActiveLayer({ fontFamily: e.target.value, fontSource: 'system' })}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white"
>
<option value="Arial">Arial</option>
<option value="Arial Narrow">Arial Narrow</option>
<option value="Impact">Impact</option>
<option value="Trebuchet MS">Trebuchet MS</option>
<option value="Helvetica">Helvetica</option>
<option value="Verdana">Verdana</option>
</select>
</div>
)}
{/* Hrúbka fontu */}
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">Hrúbka písma (Weight)</label>
<select
value={activeLayer.fontWeight || '700'}
onChange={(e) => updateActiveLayer({ fontWeight: e.target.value })}
className="w-full text-xs border border-gray-300 rounded-lg p-1.5 bg-white font-medium"
>
<option value="400">400 - Normal</option>
<option value="600">600 - SemiBold</option>
<option value="700">700 - Bold</option>
<option value="800">800 - ExtraBold</option>
<option value="900">900 - Black</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[10px] text-gray-500 mb-0.5">Pozícia X ({activeLayer.x}px)</label>