// Live, in-place text editing for logged-in admins (Divi-style: click the // text on the page itself, type, click away, it's saved) instead of a // separate modal disconnected from what you're actually looking at. const AdminEditContext = React.createContext({ isAdmin: false, editMode: false, setEditMode: () => {}, lang: 'it', refresh: () => {}, ov: (key, fallback) => fallback, }); // site_content has it/de/pl columns directly; only de-CH needs mapping. function contentApiLang(lang) { return lang === 'de-CH' ? 'de' : lang; } // Editable list items (services, process steps, FAQ entries, ...) are fixed // arrays in copy.js, not simple scalar CZ_EDITABLE_KEYS fields, so they're // saved under synthetic keys like "svc_0_title" instead. This turns one of // those keys plus its code default into the value to actually display. function AdminEditProvider({ children, lang, overrides, t, onSaved }) { const [isAdmin, setIsAdmin] = React.useState(false); const [editMode, setEditMode] = React.useState(false); const apiLang = contentApiLang(lang); React.useEffect(() => { fetch('/api/config-api.php/whoami') .then((r) => (r.ok ? r.json() : null)) .then((data) => { if (data && data.loggedIn) setIsAdmin(true); }) .catch(() => {}); }, []); // Reserve space at the very top of the page for the admin bar so it // doesn't sit on top of the fixed site header — see .site-header{top: // var(--admin-bar-h)} in navigation.css. React.useEffect(() => { document.documentElement.style.setProperty('--admin-bar-h', isAdmin ? '44px' : '0px'); return () => document.documentElement.style.setProperty('--admin-bar-h', '0px'); }, [isAdmin]); const ov = React.useCallback( (key, fallback) => (overrides && overrides[key] && overrides[key][apiLang]) || fallback, [overrides, apiLang] ); const value = React.useMemo( () => ({ isAdmin, editMode, setEditMode, lang: apiLang, refresh: onSaved, ov, t }), [isAdmin, editMode, apiLang, onSaved, ov, t] ); return {children}; } function AdminEditToolbar({ t }) { const { isAdmin, editMode, setEditMode } = React.useContext(AdminEditContext); const [loggingOut, setLoggingOut] = React.useState(false); if (!isAdmin) return null; const logout = () => { setLoggingOut(true); fetch('/panel/logout.php').finally(() => window.location.reload()); }; return (
{t.adminLoggedIn} {t.adminPanelLink}
); } // Wraps a single piece of text (already resolved via CZ_MERGE_CONTENT, so // it shows the real current value — override if one is saved, otherwise // the copy.js default) and makes it click-to-edit when edit mode is on. // `multiline` allows Enter to insert a line break instead of saving. function EditableText({ fieldKey, tag, className, multiline, onSave, children }) { const { isAdmin, editMode, lang, refresh, t } = React.useContext(AdminEditContext); const ref = React.useRef(null); const initialText = React.useRef(String(children ?? '')); React.useEffect(() => { initialText.current = String(children ?? ''); }, [children]); const Tag = tag || 'span'; if (!isAdmin || !editMode) { return {children}; } const commit = () => { const el = ref.current; if (!el) return; const newText = el.innerText.replace(/\n+$/, ''); if (newText === initialText.current) return; initialText.current = newText; if (onSave) { onSave(newText); return; } fetch('/api/config-api.php/content', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fields: [{ key: fieldKey, [lang]: newText }] }), }).then(() => { if (refresh) refresh(); }).catch(() => {}); }; const onKeyDown = (e) => { if (e.key === 'Enter' && !multiline) { e.preventDefault(); e.target.blur(); } if (e.key === 'Escape') { e.target.innerText = initialText.current; e.target.blur(); } }; return ( { e.preventDefault(); e.stopPropagation(); }} title={t.adminEditHint} > {children} ); } function ImageEditPencil({ slot, label }) { const { isAdmin, editMode, refresh, t } = React.useContext(AdminEditContext); const inputRef = React.useRef(null); const [uploading, setUploading] = React.useState(false); if (!isAdmin || !editMode) return null; const upload = async (file) => { if (!file) return; setUploading(true); const formData = new FormData(); formData.append('image', file); formData.append('slot', slot); try { const res = await fetch('/api/config-api.php/upload-content-image', { method: 'POST', body: formData }); if (res.ok && refresh) refresh(); } catch (e) { /* silent — admin can retry */ } setUploading(false); }; return ( <> upload(e.target.files[0])} /> ); } Object.assign(window, { AdminEditProvider, AdminEditToolbar, EditableText, ImageEditPencil, AdminEditContext, contentApiLang });