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 */}
;
}ReactDOM.createRoot(document.getElementById('root')).render();