// ============================================================ // DATA LAYER — Real backend (PHP) // Mantém EXATAMENTE a mesma interface api.* do protótipo, // porém agora cada método é uma chamada fetch() para /api/. // ============================================================ // Caminho da API relativo à pasta do blog. Funciona em qualquer // domínio (drangelo.com.br/blog/ → drangelo.com.br/blog/api/). const API_BASE = 'api'; // ---------- HTTP helper ---------- async function http(path, opts = {}) { const url = `${API_BASE}/${path}`; const res = await fetch(url, { credentials: 'same-origin', headers: { 'Accept': 'application/json' }, ...opts, }); const text = await res.text(); let payload = null; try { payload = text ? JSON.parse(text) : null; } catch (e) { payload = { error: text }; } if (!res.ok) { const msg = (payload && (payload.error || payload.message)) || `Erro HTTP ${res.status}`; throw new Error(msg); } return payload; } function httpJSON(path, method, body) { return http(path, { method, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(body || {}), }); } // ---------- Slug & utils (clientside) ---------- function slugify(s) { return (s || '') .toLowerCase() .normalize('NFD').replace(/[̀-ͯ]/g, '') .replace(/[^a-z0-9\s-]/g, '') .trim().replace(/\s+/g, '-').slice(0, 80); } function uid() { return 'a' + Math.random().toString(36).slice(2, 10); } // ---------- Cache de categorias (evita chamadas repetidas) ---------- let _catCache = null; async function ensureCategories() { if (_catCache) return _catCache; _catCache = await http('categories.php'); return _catCache; } function invalidateCats() { _catCache = null; } // ---------- Public API (mesma assinatura do protótipo) ---------- const api = { async listArticles({ status, category, q } = {}) { const params = new URLSearchParams(); if (status) params.set('status', status); if (category) params.set('category', category); if (q) params.set('q', q); const qs = params.toString(); return http('articles.php' + (qs ? '?' + qs : '')); }, async getArticle(slugOrId) { try { return await http('articles.php?id=' + encodeURIComponent(slugOrId)); } catch (e) { // 404 → retorna null (compatível com a expectativa do article.jsx) return null; } }, async upsertArticle(payload) { return httpJSON('articles.php', 'POST', payload); }, async deleteArticle(id) { await http('articles.php?id=' + encodeURIComponent(id), { method: 'DELETE' }); return true; }, async listCategories() { const cats = await ensureCategories(); _catSyncCache = cats; return cats; }, async saveCategories(cats) { const saved = await httpJSON('categories.php', 'POST', { categories: cats }); _catCache = saved; _catSyncCache = saved; return saved; }, async subscribe(email) { await httpJSON('subscribe.php', 'POST', { email }); return true; }, // -------- Upload de imagem (capa ou corpo do artigo) -------- async uploadImage(file) { const fd = new FormData(); fd.append('file', file); const res = await fetch(`${API_BASE}/upload.php`, { method: 'POST', credentials: 'same-origin', body: fd, }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'Falha no upload da imagem.'); return data.url; }, // -------- Auth (senha única) -------- async login(password) { const session = await httpJSON('auth.php?action=login', 'POST', { password }); try { localStorage.setItem('angelo_session_hint', JSON.stringify(session)); } catch (e) {} return session; }, getSession() { // Hint local só para a UI renderizar sem flicker. A validação real // é o cookie de sessão do PHP (verificado em toda chamada protegida). try { const s = JSON.parse(localStorage.getItem('angelo_session_hint') || 'null'); if (s && s.exp > Date.now()) return s; } catch (e) {} return null; }, async logout() { try { await httpJSON('auth.php?action=logout', 'POST', {}); } catch (e) {} localStorage.removeItem('angelo_session_hint'); }, // -------- Export / Import (backup completo em JSON) -------- async exportData() { const data = await http('export.php'); return JSON.stringify(data, null, 2); }, async importData(json, { merge = true } = {}) { const incoming = typeof json === 'string' ? JSON.parse(json) : json; await httpJSON('import.php' + (merge ? '?merge=1' : ''), 'POST', incoming); invalidateCats(); return true; }, // utility slugify, uid, }; // ---------- Formatting helpers (idem ao protótipo) ---------- function readingTime(html) { const text = (html || '').replace(/<[^>]+>/g, ' '); const words = text.split(/\s+/).filter(Boolean).length; return Math.max(1, Math.round(words / 220)); } function fmtDate(iso, opts = {}) { if (!iso) return '—'; const d = new Date(iso); const months = ['jan.', 'fev.', 'mar.', 'abr.', 'mai.', 'jun.', 'jul.', 'ago.', 'set.', 'out.', 'nov.', 'dez.']; const monthsFull = ['janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho', 'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro']; if (opts.long) return `${d.getDate()} de ${monthsFull[d.getMonth()]} de ${d.getFullYear()}`; return `${d.getDate().toString().padStart(2,'0')} ${months[d.getMonth()]} ${d.getFullYear()}`; } function fmtDateLong(iso) { if (!iso) return '—'; const d = new Date(iso); const dias = ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado']; return `${dias[d.getDay()]}, ${fmtDate(iso, { long: true })}`; } // Cache síncrono de categorias para categoryName/categoryById // (mantém compatibilidade com home.jsx/article.jsx/ui.jsx que chamam essas // funções de forma síncrona dentro do render do React). let _catSyncCache = []; ensureCategories().then(c => { _catSyncCache = c; }).catch(() => {}); function categoryName(id) { const c = _catSyncCache.find(c => c.id === id); return c ? c.name : id; } function categoryById(id) { return _catSyncCache.find(c => c.id === id); } // Export para window (mantém compatibilidade com o restante do prototype) Object.assign(window, { api, readingTime, fmtDate, fmtDateLong, categoryName, categoryById, slugify });