/* ============================================================
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 (
);
}
if (variant === 'large-light') {
return (
);
}
// navbar — mark + lockup text alongside
return (
Instituto Brasileiro
de Derivativos
);
}
/* ---------------- TopBar ---------------- */
function TopBar() {
return (
IBD · Fundação Educativa · CNPJ em constituição
);
}
/* ---------------- 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 (
{NAV_ITEMS.map(item => (
))}
{currentUser ? (
<>
{currentUser.name.split(' ')[0]}
{ logout(); nav('/'); }}>Sair
>
) : (
<>
Entrar
Criar conta
>
)}
);
}
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}
>
);
}
/* ---------------- Phone mask ---------------- */
function maskPhoneBR(v) {
const d = (v || '').replace(/\D/g, '').slice(0, 11);
if (d.length === 0) return '';
if (d.length <= 2) return '(' + d;
if (d.length <= 7) return '(' + d.slice(0, 2) + ') ' + d.slice(2);
return '(' + d.slice(0, 2) + ') ' + d.slice(2, 7) + '-' + d.slice(7);
}
function isValidPhoneBR(v) {
const d = (v || '').replace(/\D/g, '');
return d.length === 11; // (DD) 9NNNN-NNNN
}
function isValidEmail(v) {
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v || '');
}
function isValidFullName(v) {
const trimmed = (v || '').trim();
if (trimmed.length < 4) return false;
return /\s/.test(trimmed); // at least one space (first + last)
}
/* expose */
Object.assign(window, {
useHashRoute, nav, Link, GatedLink,
AuthProvider, useAuth,
TopBar, NavBar, NavMenuItem, Footer, Page, BrandMark, ComingSoonPage,
maskPhoneBR, isValidPhoneBR, isValidEmail, isValidFullName
});