Meu Feed – 2 Colunas
:root{ --bg:#0b0c10; --card:#111319; --text:#e7e7ea; --muted:#a7a7b3; --line:#232532; --accent:#7aa7ff; --danger:#ff6b6b; } *{ box-sizing:border-box; } body{ margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background:var(--bg); color:var(--text); } header{ padding:16px 14px; border-bottom:1px solid var(--line); background:rgba(0,0,0,.18); position:sticky; top:0; backdrop-filter:saturate(140%) blur(8px); z-index:10; } .wrap{ max-width:1200px; margin:0 auto; display:flex; align-items:center; gap:12px; justify-content:space-between; flex-wrap:wrap; } h1{ margin:0; font-size:16px; font-weight:700; letter-spacing:.2px; } .meta{ display:flex; gap:10px; align-items:center; color:var(--muted); font-size:12px; flex-wrap:wrap; } .pill{ border:1px solid var(--line); background:rgba(255,255,255,.03); padding:6px 10px; border-radius:999px; display:inline-flex; gap:8px; align-items:center; } .dot{ width:8px;height:8px;border-radius:50%; background:var(--muted); display:inline-block; } .dot.ok{ background:#37d67a; } .dot.bad{ background:var(--danger); }main{ max-width:1200px; margin:0 auto; padding:14px; } .grid{ display:grid; grid-template-columns: 1fr 1fr; gap:14px; } @media (max-width: 920px){ .grid{ grid-template-columns:1fr; } } .col{ border:1px solid var(--line); background:var(--card); border-radius:14px; overflow:hidden; min-height:60vh; display:flex; flex-direction:column; } .col header{ position:unset; backdrop-filter:none; background:rgba(255,255,255,.02); border-bottom:1px solid var(--line); padding:12px 12px; } .colTitle{ display:flex; align-items:flex-start; justify-content:space-between; gap:10px; } .colTitle h2{ margin:0; font-size:14px; line-height:1.2; } .colTitle a{ color:var(--accent); font-size:12px; text-decoration:none; border:1px solid rgba(122,167,255,.35); padding:6px 10px; border-radius:999px; } .colTitle a:hover{ background:rgba(122,167,255,.08); } .colSub{ margin-top:6px; color:var(--muted); font-size:12px; display:flex; gap:10px; flex-wrap:wrap; } .list{ padding:10px 10px 12px; display:flex; flex-direction:column; gap:10px; } .item{ border:1px solid var(--line); background:rgba(255,255,255,.02); border-radius:12px; padding:10px 10px; } .item a{ color:var(--text); text-decoration:none; display:block; font-weight:650; line-height:1.25; font-size:13px; } .item a:hover{ color:var(--accent); } .small{ margin-top:6px; font-size:12px; color:var(--muted); line-height:1.35; display:flex; gap:10px; flex-wrap:wrap; } .badge{ border:1px solid var(--line); border-radius:999px; padding:2px 8px; font-size:11px; color:var(--muted); } .empty{ padding:14px; color:var(--muted); font-size:13px; } .error{ color:var(--danger); font-size:12px; } button{ border:1px solid var(--line); background:rgba(255,255,255,.03); color:var(--text); border-radius:999px; padding:8px 12px; cursor:pointer; font-size:12px; } button:hover{ background:rgba(255,255,255,.06); }
// ========================= // CONFIG (leve e simples) // ========================= const FEEDS = [ { name: "Cidade Repórter", url: "https://cidadereporter.com/feed", listEl: document.getElementById("list1"), statusEl: document.getElementById("status1"), dotEl: document.getElementById("dot1"), countEl: document.getElementById("count1") }, { name: "Conexão Juquery", url: "https://conexaojuquery.com.br/feed", listEl: document.getElementById("list2"), statusEl: document.getElementById("status2"), dotEl: document.getElementById("dot2"), countEl: document.getElementById("count2") } ];
// Atualiza a cada 10 min const REFRESH_MS = 10 * 60 * 1000;
// Se CORS bloquear no navegador, você pode usar um proxy SEU bem simples. // Exemplo: // const PROXY = "/rss-proxy?url="; // seu endpoint // A requisição vira: /rss-proxy?url=https%3A%2F%2Fcidadereporter.com%2Ffeed const PROXY = ""; // deixe vazio para tentar direto
// Limite de itens por feed (leve) const MAX_ITEMS = 12;
// Timeout curto (não prender conexão) const TIMEOUT_MS = 8000;
// ========================= // Helpers // ========================= const globalDot = document.getElementById("globalDot"); const globalStatus = document.getElementById("globalStatus"); const lastUpdate = document.getElementById("lastUpdate"); const btnRefresh = document.getElementById("btnRefresh");
function setDot(el, state){ el.classList.remove("ok","bad"); if(state === "ok") el.classList.add("ok"); if(state === "bad") el.classList.add("bad"); }
function fmtDate(d){ const pad = (n)=> String(n).padStart(2,"0"); return `${pad(d.getDate())}/${pad(d.getMonth()+1)} ${pad(d.getHours())}:${pad(d.getMinutes())}`; }
function stripHtml(html){ const div = document.createElement("div"); div.innerHTML = html || ""; return (div.textContent || div.innerText || "").trim(); }
function truncate(str, n){ str = (str || "").trim(); if(str.length controller.abort(), TIMEOUT_MS); try{ const res = await fetch(url, { signal: controller.signal, cache: "no-store" }); clearTimeout(t); if(!res.ok) throw new Error(`HTTP ${res.status}`); return await res.text(); } catch(err){ clearTimeout(t); throw err; } }
// Parser bem leve (RSS/Atom comuns) function parseFeed(xmlText){ const parser = new DOMParser(); const doc = parser.parseFromString(xmlText, "text/xml");
// Alguns erros de parse vêm como if(doc.querySelector("parsererror")){ throw new Error("XML inválido (parsererror)"); }
const isAtom = !!doc.querySelector("feed > entry"); let items = [];
if(isAtom){ const entries = Array.from(doc.querySelectorAll("feed > entry")).slice(0, MAX_ITEMS); items = entries.map(e => { const title = e.querySelector("title")?.textContent?.trim() || "Sem título"; const linkEl = e.querySelector("link[rel='alternate']") || e.querySelector("link"); const link = linkEl?.getAttribute("href") || ""; const published = e.querySelector("published")?.textContent || e.querySelector("updated")?.textContent || ""; const summary = e.querySelector("summary")?.textContent || e.querySelector("content")?.textContent || ""; return { title, link, published, summary }; }); } else { const rssItems = Array.from(doc.querySelectorAll("channel > item")).slice(0, MAX_ITEMS); items = rssItems.map(it => { const title = it.querySelector("title")?.textContent?.trim() || "Sem título"; const link = it.querySelector("link")?.textContent?.trim() || ""; const pubDate = it.querySelector("pubDate")?.textContent?.trim() || ""; // description às vezes vem com HTML pesado; vamos enxugar const desc = it.querySelector("description")?.textContent || ""; return { title, link, published: pubDate, summary: desc }; }); }
return items; }
function renderFeed(feed, items){ feed.countEl.textContent = String(items.length);
if(!items.length){ feed.listEl.innerHTML = `
`; return; }
const html = items.map(item => { const dateLabel = item.published ? new Date(item.published) : null; const dateOk = dateLabel && !isNaN(dateLabel.getTime()); const when = dateOk ? fmtDate(dateLabel) : (item.published ? item.published : ""); const summaryText = truncate(stripHtml(item.summary), 170);
return `
${stripHtml(item.title)}`; }).join("");
feed.listEl.innerHTML = html; }
async function loadOne(feed){ feed.statusEl.textContent = "Carregando…"; setDot(feed.dotEl, "");
const url = PROXY ? (PROXY + encodeURIComponent(feed.url)) : feed.url;
try{ const xmlText = await fetchWithTimeout(url); const items = parseFeed(xmlText); renderFeed(feed, items); feed.statusEl.textContent = "OK"; setDot(feed.dotEl, "ok"); return { ok: true }; } catch(err){ feed.countEl.textContent = "0"; feed.listEl.innerHTML = `
`; feed.statusEl.textContent = "Erro"; setDot(feed.dotEl, "bad"); return { ok: false, error: err }; } }
async function loadAll(){ globalStatus.textContent = "Atualizando…"; setDot(globalDot, "");
const results = await Promise.allSettled(FEEDS.map(loadOne));
const okCount = results.filter(r => r.status === "fulfilled" && r.value?.ok).length; const allOk = okCount === FEEDS.length;
const now = new Date(); lastUpdate.textContent = fmtDate(now);
globalStatus.textContent = allOk ? "Tudo certo" : (okCount > 0 ? "Parcial (alguns feeds falharam)" : "Falha geral");
setDot(globalDot, allOk ? "ok" : "bad"); }
btnRefresh.addEventListener("click", loadAll);
// Primeira carga + ciclo automático loadAll(); setInterval(loadAll, REFRESH_MS);