/* ============================================================ App shell — Router, AuthStore, layout primitives Globals: useHashRoute, useAuth, NavBar, TopBar, Footer, Page ============================================================ */ const { useState, useEffect, useMemo, useRef, useCallback, createContext, useContext } = React; /* ---------------- Hash router ---------------- */ function parseHash() { const raw = (location.hash || '#/').replace(/^#/, ''); const [path, query] = raw.split('?'); const params = {}; if (query) { query.split('&').forEach(kv => { const [k, v] = kv.split('='); params[decodeURIComponent(k)] = v == null ? '' : decodeURIComponent(v); }); } return { path: path || '/', params }; } function useHashRoute() { const [route, setRoute] = useState(parseHash()); useEffect(() => { const on = () => setRoute(parseHash()); window.addEventListener('hashchange', on); return () => window.removeEventListener('hashchange', on); }, []); return route; } function nav(path) { if (path.startsWith('#')) { location.hash = path; } else { location.hash = '#' + path; } } function Link({ to, children, className, onClick, ...rest }) { return ( { if (onClick) onClick(e); }} {...rest} > {children} ); } /* ---------------- Auth store ---------------- */ const AuthCtx = createContext(null); function useAuthStore() { const [users, setUsers] = useState(() => { try { return JSON.parse(localStorage.getItem('ibd:users') || '[]'); } catch { return []; } }); const [session, setSession] = useState(() => { try { return JSON.parse(localStorage.getItem('ibd:session') || 'null'); } catch { return null; } }); const persistUsers = (next) => { setUsers(next); localStorage.setItem('ibd:users', JSON.stringify(next)); }; const persistSession = (s) => { setSession(s); if (s) localStorage.setItem('ibd:session', JSON.stringify(s)); else localStorage.removeItem('ibd:session'); }; const signup = (data) => { const exists = users.find(u => u.email.toLowerCase() === data.email.toLowerCase()); if (exists) return { ok: false, error: 'Já existe uma conta com este e-mail.' }; const user = { id: 'u_' + Date.now().toString(36), name: data.name, email: data.email, phone: data.phone, createdAt: new Date().toISOString(), progress: {}, bookmarks: [] }; persistUsers([...users, user]); persistSession({ userId: user.id, since: Date.now() }); return { ok: true, user }; }; const login = (email, _password) => { const user = users.find(u => u.email.toLowerCase() === email.toLowerCase()); if (!user) return { ok: false, error: 'Não encontramos uma conta com este e-mail.' }; persistSession({ userId: user.id, since: Date.now() }); return { ok: true, user }; }; const logout = () => persistSession(null); const currentUser = useMemo(() => { if (!session) return null; return users.find(u => u.id === session.userId) || null; }, [session, users]); const updateUser = (patch) => { if (!currentUser) return; const next = users.map(u => u.id === currentUser.id ? { ...u, ...patch } : u); persistUsers(next); }; return { users, currentUser, signup, login, logout, updateUser }; } function AuthProvider({ children }) { const store = useAuthStore(); return {children}; } function useAuth() { return useContext(AuthCtx); } /* ---------------- Brand mark ---------------- */ function BrandMark({ variant = 'navbar' }) { if (variant === 'large') { return ( Instituto Brasileiro de Derivativos ); } if (variant === 'large-light') { return ( Instituto Brasileiro de Derivativos ); } // navbar — mark + lockup text alongside return (
Instituto Brasileiro de Derivativos
); } /* ---------------- TopBar ---------------- */ function TopBar() { return (
IBD · Fundação Educativa · CNPJ em constituição
Sobre Imprensa Pesquisa · PT-BR
); } /* ---------------- NavBar + submenus + gating + placeholder ---------------- */ // Tabela de menus. children[].premium=true marca itens exclusivos a membros — clique // quando deslogado desvia para /login?next=. const NAV_ITEMS = [ { to: '/biblioteca', label: 'Biblioteca', children: [ { to: '/biblioteca', label: 'Visão geral', desc: 'Todo o acervo, sem filtro' }, { to: '/biblioteca?tema=Fundamentos', label: 'Fundamentos' }, { to: '/biblioteca?tema=' + encodeURIComponent('Opções'), label: 'Opções' }, { to: '/biblioteca?tema=' + encodeURIComponent('Câmbio'), label: 'Câmbio' }, { to: '/biblioteca?tema=Juros', label: 'Juros' }, { to: '/biblioteca?tema=' + encodeURIComponent('Agropecuário'), label: 'Agropecuário' }, { to: '/biblioteca?tema=Modelagem', label: 'Quant & modelagem' }, ], }, { to: '/biblioteca?tipo=Aula', label: 'Vídeo-aulas', children: [ { to: '/biblioteca?tipo=Aula', label: 'Todas as aulas', desc: 'Catálogo de vídeos' }, { to: '/aula/intro-derivativos', label: 'Trilha introdutória', desc: 'Fundamentos em 8 aulas' }, { type: 'separator' }, { to: '/membros/cursos', label: 'Cursos completos', desc: 'Em desenvolvimento', premium: true }, ], }, { to: '/ferramentas/black-scholes', label: 'Ferramentas', children: [ { to: '/ferramentas/black-scholes', label: 'Calculadora Black-Scholes', desc: 'Prêmio e gregas de opções europeias' }, { to: '/ferramentas/payoff', label: 'Diagrama de Payoff', desc: 'Estratégias multi-pernas' }, { to: '/ferramentas/futuros', label: 'Simulador de Futuros', desc: 'Mark-to-market e chamadas de margem' }, { type: 'separator' }, { to: '/ferramentas/backtest', label: 'Backtest de Estratégias', desc: 'Em desenvolvimento', premium: true }, { to: '/ferramentas/var', label: 'Calculadora de VaR', desc: 'Em desenvolvimento', premium: true }, ], }, { to: '/post/o-que-sao-derivativos', label: 'Conceitos', children: [ { to: '/post/o-que-sao-derivativos', label: 'O que são derivativos' }, { to: '/biblioteca?nivel=Iniciante', label: 'Para iniciantes', desc: 'Curadoria de leituras' }, { to: '/conceitos/glossario', label: 'Glossário', desc: 'Em desenvolvimento' }, { type: 'separator' }, { to: '/conceitos/quizzes', label: 'Quizzes interativos', desc: 'Em desenvolvimento', premium: true }, ], }, { to: '/sobre', label: 'Sobre o IBD', children: [ { to: '/sobre', label: 'Missão e princípios' }, { to: '/biblioteca?cat=pesquisa', label: 'Pesquisa aplicada' }, { to: '/biblioteca?cat=imprensa', label: 'Imprensa' }, { to: '/cadastro', label: 'Apoiar o Instituto', desc: 'Cadastre-se gratuitamente' }, ], }, ]; function NavBar() { const { currentUser, logout } = useAuth(); return ( ); } function NavMenuItem({ item }) { const { path } = useHashRoute(); const [open, setOpen] = useState(false); const closeTimer = useRef(null); const hasChildren = !!(item.children && item.children.length); const onEnter = () => { if (closeTimer.current) clearTimeout(closeTimer.current); setOpen(true); }; const onLeave = () => { closeTimer.current = setTimeout(() => setOpen(false), 200); }; const baseOf = (p) => (p || '').split('?')[0].split('#')[0]; const currentBase = baseOf(path); const itemBase = baseOf(item.to); const isActive = currentBase === itemBase || (itemBase !== '/' && itemBase !== '' && currentBase.startsWith(itemBase)) || (hasChildren && item.children.some(c => c.to && baseOf(c.to) === currentBase)); const parentLink = ( {item.label} {hasChildren && } ); if (!hasChildren) return parentLink; return (
{parentLink} {open && (
{item.children.map((child, idx) => { if (child.type === 'separator') { return
; } return ( {child.label} {child.premium && Membros} {child.desc && {child.desc}} ); })}
)}
); } // Link que intercepta cliques em destinos marcados como premium. Se o usuário // não estiver logado, desvia para /login?next=. function GatedLink({ to, premium, className, children, onClick, ...rest }) { const auth = useAuth(); const currentUser = auth ? auth.currentUser : null; const handleClick = (e) => { if (premium && !currentUser) { e.preventDefault(); nav('/login?next=' + encodeURIComponent(to)); } if (onClick) onClick(e); }; return ( {children} ); } // Página reutilizável "Em desenvolvimento" — usada por rotas de ferramentas // e conteúdos ainda não construídos. Quando combinada com GatedLink premium, // só usuários logados conseguem chegar aqui. function ComingSoonPage({ title, eta, description, kind }) { return (
Em desenvolvimento

{title}

{description}

{kind && {kind}} {eta && Previsão {eta}} Sem custo
Voltar ao painel
); } /* ---------------- Footer ---------------- */ function Footer() { return ( ); } /* ---------------- Generic page ---------------- */ function Page({ children, fullBleed }) { return ( <>
{children}