// ============ AcademIA8 · Consola — componentes UI compartidos ============
const { useState, useRef, useEffect } = React;
// ---------- Contención de errores (CONSOLA-FIX: deuda saldada) ----------
// Un dato inesperado en cualquier vista (ej. checkpoint sin `options`, ver
// commit 7121576) no puede volver a tirar TODO el árbol de React — antes no
// había nada que lo frenara. Envuelve el contenido principal (no el sidebar/
// topbar), así una vista rota deja el resto de la consola operable. Tiene que
// ser clase: los error boundaries de React no existen como hook.
class ErrorBoundary extends React.Component {
constructor(props){
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(){
return { hasError: true };
}
componentDidCatch(error, info){
console.error('[ErrorBoundary] vista rota:', error, info && info.componentStack);
}
render(){
if (this.state.hasError){
return (
Algo falló al mostrar esta vista
Probá recargar la página o volver a la lista de cursos. Si el problema sigue, avisá al equipo técnico.
);
}
return this.props.children;
}
}
// ---------- Iconos (línea, ~1.8px, esquinas redondeadas — estilo de marca) ----------
const PATHS = {
courses:'M4 5.5A1.5 1.5 0 0 1 5.5 4H18a2 2 0 0 1 2 2v13.5M4 5.5V18a2 2 0 0 0 2 2h14M4 5.5A1.5 1.5 0 0 0 4 8.5H18',
layers:'M12 3 2 8l10 5 10-5-10-5ZM2 12l10 5 10-5M2 16l10 5 10-5',
publish:'M12 19V6M5 12l7-7 7 7',
plus:'M12 5v14M5 12h14',
edit:'M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z',
trash:'M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6',
drag:'M9 5h.01M9 12h.01M9 19h.01M15 5h.01M15 12h.01M15 19h.01',
up:'M18 15l-6-6-6 6',
down:'M6 9l6 6 6-6',
check:'M20 6 9 17l-5-5',
x:'M18 6 6 18M6 6l12 12',
text:'M4 6h16M4 12h16M4 18h10',
video:'M15 10l4.5-2.6v9.2L15 14M3 6h12v12H3z',
audio:'M9 18V5l10-2v13M9 18a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM19 16a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z',
image:'M3 5h18v14H3zM3 15l5-5 4 4 3-3 6 6',
question:'M9.1 9a3 3 0 1 1 5 2.2c-.9.8-2.1 1.3-2.1 2.8M12 17h.01',
search:'M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14ZM21 21l-4.3-4.3',
clock:'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 7v5l3 2',
calendar:'M3 5h18v16H3zM3 10h18M8 3v4M16 3v4',
eye:'M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12ZM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z',
alert:'M12 3 2 20h20L12 3ZM12 10v4M12 17h.01',
play:'M6 4l14 8-14 8V4Z',
upload:'M12 15V3M8 7l4-4 4 4M4 15v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4',
refresh:'M4 9a8 8 0 0 1 14-3l2 2M20 15a8 8 0 0 1-14 3l-2-2M18 4v4h-4M6 20v-4h4',
chevright:'M9 6l6 6-6 6',
settings:'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM19.4 13.5a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-2.7-1.1l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0-1.1-2.7H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.1-2.7l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 2.7-1.1V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 2.7 1.1l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.8Z',
send:'M22 2 11 13M22 2l-7 20-4-9-9-4 20-7Z',
copy:'M9 9h11v11H9zM5 15H4V4h11v1',
help:'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM9.6 9a2.4 2.4 0 1 1 3.4 2.2c-.8.4-1 1-1 1.7M12 17h.01',
doc:'M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8ZM14 3v5h5M9 13h6M9 17h4',
chat:'M21 12a8 8 0 0 1-8 8 8.5 8.5 0 0 1-3.6-.8L3 20l1-4.4A8 8 0 1 1 21 12ZM8 11h.01M12 11h.01M16 11h.01',
infinity:'M6 8a4 4 0 1 0 0 8c2 0 3.2-1.5 4-3 1.2-2.3 2.2-5 6-5a4 4 0 0 1 0 8c-3.8 0-4.8-2.7-6-5-.8-1.5-2-3-4-3Z',
sort:'M4 7h11M4 12h7M4 17h4M18 5v14M18 19l-3.5-3.5M18 19l3.5-3.5',
logout:'M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l-5-5 5-5M5 12h11',
sidebar:'M4 4h16v16H4zM9 4v16',
panelclose:'M4 4h16v16H4zM9 4v16M15 9l-2 3 2 3',
panelopen:'M4 4h16v16H4zM9 4v16M13 9l2 3-2 3',
money:'M12 1v22M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6',
};
function Icon({ name, style }){
const d = PATHS[name] || '';
return (
{d.split('M').filter(Boolean).map((seg,i)=> )}
);
}
const FRAG_ICON = { texto:'text', video:'video', audio:'audio', imagen:'image', documento:'doc' };
const FRAG_LABEL = { texto:'Texto', video:'Video', audio:'Audio', imagen:'Imagen', documento:'Documento' };
// ---------- Botón ----------
function Btn({ variant='ghost', size, icon, iconRight, children, ...rest }){
const cls = `btn btn-${variant}${size==='sm'?' btn-sm':''}`;
return (
{icon && }
{children}
{iconRight && }
);
}
function IconBtn({ icon, danger, title, ...rest }){
return ;
}
// ---------- Pill de estado del curso ----------
function StatusPill({ status }){
if (status==='draft-only') return Borrador · sin publicar ;
if (status==='published-dirty') return Cambios sin publicar ;
return Publicado ;
}
// ---------- Toggle global Borrador / Publicado ----------
function ViewToggle({ value, onChange, disabled }){
return (
Estás viendo
onChange('borrador')}>
Borrador
!disabled&&onChange('publicado')} disabled={disabled}>
Publicado
);
}
// ---------- Modal ----------
function Modal({ title, subtitle, icon, danger, wide, onClose, children, foot }){
useEffect(()=>{
const h = (e)=>{ if(e.key==='Escape') onClose(); };
window.addEventListener('keydown', h);
return ()=>window.removeEventListener('keydown', h);
},[]);
return (
{ if(e.target===e.currentTarget) onClose(); }}>
{icon &&
}
{title}
{subtitle &&
{subtitle}
}
{children}
{foot &&
{foot}
}
);
}
// ---------- Field ----------
function Field({ label, optional, hint, children }){
return (
{label &&
{label}{optional && opcional } }
{children}
{hint &&
{hint}
}
);
}
// ---------- Toast ----------
function Toast({ msg }){
return {msg}
;
}
// ---------- Confirmar borrado ----------
function ConfirmDelete({ what, detail, onClose, onConfirm }){
return (
Cancelar Sí, borrar >}>
{detail}
);
}
// ---------- HelpTip: ícono ? + popover con reglas (reutilizable) ----------
function HelpTip({ title, children, align }){
const [open,setOpen]=useState(false);
return (
setOpen(true)} onMouseLeave={()=>setOpen(false)}>
{ e.preventDefault(); e.stopPropagation(); setOpen(o=>!o); }}>
{open && (
e.stopPropagation()}>
{title && {title} }
{children}
)}
);
}
// ---------- Indicador de sesión (usuario logueado vía Cognito) ----------
function SessionMenu({ user, onLogout }){
const [open,setOpen]=useState(false);
const ref=useRef(null);
useEffect(()=>{
if(!open) return;
const h=(e)=>{ if(ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown',h);
return ()=>document.removeEventListener('mousedown',h);
},[open]);
return (
setOpen(o=>!o)} title={user.email}>
{user.initials}
{user.name}
{open && (
{ setOpen(false); onLogout&&onLogout(); }}>
Cerrar sesión
)}
);
}
// ---------- Estado de compresión de video (polling async post-upload) ----------
function CompressionStatusLine({ status, info }){
if (status==='procesando') return (
Comprimiendo video para WhatsApp (hasta 16 MB)…
);
if (status==='listo') return (
Comprimido{info && info.tamano_final_bytes ? ` · ${(info.tamano_final_bytes/1024/1024).toFixed(1)} MB` : ''}
);
if (status==='error') return (
No se pudo comprimir{info && info.motivo ? `: ${info.motivo}` : ''}
);
return null;
}
Object.assign(window, {
Icon, FRAG_ICON, FRAG_LABEL, Btn, IconBtn, StatusPill, ViewToggle, Modal, Field, Toast, ConfirmDelete, HelpTip, SessionMenu, CompressionStatusLine, ErrorBoundary,
useState, useRef, useEffect,
});