// data.jsx — integração com Supabase + APIs públicas const C = React.createElement; const SUPABASE_URL = "https://cfmtprdrwjicrduiqwrk.supabase.co"; const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNmbXRwcmRyd2ppY3JkdWlxd3JrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg0MzMwNTYsImV4cCI6MjA5NDAwOTA1Nn0.wSugGuhpM7YvCSvF4juS9lWRl5i2Yt4UNuS3bxBIJf8"; const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); const MESES = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez']; // ─── Fuso do site ─────────────────────────────────────────── // As datas vêm do banco como timestamptz (instante, em UTC). Lê-las com // getHours() e afins mostraria a hora no fuso de quem está visitando, então // um evento marcado pra 20h30 apareceria em outra hora pra quem acessa de // fora do Brasil. Aqui tudo é convertido pra horário de Brasília, que é o // horário em que as coisas de fato acontecem — e o mesmo que o admin digita. const TZ_SITE = 'America/Sao_Paulo'; // Offset do fuso do site num dado instante, em minutos (-180 no horário padrão). function offsetTZ(date) { const p = {}; new Intl.DateTimeFormat('en-US', { timeZone: TZ_SITE, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).formatToParts(date).forEach(x => { p[x.type] = x.value; }); const comoUTC = Date.UTC(+p.year, p.month - 1, +p.day, p.hour === '24' ? 0 : +p.hour, +p.minute, +p.second); return (comoUTC - Math.floor(date.getTime() / 1000) * 1000) / 60000; } // Devolve um Date cujos campos *UTC* são a hora de parede em Brasília, pra // poder usar getUTCHours() e companhia sem depender do fuso do navegador. function emBrasilia(iso) { if (!iso) return null; // Coluna 'date' pura (10 chars) não tem fuso: ancora no meio-dia de Brasília // pra não escorregar de dia na conversão. const d = new Date(iso.length === 10 ? iso + 'T12:00:00-03:00' : iso); if (isNaN(d.getTime())) return null; return new Date(d.getTime() + offsetTZ(d) * 60000); } function fmtDate(iso) { const d = emBrasilia(iso); if (!d) return ''; return `${String(d.getUTCDate()).padStart(2,'0')} ${MESES[d.getUTCMonth()]} ${d.getUTCFullYear()}`; } function fmtTime(iso) { const d = emBrasilia(iso); if (!d) return ''; return `${String(d.getUTCHours()).padStart(2,'0')}h${String(d.getUTCMinutes()).padStart(2,'0')}`; } function fmtDataCurta(iso) { const d = emBrasilia(iso); if (!d) return ''; return `${String(d.getUTCDate()).padStart(2,'0')}/${String(d.getUTCMonth()+1).padStart(2,'0')}`; } function fmtPct(v, casas = 2) { if (v == null || isNaN(v)) return '—'; return Number(v).toLocaleString('pt-BR', { minimumFractionDigits: casas, maximumFractionDigits: casas }) + '%'; } function fmtMoney(v, moeda = 'R$') { if (v == null || isNaN(v)) return '—'; return moeda + ' ' + Number(v).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function fmtIndice(v) { if (v == null || isNaN(v)) return '—'; return Number(v).toLocaleString('pt-BR', { maximumFractionDigits: 0 }); } function tempoLeitura(texto) { if (!texto) return 1; const palavras = texto.trim().split(/\s+/).length; return Math.max(1, Math.round(palavras / 200)); } // ─── Verifica se duas datas ISO caem no mesmo dia (horário de Brasília) ─── function mesmoDia(iso1, iso2) { const d1 = emBrasilia(iso1); const d2 = emBrasilia(iso2); if (!d1 || !d2) return false; return d1.toISOString().slice(0, 10) === d2.toISOString().slice(0, 10); } // ═══════════════════════════════════════════════════════════ // PDF EMBED // ═══════════════════════════════════════════════════════════ function pdfEmbedUrl(url) { if (!url) return null; // ─── Google Drive ──────────────────────────────────────── const gdMatch = url.match(/drive\.google\.com\/file\/d\/([^\/]+)/); if (gdMatch) return `https://drive.google.com/file/d/${gdMatch[1]}/preview`; // ─── OneDrive pessoal (onedrive.live.com) ────────────── if (url.includes('onedrive.live.com/?')) { return url.replace('onedrive.live.com/?', 'onedrive.live.com/embed?'); } if (url.includes('onedrive.live.com/embed')) return url; // ─── OneDrive shortlink (1drv.ms) ──────────────────────── if (url.includes('1drv.ms/')) { const sep = url.includes('?') ? '&' : '?'; return url + sep + 'embed=1'; } // ─── SharePoint corporativo (xxx-my.sharepoint.com) ───── // O SharePoint da FACAMP bloqueia embed externo por política do // tenant Microsoft 365. Retornar null faz o site pular o iframe // e mostrar direto os botões "Abrir documento" + "Baixar PDF". if (url.includes('sharepoint.com')) { return null; } // ─── Fallback: URL direta (ex: link .pdf) ──────────────── return url; } const driveEmbedUrl = pdfEmbedUrl; // ═══════════════════════════════════════════════════════════ // INDICADORES // ═══════════════════════════════════════════════════════════ const CACHE_KEY = 'caef_indicadores_v6'; const CACHE_TTL = 60 * 60 * 1000; async function bcbSerie(codigo, n = 1) { const url = `https://api.bcb.gov.br/dados/serie/bcdata.sgs.${codigo}/dados/ultimos/${n}?formato=json`; const r = await fetch(url); if (!r.ok) throw new Error('BCB ' + codigo + ' falhou: ' + r.status); const arr = await r.json(); return arr.map(x => ({ data: x.data, valor: parseFloat(x.valor) })); } function fetchTimeout(url, ms = 8000) { return Promise.race([ fetch(url), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms)) ]); } async function carregarIbovespa() { try { const r = await fetchTimeout('https://brapi.dev/api/quote/%5EBVSP', 6000); if (!r.ok) return null; const j = await r.json(); const result = j?.results?.[0]; if (!result || !result.regularMarketPrice) return null; const valor = Number(result.regularMarketPrice); if (valor < 50000) return null; return { valor: valor, variacao: Number(result.regularMarketChangePercent), fonte: 'B3 · hoje' }; } catch (err) { console.warn('[CAEF] brapi.dev falhou:', err.message); return null; } } async function carregarIndicadores() { try { const cached = JSON.parse(localStorage.getItem(CACHE_KEY) || 'null'); // Só usa cache se tiver dados de verdade (não array vazio de falha anterior) if (cached && Array.isArray(cached.dados) && cached.dados.length > 0 && Date.now() - cached.t < CACHE_TTL) { console.log('[CAEF] usando cache de indicadores (', cached.dados.length, 'itens)'); return cached.dados; } } catch (_) {} console.log('[CAEF] buscando indicadores das APIs…'); const resultados = await Promise.allSettled([ bcbSerie(432, 1), bcbSerie(433, 12), bcbSerie(24369, 1), fetchTimeout('https://economia.awesomeapi.com.br/json/last/USD-BRL,EUR-BRL', 6000) .then(r => r.ok ? r.json() : null).catch(() => null), carregarIbovespa(), ]); const [selic, ipca12, desemprego, awesome, ibov] = resultados.map( r => r.status === 'fulfilled' ? r.value : null ); let ipcaAcum = null; if (ipca12 && ipca12.length >= 12) { ipcaAcum = ipca12.reduce((acc, m) => acc * (1 + m.valor / 100), 1); ipcaAcum = (ipcaAcum - 1) * 100; } const usdBrl = awesome?.USDBRL; const eurBrl = awesome?.EURBRL; const dolar = usdBrl ? parseFloat(usdBrl.bid) : null; const dolarVar = usdBrl ? parseFloat(usdBrl.pctChange) : null; const euro = eurBrl ? parseFloat(eurBrl.bid) : null; const euroVar = eurBrl ? parseFloat(eurBrl.pctChange) : null; const dados = [ selic?.[0] && { name: 'SELIC', val: fmtPct(selic[0].valor, 2), chg: '—', up: false, period: 'meta a.a.', src: 'BCB · ' + selic[0].data }, ipcaAcum != null && { name: 'IPCA', val: fmtPct(ipcaAcum, 2), chg: '12m', up: ipcaAcum > 0, period: '12 meses', src: 'IBGE/BCB · ' + (ipca12[ipca12.length-1]?.data || '') }, dolar != null && { name: 'Dólar', val: fmtMoney(dolar), chg: dolarVar != null ? (dolarVar > 0 ? '+' : '') + dolarVar.toFixed(2) + '%' : '—', up: (dolarVar || 0) > 0, period: 'PTAX', src: 'AwesomeAPI · hoje' }, euro != null && { name: 'Euro', val: fmtMoney(euro), chg: euroVar != null ? (euroVar > 0 ? '+' : '') + euroVar.toFixed(2) + '%' : '—', up: (euroVar || 0) > 0, period: 'EUR/BRL', src: 'AwesomeAPI · hoje' }, ibov && { name: 'Ibovespa', val: fmtIndice(ibov.valor), chg: ibov.variacao != null ? (ibov.variacao > 0 ? '+' : '') + ibov.variacao.toFixed(2) + '%' : '—', up: (ibov.variacao || 0) > 0, period: 'pontos', src: ibov.fonte }, desemprego?.[0] && { name: 'Desemprego', val: fmtPct(desemprego[0].valor, 1), chg: 'PNADC', up: false, period: 'trimestre', src: 'IBGE · ' + desemprego[0].data }, ].filter(Boolean); console.log('%c[CAEF Indicadores]', 'color: #14123a; font-weight: bold', `${dados.length}/6 carregados:`, dados.map(d => d.name).join(', ') || 'NENHUM' ); // IMPORTANTE: só salva cache se tem pelo menos 1 indicador de verdade // Assim se todas as APIs falharem numa visita, a próxima tenta de novo if (dados.length > 0) { try { localStorage.setItem(CACHE_KEY, JSON.stringify({ t: Date.now(), dados })); } catch (_) {} } else { console.warn('[CAEF] Nenhum indicador carregado — NÃO gravando cache pra tentar de novo na próxima visita'); } return dados; } async function carregarStats() { try { const [producoes, noticias, newsletters, eventosPassados] = await Promise.all([ supabase.from('producoes').select('*', { count: 'exact', head: true }).eq('publicado', true), supabase.from('noticias').select('*', { count: 'exact', head: true }).neq('categoria', 'newsletter'), supabase.from('noticias').select('*', { count: 'exact', head: true }).eq('categoria', 'newsletter'), supabase.from('evento').select('*', { count: 'exact', head: true }).lt('data_inicio', new Date().toISOString()), ]); return { publicacoes: (producoes.count || 0) + (noticias.count || 0), newsletters: newsletters.count || 0, eventos: eventosPassados.count || 0, fundacao: 2008, }; } catch (err) { console.warn('[CAEF] carregarStats falhou', err); return { publicacoes: 0, newsletters: 0, eventos: 0, fundacao: 2008 }; } } function labelTipoEvento(t) { const m = { evento: 'Evento', palestra: 'Palestra', workshop: 'Workshop', monitoria: 'Monitorias', roda_conversa: 'Roda de Conversa' }; return m[t] || 'Evento'; } async function loadCAEFData() { const [prodRes, evRes, notRes, memRes, indicadores, stats] = await Promise.all([ supabase.from('producoes') .select('*, autores:autores_producao(nome, ordem)') .eq('publicado', true) .order('criado_em', { ascending: false }), supabase.from('evento') .select('*').eq('ativo', true) .order('data_inicio', { ascending: true }), supabase.from('noticias') .select('*').order('data_publi', { ascending: false }), supabase.from('membros') .select('*').eq('ativo', true) .order('posicao', { ascending: true }), carregarIndicadores().catch(err => { console.warn('[CAEF] indicadores falharam', err); return []; }), carregarStats(), ]); [prodRes, evRes, notRes, memRes].forEach((r, i) => { if (r.error) console.error('[CAEF] erro', i, r.error); }); const productions = (prodRes.data || []).map(p => ({ id: 'p_' + p.id_producao, kind: 'producao', type: tipoProducaoLabel(p.tipo), date: `${p.ano}${p.semestre ? '.' + p.semestre : ''}`, dateISO: p.criado_em || `${p.ano}-01-01`, title: p.titulo, desc: p.resumo || '', tags: p.palavras_chave || [], image: null, content: p.resumo || '', pdf: p.pdf_url, link: p.link_externo, area: p.area, orientador: p.orientador, coorientador: p.coorientador, autores: (p.autores || []).sort((a,b) => a.ordem - b.ordem).map(a => a.nome), ano: p.ano, semestre: p.semestre, notaMencao: p.nota_mencao, destaque: p.destaque, })); const news = (notRes.data || []) .filter(n => n.categoria !== 'newsletter') .map(n => ({ id: 'n_' + n.id_not, kind: 'noticia', type: categoriaLabel(n.categoria), categoria: n.categoria, date: fmtDate(n.data_publi), dateISO: n.data_publi, title: n.titulo, desc: trecho(n.conteudo, 180), image: n.imagem_url, content: n.conteudo, tags: [], readMinutes: tempoLeitura(n.conteudo), pdfUrl: n.pdf_url, pdfEmbedUrl: pdfEmbedUrl(n.pdf_url), galeriaFotos: n.galeria_fotos || null, })); // Newsletters ordenadas por data_publi DESC (mais recente primeiro). // Numeração deve ser cronológica: 1ª publicada = №1, última = №N. // Como estão em ordem decrescente, num = total - i. const totalNewsletters = (notRes.data || []) .filter(n => n.categoria === 'newsletter').length; const issues = (notRes.data || []) .filter(n => n.categoria === 'newsletter') .map((n, i) => ({ id: 'nl_' + n.id_not, kind: 'newsletter', num: String(totalNewsletters - i), date: fmtDate(n.data_publi), title: n.titulo, topics: trecho(n.conteudo, 80), read: tempoLeitura(n.conteudo), readMinutes: tempoLeitura(n.conteudo), image: n.imagem_url, content: n.conteudo, type: 'Newsletter', categoria: 'newsletter', pdfUrl: n.pdf_url, pdfEmbedUrl: pdfEmbedUrl(n.pdf_url), })); // ─── EVENTOS ──────────────────────────────────────── const events = (evRes.data || []).map(e => { const dInicio = emBrasilia(e.data_inicio) || new Date(NaN); const dFim = e.data_fim ? emBrasilia(e.data_fim) : null; const mesmoDiaFlag = dFim ? mesmoDia(e.data_inicio, e.data_fim) : false; return { id: 'e_' + e.id_evento, kind: 'evento', day: String(dInicio.getUTCDate()).padStart(2,'0'), mon: MESES[dInicio.getUTCMonth()], year: String(dInicio.getUTCFullYear()), time: fmtTime(e.data_inicio), loc: e.local || '', title: e.titulo, desc: e.descricao || '', tipo: e.tipo || 'evento', cat: labelTipoEvento(e.tipo || 'evento'), image: e.imagem_url, inscricao: e.link_inscricao, vagas: e.vagas_total, esgotado: !!e.esgotado, inscricoesAbertas: e.inscricoes_abertas !== false, // NOVO — default true // NOVOS CAMPOS de data pra usar na página dataInicioISO: e.data_inicio, dataFimISO: e.data_fim, horaInicio: fmtTime(e.data_inicio), horaFim: e.data_fim ? fmtTime(e.data_fim) : null, dataInicioCurta: fmtDataCurta(e.data_inicio), dataFimCurta: e.data_fim ? fmtDataCurta(e.data_fim) : null, temFim: !!e.data_fim, mesmoDia: mesmoDiaFlag, }; }); const members = (memRes.data || []).map(m => ({ name: m.nome, role: m.cargo, init: m.inicial, photo: m.foto_url, email: m.email || null, linkedin: m.linkedin || null, })); const destaquesRecentes = [...news, ...productions] .sort((a, b) => new Date(b.dateISO || 0).getTime() - new Date(a.dateISO || 0).getTime()) .slice(0, 6); window.CAEF_DATA = { productions, news, destaquesRecentes, events, indicators: indicadores, issues, members, activities: [], }; window.CAEF_STATS = stats; window.dispatchEvent(new CustomEvent('caef:data-ready')); } function tipoProducaoLabel(t) { const m = { tcc: 'TCC', iniciacao_cientifica: 'Iniciação Científica', monografia: 'Monografia', artigo: 'Artigo Científico', dissertacao: 'Dissertação', extensao_academica: 'Extensão Acadêmica', }; return m[t] || t; } function categoriaLabel(c) { const m = { artigo: 'Coluna de Opinião', blog: 'Blog', resenha: 'Resenha', noticia: 'Notícia', cobertura_evento: 'Cobertura de Evento' }; return m[c] || (c ? c[0].toUpperCase() + c.slice(1) : 'Publicação'); } function trecho(s, n) { if (!s) return ''; const limpo = s.replace(/\s+/g, ' ').trim(); return limpo.length > n ? limpo.slice(0, n).trim() + '…' : limpo; } window.CAEF_DATA = { productions: [], news: [], destaquesRecentes: [], events: [], indicators: [], issues: [], members: [], activities: [] }; window.CAEF_STATS = { publicacoes: 0, newsletters: 0, eventos: 0, fundacao: 2008 }; loadCAEFData(); // ═══════════════════════════════════════════════════════════ // UTILS // ═══════════════════════════════════════════════════════════ function ArrowRight({ size = 14 }) { return ( ); } function Sparkline({ data, color = "var(--ink-3)", up = true }) { const w = 200, h = 60, pad = 4; const min = Math.min(...data), max = Math.max(...data); const span = max - min || 1; const pts = data.map((v, i) => { const x = pad + (i * (w - pad * 2)) / (data.length - 1); const y = h - pad - ((v - min) / span) * (h - pad * 2); return [x, y]; }); const d = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' '); const area = `${d} L${pts[pts.length-1][0]},${h} L${pts[0][0]},${h} Z`; const stroke = up ? 'var(--up)' : 'var(--down)'; return ( ); } function genSpark(seed, trend = 1) { const arr = []; let v = 50 + (seed * 7) % 30; for (let i = 0; i < 24; i++) { v += (Math.sin(i * 0.7 + seed) * 4) + (Math.random() - 0.5 + trend * 0.15) * 6; arr.push(v); } return arr; } function useFadeIn() { React.useEffect(() => { const els = document.querySelectorAll('.fadein:not(.visible)'); const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); els.forEach(el => io.observe(el)); return () => io.disconnect(); }); } function useCountUp(target, dur = 1200, decimals = 0) { const [v, setV] = React.useState(0); const ref = React.useRef(null); React.useEffect(() => { if (!ref.current) return; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { const t0 = performance.now(); const tick = (t) => { const p = Math.min(1, (t - t0) / dur); const eased = 1 - Math.pow(1 - p, 3); setV(target * eased); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); io.disconnect(); } }, { threshold: 0.3 }); io.observe(ref.current); return () => io.disconnect(); }, [target, dur]); return [v, ref, decimals]; } function CountStat({ to, decimals = 0, suffix = '', prefix = '' }) { const [v, ref] = useCountUp(to, 1400, decimals); return ( {prefix}{v.toFixed(decimals)}{suffix} ); } Object.assign(window, { ArrowRight, Sparkline, genSpark, useFadeIn, CountStat });