/* Blackstock shared primitives */ /* Hook: detecta viewport mobile (≤ 720px) y se actualiza al rotar/redimensionar. * Se usa para decidir si renderizar HomePage (desktop) o HomeMobile. * SSR-safe: durante el primer render server-side devuelve false; en cliente * se sincroniza tras montaje. */ const useIsMobile = (breakpoint = 720) => { const [isMobile, setIsMobile] = React.useState(() => { if (typeof window === 'undefined') return false; return window.matchMedia(`(max-width: ${breakpoint}px)`).matches; }); React.useEffect(() => { const mql = window.matchMedia(`(max-width: ${breakpoint}px)`); const onChange = (e) => setIsMobile(e.matches); setIsMobile(mql.matches); if (mql.addEventListener) mql.addEventListener('change', onChange); else mql.addListener(onChange); // Safari < 14 return () => { if (mql.removeEventListener) mql.removeEventListener('change', onChange); else mql.removeListener(onChange); }; }, [breakpoint]); return isMobile; }; window.useIsMobile = useIsMobile; const LogoMark = ({ size = 22 }) => ( ); const Logo = ({ size = 22 }) => ( e.preventDefault()}> BLACKSTOCK ); const Nav = ({ activePage = 'home', onNavigate = () => {} }) => { const isMobile = useIsMobile(); const items = [ { id: 'home', label: 'Sistema' }, { id: 'product', label: 'Dentro' }, { id: 'anty', label: 'Anty', accent: true }, { id: 'pricing', label: 'Planes' }, { id: 'home', label: 'Casos', anchor: '#casos' }, { id: 'home', label: 'FAQ', anchor: '#faq' }, ]; return ( ); }; /* Bloque del navbar para auth: cuando NO hay sesión muestra "Empezar". * Cuando hay sesión muestra avatar + dropdown (Portal/Academia/Panel/Logout). * Se suscribe al evento 'bs:session-changed' para refrescar al instante. */ const UserAuthZone = ({ onNavigate }) => { const [session, setSession] = React.useState( (typeof window !== 'undefined' && window.bsSession) || { authenticated: false, account: null } ); const [open, setOpen] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { const handler = (e) => setSession(e.detail || { authenticated: false, account: null }); document.addEventListener('bs:session-changed', handler); // Refrescar al montar if (typeof window.bsRefreshSession === 'function') { window.bsRefreshSession().then((s) => setSession(s)).catch(() => {}); } return () => document.removeEventListener('bs:session-changed', handler); }, []); React.useEffect(() => { if (!open) return; const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; const onKey = (e) => { if (e.key === 'Escape') setOpen(false); }; document.addEventListener('mousedown', onClick); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onClick); document.removeEventListener('keydown', onKey); }; }, [open]); if (!session.authenticated) { return ( ); } const account = session.account || {}; const displayName = account.name || account.username || account.email || 'Usuario'; const initial = (displayName.match(/\S/) || ['U'])[0].toUpperCase(); return (
{open && (
{initial}
{displayName}
{account.email ?
{account.email}
: null}
Portal de clientes Panel de operaciones Academia
)}
); }; window.UserAuthZone = UserAuthZone; const Footer = () => ( ); /* Sparkline */ const Sparkline = ({ width = 280, height = 60, points = null, smooth = true, color = 'var(--acid)' }) => { const data = points || [12, 18, 14, 22, 19, 28, 24, 32, 29, 38, 34, 42, 38, 48, 44, 52, 49, 58]; const max = Math.max(...data); const min = Math.min(...data); const range = max - min || 1; const stepX = width / (data.length - 1); const pts = data.map((v, i) => [i * stepX, height - ((v - min) / range) * (height - 8) - 4]); const d = smooth ? pts.reduce((acc, [x, y], i) => { if (i === 0) return `M${x},${y}`; const [px, py] = pts[i - 1]; const cx = (px + x) / 2; return `${acc} Q${px},${py} ${cx},${(py + y) / 2} T${x},${y}`; }, '') : 'M' + pts.map(p => p.join(',')).join(' L'); const area = d + ` L${width},${height} L0,${height} Z`; return ( ); }; /* Metric tile (used in panel mockups) */ const MetricTile = ({ label, value, sub, color = 'fg', size = 'md' }) => (
{label}
{value}
{sub &&
{sub}
}
); /* Panel preview — used in hero A and product page */ const PanelPreview = ({ compact = false }) => { const [tick, setTick] = React.useState(0); React.useEffect(() => { const id = setInterval(() => setTick(t => t + 1), 2200); return () => clearInterval(id); }, []); const ticker = ['Carhartt Detroit M · 92€', 'Acne scarf wool · 78€', 'Birkenstock Boston 42 · 65€', 'Levi\'s 501 MIUSA · 54€']; return (
{/* Inner glow */}
{/* Header */}
HQ DE VENTAS
LIVE
La capa operativa
Pedidos, mensajes, restocks, IA — todo en un panel.
{/* Top metrics */}
{/* Chart */}
FACTURACIÓN HOY
268€
+9% vs ayer
00H06H12H18HNOW
{/* Activity ticker */}
VENDIDO
{ticker[tick % ticker.length]}
hace {Math.floor(Math.random() * 30) + 1}s
); }; /* Floating chip for hero */ const FloatingChip = ({ label, value, sub, x, y, delay = 0 }) => (
{label}
{value} {sub && {sub}}
); Object.assign(window, { LogoMark, Logo, Nav, Footer, Sparkline, MetricTile, PanelPreview, FloatingChip, });