Hola, ¿qué enseñaremos hoy?

Gestor de Aula • EduSpace
`; }function buildRTF(d){ const lines=['{\\rtf1\\ansi\\ansicpg1252\\deff0', '{\\fonttbl{\\f0\\fswiss\\fcharset0 Arial;}{\\f1\\froman\\fcharset0 Georgia;}}', '{\\colortbl;\\red27\\green42\\blue74;\\red58\\green86\\blue160;\\red136\\green150\\blue171;\\red42\\green125\\blue79;\\red255\\green255\\blue255;}', '\\f0\\fs22\\widowctrl\\hyphauto'];const h=(t,sz=28,bold=true,col=1)=> `\\pard\\sb120\\sa60${bold?'\\b':''}\\f1\\fs${sz}\\cf${col} ${rtfEsc(t)}${bold?'\\b0':''} \\f0\\par`; const p=(t,sz=20,col=1)=>`\\pard\\sa60\\fs${sz}\\cf${col} ${rtfEsc(t)}\\par`; const rule='\\pard\\brdrb\\brdrs\\brdrw5\\brdrcf3 \\par';lines.push(h(d.info.name,36,true,1)); lines.push(p(`${d.info.grade||'Sin especificar'} · ${d.occupied}/${d.total} estudiantes · ${d.date}`,18,3)); lines.push(rule); lines.push(h('Configuración del Aula',24,true,2)); lines.push(p(`Filas: ${d.cfg.rows} Columnas: ${d.cfg.cols} Total: ${d.total} Máx. estudiantes: ${d.cfg.maxStu}`,18)); if(d.cfg.groupMode)lines.push(p(`Modo grupos: Sí (${d.cfg.groupSize} filas por grupo)`,18)); lines.push(rule); lines.push(h('Estadísticas',24,true,2)); lines.push(p(`Asientos ocupados: ${d.occupied} Vacíos: ${d.total-d.occupied} Ocupación: ${Math.round((d.occupied/d.total)*100)}%`,18)); lines.push(rule);if(d.namedSeats.length>0){ lines.push(h(`Lista de Estudiantes (${d.namedSeats.length})`,24,true,2)); // table header const cw=[600,3200,1000,3200,2400]; const rowFmt=()=>`\\trowd\\trgaph108\\trleft-108${cw.map((_,i)=>`\\cellx${cw.slice(0,i+1).reduce((a,b)=>a+b,0)}`).join('')}`; lines.push(`${rowFmt()}\\pard\\intbl\\b\\fs18\\cf5\\cb1\\shading5000\\shadingparm 0 0 0 0 255 255 255 N\\cell ${rtfEsc('Nombre')}\\cell ${rtfEsc('Asiento')}\\cell ${rtfEsc('Categorías')}\\cell ${rtfEsc('Notas')}\\cell\\row`); d.namedSeats.forEach((s,i)=>{ const tl=(s.tags||[]).map(t=>CATS.find(c=>c.id===t)?.lbl||'').filter(Boolean).join(', '); lines.push(`${rowFmt()}\\pard\\intbl\\fs18\\cf1 ${i+1}. ${rtfEsc(s.name)}\\cell #${s.id}\\cell ${rtfEsc(tl||'—')}\\cell ${rtfEsc(s.notes||'—')}\\cell\\row`); }); lines.push('\\par'); }lines.push(h('Distribución por asiento',24,true,2)); for(let r=0;r0&&r>0&&r%d.cfg.groupSize===0) lines.push(p(`── Grupo ${Math.floor(r/d.cfg.groupSize)+1} ──`,16,2)); const rowSeats=d.allSeats.slice(r*d.cfg.cols,(r+1)*d.cfg.cols); const row=rowSeats.map(s=>s.name?`[${s.id}:${s.name.split(' ')[0]}]`:`[${s.id}:—]`).join(' '); lines.push(p(`Fila ${r+1}: ${row}`,16)); } lines.push('}'); return lines.join('\n'); }/* ── ROOMS DRAWER ────────────────────────────────────────────────────── */ const EMOJIS=['🏫','📚','🎨','🔬','🎵','🌍','✏️','🧮','💻','🏃','🧪','📐','🌱','🎭','🤸'];function RoomsDrawer({rooms,activeId,onSelect,onNew,onDuplicate,onDelete,onRename,onClose}){ const[newMode,setNewMode]=useState(false); const[newName,setNewName]=useState(''); const[newGrade,setNewGrade]=useState(''); const[newEmoji,setNewEmoji]=useState('🏫'); const[renaming,setRenaming]=useState(null); // {id,name,grade,emoji} const ref=useRef(); useEffect(()=>{if(ref.current)ref.current.focus();},[]);function submitNew(){ if(!newName.trim())return; onNew(newName.trim(),newGrade.trim(),newEmoji); setNewName('');setNewGrade('');setNewEmoji('🏫');setNewMode(false); } function submitRename(){ if(!renaming||!renaming.name.trim())return; onRename(renaming.id,renaming.name.trim(),renaming.grade.trim(),renaming.emoji); setRenaming(null); }return
e.key==='Escape'&&onClose()}> {/* Header */}
Mis Aulas
{rooms.length} aula(s) guardada(s)
{/* New room form */} {newMode&&
Nueva aula
{/* Emoji selector */}
Icono
{EMOJIS.map(e=>)}
setNewName(e.target.value)} onKeyDown={e=>e.key==='Enter'&&submitNew()} autoFocus style={{fontSize:'.8rem',padding:'7px 10px'}}/> setNewGrade(e.target.value)} onKeyDown={e=>e.key==='Enter'&&submitNew()} style={{fontSize:'.8rem',padding:'7px 10px'}}/>
}{/* Room list */}
{rooms.length===0&&
🏫
Sin aulas
Crea tu primera aula con el botón de arriba
} {rooms.map(room=>{ const isActive=room.id===activeId; const occ=room.seats.filter(s=>s.name).length; const tot=room.seats.length; if(renaming?.id===room.id){ return
{EMOJIS.map(e=>)}
setRenaming(r=>({...r,name:e.target.value}))} onKeyDown={e=>e.key==='Enter'&&submitRename()} style={{marginBottom:6,fontSize:'.78rem',padding:'6px 9px'}}/> setRenaming(r=>({...r,grade:e.target.value}))} onKeyDown={e=>e.key==='Enter'&&submitRename()} placeholder="Grado / sección" style={{marginBottom:8,fontSize:'.78rem',padding:'6px 9px'}}/>
; } return
{onSelect(room.id);onClose();}}>
{room.emoji||'🏫'}
{room.info.name}
{room.info.grade||'Sin grado'} · guardado {room.savedAt||'—'}
{occ}/{tot}
{rooms.length>1&&}
; })}
Los cambios se guardan automáticamente.
Usa Duplicar ⎘ para crear una copia de un aula existente.
; }/* ── APP ─────────────────────────────────────────────────────────────── */ function App(){ // Multi-room state const[rooms,setRooms]=useState(()=>initRooms()); const[activeId,setActiveId]=useState(()=>{ const saved=loadActiveId(); const all=initRooms(); return(saved&&all.find(r=>r.id===saved))?saved:all[0].id; }); const[showRooms,setShowRooms]=useState(false); const[unsaved,setUnsaved]=useState(false);// Derive active room const activeRoom=rooms.find(r=>r.id===activeId)||rooms[0];// Local editing state — sourced from active room const[cfg,setCfg] =useState(()=>activeRoom.cfg); const[seats,setSeats] =useState(()=>activeRoom.seats); const[roster,setRoster]=useState(()=>activeRoom.roster); const[info,setInfo] =useState(()=>activeRoom.info);const[selected,setSelected]=useState(null); const[filter,setFilter]=useState(null); const[search,setSearch]=useState(''); const[picking,setPicking]=useState(null); const[toast,setToast] =useState(null); const[showCfg,setShowCfg]=useState(false); const[editName,setEditName]=useState(false);const total=cfg.rows*cfg.cols; const maxStu=cfg.maxStu||total;// Auto-save to rooms whenever local state changes const saveCurrentRoom=useCallback(()=>{ setRooms(prev=>prev.map(r=>r.id===activeId?{...r,cfg,seats,roster,info,savedAt:fmtDate()}:r)); setUnsaved(false); },[activeId,cfg,seats,roster,info]);// Auto-save with debounce const saveTimerRef=useRef(null); useEffect(()=>{ setUnsaved(true); clearTimeout(saveTimerRef.current); saveTimerRef.current=setTimeout(()=>saveCurrentRoom(),1200); return()=>clearTimeout(saveTimerRef.current); },[cfg,seats,roster,info]);// Persist rooms to localStorage whenever they change useEffect(()=>{saveAllRooms(rooms);},[rooms]); useEffect(()=>{saveActiveId(activeId);},[activeId]);// When active room changes, load its data useEffect(()=>{ const r=rooms.find(x=>x.id===activeId)||rooms[0]; if(r){setCfg(r.cfg);setSeats(r.seats);setRoster(r.roster);setInfo(r.info);setFilter(null);setSearch('');setPicking(null);setSelected(null);} },[activeId]);useEffect(()=>{if(toast){const t=setTimeout(()=>setToast(null),2500);return()=>clearTimeout(t);}},[toast]); useEffect(()=>{ const fn=e=>{if(e.key==='Escape'){setPicking(null);setShowCfg(false);setShowRooms(false);}}; window.addEventListener('keydown',fn);return()=>window.removeEventListener('keydown',fn); },[]);const boom=(msg,ico='✓')=>setToast({msg,ico}); const occupied=seats.filter(s=>s.name).length; const tagCounts=CATS.map(cat=>({...cat,count:seats.filter(s=>s.tags?.includes(cat.id)).length})); const sl=search.toLowerCase(); const hl=search?new Set(seats.filter(s=>s.name.toLowerCase().includes(sl)).map(s=>s.id)):new Set();const rowsData=useMemo(()=>{ return Array.from({length:cfg.rows},(_,r)=>({r,seats:seats.slice(r*cfg.cols,(r+1)*cfg.cols)})); },[cfg,seats]);// Multi-room operations function createRoom(name,grade,emoji){ const r=makeDefaultRoom(name,grade); r.emoji=emoji||'🏫'; setRooms(p=>[...p,r]); // Save current first setRooms(prev=>prev.map(x=>x.id===activeId?{...x,cfg,seats,roster,info,savedAt:fmtDate()}:x)); setActiveId(r.id); boom(`Aula "${name}" creada`,'🏫'); } function switchRoom(id){ // Save current before switching setRooms(prev=>prev.map(r=>r.id===activeId?{...r,cfg,seats,roster,info,savedAt:fmtDate()}:r)); setActiveId(id); } function duplicateRoom(id){ const src=rooms.find(r=>r.id===id); if(!src)return; const copy={...JSON.parse(JSON.stringify(src)),id:uid(),info:{...src.info,name:src.info.name+' (copia)'},savedAt:fmtDate()}; setRooms(p=>[...p,copy]); boom(`Aula duplicada`,'⎘'); } function deleteRoom(id){ const remaining=rooms.filter(r=>r.id!==id); if(!remaining.length)return; setRooms(remaining); if(activeId===id)setActiveId(remaining[0].id); } function renameRoom(id,name,grade,emoji){ setRooms(p=>p.map(r=>r.id===id?{...r,info:{...r.info,name,grade},emoji}:r)); if(id===activeId)setInfo(p=>({...p,name,grade})); boom('Aula actualizada','✏️'); } function saveNow(){ clearTimeout(saveTimerRef.current); setRooms(prev=>prev.map(r=>r.id===activeId?{...r,cfg,seats,roster,info,savedAt:fmtDate()}:r)); setUnsaved(false); boom('Aula guardada','💾'); }function applyCfg(nc){ const t=nc.rows*nc.cols; const ns=Array.from({length:t},(_,i)=>seats[i]||{id:i+1,name:'',tags:[],notes:''}).map((s,i)=>({...s,id:i+1})); setCfg(nc);setSeats(ns);setShowCfg(false); boom(`Aula: ${nc.rows}×${nc.cols} = ${t} asientos`,'⚙️'); }function handleSeatClick(seat){ if(picking){ if(seat.name){boom('Asiento ocupado','⚠');return;} if(occupied>=maxStu){boom(`Máx. ${maxStu} estudiantes`,'⚠');return;} setSeats(p=>p.map(s=>s.id===seat.id?{...s,name:picking}:s)); boom(`${picking} → #${seat.id}`,'🎯');setPicking(null); }else setSelected(seat); } function handleSave(u){ if(u.name&&!seats.find(s=>s.id===u.id)?.name&&occupied>=maxStu){boom(`Máx. ${maxStu} est.`,'⚠');return;} setSeats(p=>p.map(s=>s.id===u.id?u:s));setSelected(null);boom(u.name?`${u.name} guardado`:'Actualizado'); } function handleClear(seat){setSeats(p=>p.map(s=>s.id===seat.id?{id:s.id,name:'',tags:[],notes:''}:s));setSelected(null);boom('Asiento liberado','🗑');} function loadNames(names){setRoster(p=>{const ex=new Set(p.map(n=>n.toLowerCase()));const nw=names.filter(n=>!ex.has(n.toLowerCase()));boom(`${nw.length} nombre(s) agregados`,'📋');return[...p,...nw];});} function autoAll(){ const as=new Set(seats.filter(s=>s.name).map(s=>s.name.toLowerCase())); const todo=roster.filter(n=>!as.has(n.toLowerCase())); const slots=maxStu-occupied; const n=Math.min(todo.length,slots,seats.filter(s=>!s.name).length); if(!n){boom('Sin asientos disponibles','⚠');return;} setSeats(p=>{const u=[...p];let ai=0;for(let i=0;i{try{const d=JSON.parse(ev.target.result);if(d.seats){setSeats(d.seats);if(d.info)setInfo(d.info);if(d.roster)setRoster(d.roster);if(d.cfg)setCfg(d.cfg);boom('Importado','📂');}}catch{boom('Error','✗');}}; r.readAsText(file);e.target.value=''; } function resetAll(){if(!confirm('¿Resetear toda el aula?'))return;setSeats(mkSeats(cfg.rows,cfg.cols));boom('Reseteada','🔄');}function getExportData(){ return{ info,cfg, allSeats:seats, namedSeats:seats.filter(s=>s.name).sort((a,b)=>a.id-b.id), occupied,total,maxStu, date:new Date().toLocaleDateString('es',{year:'numeric',month:'long',day:'numeric'}), }; }function downloadPDF(){ const d=getExportData(); const html=buildPrintHTML(d); const w=window.open('','_blank'); if(!w){boom('Permite ventanas emergentes','⚠');return;} w.document.write(html);w.document.close(); boom('Ventana de impresión abierta → Guardar como PDF','📄'); }function downloadWord(){ const d=getExportData(); const rtf=buildRTF(d); const blob=new Blob([rtf],{type:'application/rtf'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`aula-${info.name.replace(/\s/g,'_')}.rtf`; a.click(); boom('Archivo Word (.rtf) descargado','📝'); }return
{/* TOPBAR */}
🏫
{editName ?setInfo(p=>({...p,name:e.target.value}))} onBlur={()=>setEditName(false)} autoFocus style={{background:'transparent',border:'none',color:'#fff',fontFamily:'Fraunces,serif',fontWeight:700,fontSize:'1rem',outline:'none',width:160}}/> :setEditName(true)}>{info.name} } {unsaved&&}
setInfo(p=>({...p,grade:e.target.value}))} placeholder="GRADO / SECCIÓN" style={{background:'transparent',border:'none',color:'rgba(255,255,255,.38)',fontFamily:'inherit',fontSize:'.63rem',letterSpacing:'.08em',textTransform:'uppercase',outline:'none',width:150,fontWeight:500}}/>
{/* Rooms switcher button */}
setSearch(e.target.value)}/> {search&&}

{info.name}

{info.grade} · {occupied}/{total} estudiantes

{/* SIDEBAR */}{/* MAIN */}
{info.name}
{info.grade||'Sin especificar'} · {cfg.rows} filas × {cfg.cols} col. · {total} asientos · máx. {maxStu} est. {cfg.groupMode?` · Grupos de ${cfg.groupSize} fil.`:''} {(cfg.aisles||[]).length>0?` · ${cfg.aisles.length} pasillo(s)`:''}
{search&&{hl.size} resultado(s)}
↑ Pizarrón — Zona del docente
{/* Seat grid — flex-based to allow aisle gaps */}
{rowsData.map(({r,seats:rs})=>{ const aSet=new Set(cfg.aisles||[]); return {cfg.groupMode&&cfg.groupSize>0&&r>0&&r%cfg.groupSize===0&&
Grupo {Math.floor(r/cfg.groupSize)+1}
} {/* Row label */}
Fila {r+1}
{/* Seats row with aisles */}
{rs.map((seat,ci)=>{ const sn=r*cfg.cols+ci+1; const active=sn<=maxStu; return
{/* Aisle after this column */} {aSet.has(ci+1)&&ci+1
}
; })}
; })}
{CATS.map(cat=>{cat.ico} {cat.lbl})}
{tagCounts.some(c=>c.count>0)&&
{tagCounts.filter(c=>c.count>0).map(cat=>
setFilter(filter===cat.id?null:cat.id)}>
{cat.ico}
{cat.count}
{cat.lbl}
)}
}
{/* ROSTER */}
setRoster(p=>p.filter(x=>x!==n))} onClear={()=>setRoster([])}/>
{/* MODALS */} {showRooms&&setShowRooms(false)}/>} {showCfg&&setShowCfg(false)}/>} {selected&&!picking&&setSelected(null)} onClear={handleClear}/>}{/* ASSIGN BANNER */} {picking&&
🎯
Asignando
{picking}
→ Clic en asiento vacío
}{/* TOAST */} {toast&&
{toast.ico}{toast.msg}
}
; }ReactDOM.createRoot(document.getElementById('root')).render();
Desplazamiento al inicio