/* ===== Admin panel ===== */ const D6 = window.DATA; const API_BASE = 'api'; // Снапшот demo-FAQ снимается синхронно при парсинге файла — до того как bootstrap.php // перезапишет window.DATA через Object.assign и обнулит faq из пустой БД. const _DEMO_FAQ_SNAPSHOT = (window.DATA?.faq || []).map(x => ({...x})); async function apiFetch(path, options={}){ const res = await fetch(`${API_BASE}${path}`, { credentials:'same-origin', ...options, headers: {'Content-Type':'application/json', ...(options.headers||{})}, }); const data = await res.json().catch(()=>({ok:false,error:'Пустой ответ сервера'})); if(!res.ok || data.ok===false) throw new Error(data.error || `HTTP ${res.status}`); return data; } async function uploadAdminFile(file){ if(!file) throw new Error('Файл не выбран'); const form = new FormData(); form.append('file', file); form.append('purpose', 'admin-content'); const res = await fetch(`${API_BASE}/upload.php`, {method:'POST', credentials:'same-origin', body:form}); const data = await res.json().catch(()=>({ok:false,error:'Пустой ответ сервера'})); if(!res.ok || data.ok===false) throw new Error(data.error || `HTTP ${res.status}`); return data.path; } async function refreshBootstrapData(){ const res = await fetch(`${API_BASE}/bootstrap.php`, {credentials:'same-origin'}); const data = await res.json().catch(()=>({ok:false,error:'Пустой ответ сервера'})); if(!res.ok || data.ok===false || !data.data) throw new Error(data.error || `HTTP ${res.status}`); Object.assign(window.DATA, data.data); return data.data; } function downloadAdminExport(format){ const link = document.createElement('a'); link.href = `${API_BASE}/backup.php?format=${encodeURIComponent(format)}`; link.style.display = 'none'; document.body.appendChild(link); link.click(); link.remove(); } function downloadAdminFile(path){ const link = document.createElement('a'); link.href = `${API_BASE}${path}`; link.style.display = 'none'; document.body.appendChild(link); link.click(); link.remove(); } function readFileText(file){ return new Promise((resolve,reject)=>{ if(!file){ reject(new Error('Файл не выбран')); return; } const reader = new FileReader(); reader.onload = ()=>resolve(String(reader.result || '')); reader.onerror = ()=>reject(new Error('Не удалось прочитать файл')); reader.readAsText(file, 'utf-8'); }); } function TinyEditor({ value, onChange, placeholder='', minHeight=320 }){ const textareaRef = useRef(null); const editorRef = useRef(null); const editorIdRef = useRef(`tiny-${Math.random().toString(36).slice(2)}`); const readyRef = useRef(false); const syncingRef = useRef(false); const [ready,setReady] = useState(!!window.tinymce); useEffect(()=>{ if(!window.tinymce || !textareaRef.current){ setReady(false); return; } let removed = false; const existing = window.tinymce.get(editorIdRef.current); if(existing){ try{ existing.remove(); }catch(e){} } const target = textareaRef.current; target.id = editorIdRef.current; window.tinymce.init({ target: textareaRef.current, license_key: 'gpl', menubar: false, branding: false, promotion: false, min_height: minHeight, plugins: 'autolink lists link table code autoresize preview', toolbar: 'undo redo | blocks | bold italic underline | bullist numlist | alignleft aligncenter alignright | link table | removeformat code preview', placeholder, setup(editor){ editorRef.current = editor; editor.on('init', ()=>{ if(removed) return; readyRef.current = true; try{ syncingRef.current = true; editor.setContent(value || ''); }catch(e){ console.error('TinyMCE init error', e); }finally{ syncingRef.current = false; } }); editor.on('remove', ()=>{ readyRef.current = false; if(editorRef.current === editor) editorRef.current = null; }); editor.on('change input undo redo keyup setcontent', ()=>{ if(syncingRef.current || !readyRef.current) return; try{ onChange(editor.getContent({format:'html'})); }catch(e){ console.error('TinyMCE change error', e); } }); }, }).catch(err=>{ console.error('TinyMCE load error', err); setReady(false); }); return ()=>{ removed = true; readyRef.current = false; const editor = editorRef.current; editorRef.current = null; if(editor){ try{ editor.remove(); }catch(e){} } }; },[]); useEffect(()=>{ if(editorRef.current && readyRef.current){ try{ const current = editorRef.current.getContent({format:'html'}); if(current !== (value || '')){ syncingRef.current = true; editorRef.current.setContent(value || ''); syncingRef.current = false; } }catch(e){ console.error('TinyMCE sync error', e); } } },[value]); if(!ready){ return