ZenVoria Cursos

Selecciona tema

Tema

Iniciar sesión

Al continuar aceptas los Términos.

Crear cuenta

Seguimientos

Tus cursos favoritos, listos para organizar. Arrastra para reordenar, elimina o añade al carrito.

Tu lista
0 cursos en seguimiento
    Ventana de decisión
    Este temporizador simula una oferta: organiza tu lista antes de que finalice.
    10:00
    Consejo: puedes arrastrar por el asa de la izquierda. El orden se guarda en tu navegador.
    '; elFooter.innerHTML = ''; } } function wireHeaderEnhancements(){ const tgl = d.querySelector('[data-theme-toggle], #themeToggle, .themeToggle'); if(tgl) tgl.addEventListener('click', (e)=>{ e.preventDefault(); themeToggle(); }); } function openModal(opts){ const o = opts || {}; modalTitle.textContent = o.title || 'Información'; modalSub.textContent = o.sub || ''; modalBody.innerHTML = ''; modalActions.innerHTML = ''; if(o.bodyNode){ modalBody.appendChild(o.bodyNode); }else if(typeof o.bodyHTML === 'string'){ modalBody.innerHTML = o.bodyHTML; } if(o.actionsNode){ modalActions.appendChild(o.actionsNode); }else if(typeof o.actionsHTML === 'string'){ modalActions.innerHTML = o.actionsHTML; }else{ const wrap=d.createElement('div'); wrap.className='flex items-center justify-end gap-2'; const b=d.createElement('button'); b.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-900 hover:bg-slate-50'; b.textContent='Cerrar'; b.addEventListener('click', closeModal); wrap.appendChild(b); modalActions.appendChild(wrap); } modalBackdrop.classList.remove('hidden'); modalBackdrop.setAttribute('aria-hidden','false'); setTimeout(()=>{ modalClose.focus(); }, 0); d.addEventListener('keydown', escClose, {passive:true}); } function closeModal(){ modalBackdrop.classList.add('hidden'); modalBackdrop.setAttribute('aria-hidden','true'); d.removeEventListener('keydown', escClose, {passive:true}); } function escClose(e){ if(e.key==='Escape') closeModal(); } modalClose.addEventListener('click', closeModal); modalBackdrop.addEventListener('click', (e)=>{ if(e.target===modalBackdrop || e.target===modalBackdrop.firstElementChild) closeModal(); }); function normalizeFavorites(raw){ if(Array.isArray(raw)) return raw.map(v=>String(v)).filter(Boolean); if(raw && typeof raw === 'object'){ if(Array.isArray(raw.items)) return raw.items.map(v=>String(v)).filter(Boolean); if(Array.isArray(raw.ids)) return raw.ids.map(v=>String(v)).filter(Boolean); } return []; } function readFavorites(){ const raw = getLS(LS_FAV, []); const ids = normalizeFavorites(raw); const uniq = Array.from(new Set(ids)); if(uniq.length !== ids.length) setLS(LS_FAV, uniq); return uniq; } function writeFavorites(ids){ setLS(LS_FAV, ids); } function readCart(){ const raw = getLS(LS_CART, []); if(Array.isArray(raw)) return raw.map(v=>String(v)).filter(Boolean); if(raw && typeof raw==='object'){ if(Array.isArray(raw.items)) return raw.items.map(v=>String(v)).filter(Boolean); if(Array.isArray(raw.ids)) return raw.ids.map(v=>String(v)).filter(Boolean); } return []; } function writeCart(ids){ setLS(LS_CART, ids); } async function fetchCatalog(){ const r = await fetch('./catalog.json', {cache:'no-store'}); if(!r.ok) throw new Error('No se pudo cargar catalog.json'); const data = await r.json(); const list = Array.isArray(data) ? data : (Array.isArray(data.courses) ? data.courses : (Array.isArray(data.items) ? data.items : [])); const byId = new Map(); for(const c of list){ const id = c && (c.id ?? c.slug ?? c.code ?? c.uid ?? c._id); if(id===undefined || id===null) continue; byId.set(String(id), c); } return {list, byId, raw:data}; } function fmtEUR(v){ const n = Number(v); if(!isFinite(n)) return '0,00 €'; try{ return new Intl.NumberFormat('es-ES',{style:'currency',currency:'EUR'}).format(n); }catch(e){ return n.toFixed(2).replace('.',',')+' €'; } } function pickCourseFields(c){ const title = c?.title ?? c?.name ?? c?.nombre ?? 'Curso'; const subtitle = c?.subtitle ?? c?.summary ?? c?.description ?? c?.descripcion ?? ''; const level = c?.level ?? c?.nivel ?? c?.difficulty ?? ''; const duration = c?.duration ?? c?.duracion ?? c?.hours ?? c?.horas ?? ''; const price = c?.price ?? c?.precio ?? c?.cost ?? 0; const rating = c?.rating ?? c?.valoracion ?? c?.stars ?? null; const tag = c?.category ?? c?.categoria ?? c?.track ?? ''; const img = c?.image ?? c?.img ?? c?.cover ?? ''; return {title:String(title), subtitle:String(subtitle||''), level:String(level||''), duration:String(duration||''), price:Number(price)||0, rating:rating, tag:String(tag||''), img:String(img||'')}; } function buildCourseLI(id, course, idx, state){ const c = pickCourseFields(course||{}); const inCart = state.cartSet.has(id); const li = d.createElement('li'); li.className = 'n3h5b xk9h7 rounded-2xl border border-slate-200/70 bg-white/65 p2aql shadow-sm overflow-hidden'; li.setAttribute('draggable','true'); li.dataset.id = id; li.dataset.index = String(idx); const top = d.createElement('div'); top.className = 'grid grid-cols-1 sm:grid-cols-12 gap-3 sm:gap-4 p-4 sm:p-5 items-start'; const drag = d.createElement('div'); drag.className = 'sm:col-span-1 flex sm:justify-center'; drag.innerHTML = '
    Arrastrar
    '; const main = d.createElement('div'); main.className = 'sm:col-span-7 min-w-0'; const head = d.createElement('div'); head.className = 'flex items-start justify-between gap-3'; const tWrap = d.createElement('div'); tWrap.className = 'min-w-0'; const title = d.createElement('div'); title.className = 'text-sm sm:text-base font-semibold leading-snug truncate'; title.textContent = c.title; const meta = d.createElement('div'); meta.className = 'mt-1 flex flex-wrap items-center gap-2 text-xs text-slate-600'; const pill = (txt, cls) => { const s=d.createElement('span'); s.className='inline-flex items-center rounded-full border px-2.5 py-1 '+cls; s.textContent=txt; return s; }; if(c.tag) meta.appendChild(pill(c.tag,'border-sky-200 bg-sky-50 text-sky-800')); if(c.level) meta.appendChild(pill(c.level,'border-slate-200 bg-white text-slate-700')); if(c.duration) meta.appendChild(pill(String(c.duration),'border-indigo-200 bg-indigo-50 text-indigo-800')); meta.appendChild(pill('ID: '+id,'border-slate-200 bg-white text-slate-600')); const desc = d.createElement('p'); desc.className = 'mt-2 text-sm text-slate-700 line-clamp-3'; desc.textContent = c.subtitle ? c.subtitle : 'Detalles disponibles en el catálogo. Este curso está guardado en tu lista de seguimientos.'; const stats = d.createElement('div'); stats.className = 'mt-3 flex flex-wrap items-center gap-2 text-xs text-slate-600'; const price = d.createElement('div'); price.className = 'inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white/70 px-3 py-2'; price.innerHTML = 'Precio'+fmtEUR(c.price)+''; const rating = d.createElement('div'); rating.className = 'inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white/70 px-3 py-2'; const rVal = (c.rating===null || c.rating===undefined || c.rating==='') ? null : Number(c.rating); rating.innerHTML = rVal && isFinite(rVal) ? 'Valoración'+rVal.toFixed(1)+'/5' : 'Valoración'; stats.appendChild(price); stats.appendChild(rating); tWrap.appendChild(title); tWrap.appendChild(meta); main.appendChild(head); main.appendChild(desc); main.appendChild(stats); const side = d.createElement('div'); side.className = 'sm:col-span-4 flex flex-col gap-2'; const actRow = d.createElement('div'); actRow.className = 'grid grid-cols-2 gap-2'; const btnCart = d.createElement('button'); btnCart.className = 'k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold shadow-sm ' + (inCart ? 'bg-emerald-600 text-white hover:bg-emerald-700' : 'bg-brand text-white hover:bg-brand-dark'); btnCart.innerHTML = inCart ? 'En carrito' : 'Añadir'; btnCart.addEventListener('click', ()=>{ const cart = readCart(); const set = new Set(cart); if(set.has(id)){ set.delete(id); writeCart(Array.from(set)); showStatus('Eliminado del carrito: '+c.title, 'info'); }else{ set.add(id); writeCart(Array.from(set)); showStatus('Añadido al carrito: '+c.title, 'ok'); } renderAll(); }); const btnRemove = d.createElement('button'); btnRemove.className = 'k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center gap-2 rounded-xl border border-slate-200 bg-white/70 px-3 py-2.5 text-sm font-semibold text-slate-900 hover:bg-white shadow-sm'; btnRemove.innerHTML = 'Eliminar'; btnRemove.addEventListener('click', ()=>{ const body = d.createElement('div'); body.className='space-y-3'; body.innerHTML = '
    ¿Eliminar este curso de Seguimientos?
    '+escapeHTML(c.title)+'
    Se mantendrá el carrito intacto, salvo que quieras quitarlo también.
    '; const actions = d.createElement('div'); actions.className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2'; const left = d.createElement('div'); left.className='flex items-center gap-2'; const toggle = d.createElement('label'); toggle.className='inline-flex items-center gap-2 text-sm text-slate-700'; toggle.innerHTML = ' Quitar también del carrito'; left.appendChild(toggle); const right = d.createElement('div'); right.className='flex items-center justify-end gap-2'; const bCancel = d.createElement('button'); bCancel.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-900 hover:bg-slate-50'; bCancel.textContent='Cancelar'; bCancel.addEventListener('click', closeModal); const bOk = d.createElement('button'); bOk.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl bg-rose-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-rose-700'; bOk.textContent='Eliminar'; bOk.addEventListener('click', ()=>{ const fav = readFavorites().filter(x=>x!==id); writeFavorites(fav); const rmCart = d.getElementById('rmCartToo')?.checked; if(rmCart){ const cart = readCart().filter(x=>x!==id); writeCart(cart); } closeModal(); showStatus('Eliminado de Seguimientos: '+c.title, 'ok'); renderAll(); }); right.appendChild(bCancel); right.appendChild(bOk); actions.appendChild(left); actions.appendChild(right); openModal({title:'Confirmación', sub:'Gestión de favoritos', bodyNode:body, actionsNode:actions}); }); actRow.appendChild(btnCart); actRow.appendChild(btnRemove); const btnDetails = d.createElement('button'); btnDetails.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center gap-2 rounded-xl border border-slate-200 bg-white/70 px-3 py-2.5 text-sm font-semibold text-slate-900 hover:bg-white shadow-sm'; btnDetails.innerHTML='Ver ficha'; btnDetails.addEventListener('click', ()=>{ const body=d.createElement('div'); body.className='space-y-3'; const blocks=[]; blocks.push('
    '+escapeHTML(c.title)+'
    '+(c.subtitle?('
    '+escapeHTML(c.subtitle)+'
    '):'
    Sin descripción adicional en el catálogo.
    ')+'
    '); blocks.push('
    '+ '
    Precio
    '+escapeHTML(fmtEUR(c.price))+'
    '+ '
    Valoración
    '+escapeHTML((rVal && isFinite(rVal))?rVal.toFixed(1)+'/5':'—')+'
    '+ '
    '); blocks.push('
    '+ '
    '+ '
    Categoría: '+escapeHTML(c.tag||'—')+'
    '+ '
    Nivel: '+escapeHTML(c.level||'—')+'
    '+ '
    Duración: '+escapeHTML(c.duration||'—')+'
    '+ '
    ID: '+escapeHTML(id)+'
    '+ '
    '+ '
    '); body.innerHTML=blocks.join(''); const actions=d.createElement('div'); actions.className='flex flex-col sm:flex-row gap-2 sm:items-center sm:justify-between'; const left=d.createElement('div'); left.className='text-xs text-slate-600'; left.textContent='Sugerencia: arrastra este curso en la lista para priorizarlo.'; const right=d.createElement('div'); right.className='flex items-center justify-end gap-2'; const bClose=d.createElement('button'); bClose.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-900 hover:bg-slate-50'; bClose.textContent='Cerrar'; bClose.addEventListener('click', closeModal); const bCart=d.createElement('button'); bCart.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl bg-brand px-4 py-2.5 text-sm font-semibold text-white hover:bg-brand-dark'; bCart.textContent= inCart ? 'Quitar del carrito' : 'Añadir al carrito'; bCart.addEventListener('click', ()=>{ const cart = readCart(); const set = new Set(cart); if(set.has(id)) set.delete(id); else set.add(id); writeCart(Array.from(set)); closeModal(); renderAll(); }); right.appendChild(bClose); right.appendChild(bCart); actions.appendChild(left); actions.appendChild(right); openModal({title:'Ficha del curso', sub:'Vista rápida desde Seguimientos', bodyNode:body, actionsNode:actions}); }); side.appendChild(actRow); side.appendChild(btnDetails); top.appendChild(drag); top.appendChild(main); top.appendChild(side); li.appendChild(top); li.addEventListener('dragstart', (e)=>{ li.dataset.drag='true'; e.dataTransfer.effectAllowed='move'; e.dataTransfer.setData('text/plain', id); try{ e.dataTransfer.setDragImage(li, 40, 20); }catch(err){} }); li.addEventListener('dragend', ()=>{ li.dataset.drag='false'; }); return li; } function escapeHTML(s){ return String(s).replace(/[&<>"']/g, (c)=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); } let catalogCache = null; async function ensureCatalog(){ if(catalogCache) return catalogCache; catalogCache = await fetchCatalog(); return catalogCache; } function calcEst(ids, byId){ let sum=0; for(const id of ids){ const c = byId.get(id); const p = c ? (c.price ?? c.precio ?? c.cost ?? 0) : 0; const n = Number(p); if(isFinite(n)) sum+=n; } return sum; } function setupDnD(){ elList.addEventListener('dragover', (e)=>{ e.preventDefault(); const after = getDragAfterElement(elList, e.clientY); const dragging = elList.querySelector('[data-drag="true"]'); if(!dragging) return; if(after==null){ elList.appendChild(dragging); }else{ elList.insertBefore(dragging, after); } }); elList.addEventListener('drop', (e)=>{ e.preventDefault(); const ids = Array.from(elList.querySelectorAll('li[data-id]')).map(li=>li.dataset.id).filter(Boolean); writeFavorites(ids); showStatus('Orden actualizado.', 'ok'); renderAll(true); }); } function getDragAfterElement(container, y){ const draggableElements = [...container.querySelectorAll('li[draggable="true"]:not([data-drag="true"])')]; return draggableElements.reduce((closest, child)=>{ const box = child.getBoundingClientRect(); const offset = y - box.top - box.height/2; if(offset < 0 && offset > closest.offset){ return {offset: offset, element: child}; }else{ return closest; } }, {offset: Number.NEGATIVE_INFINITY}).element; } function updateSummary(favIds, cartIds, byId){ const favSet = new Set(favIds); const inCart = cartIds.filter(id=>favSet.has(id)).length; elTotalFav.textContent = String(favIds.length); elInCart.textContent = String(inCart); elEst.textContent = fmtEUR(calcEst(favIds, byId)); } function setEmptyState(isEmpty){ if(isEmpty){ elEmpty.classList.remove('hidden'); }else{ elEmpty.classList.add('hidden'); } } async function renderAll(skipFetch){ const favIds = readFavorites(); const cartIds = readCart(); const cartSet = new Set(cartIds); elCount.textContent = String(favIds.length); let byId = new Map(); try{ const cat = skipFetch ? (catalogCache || await ensureCatalog()) : await ensureCatalog(); byId = cat.byId || byId; }catch(e){ showStatus('No se pudo cargar el catálogo. Se mostrará la lista con datos básicos.', 'warn'); } updateSummary(favIds, cartIds, byId); elList.innerHTML = ''; if(favIds.length===0){ setEmptyState(true); return; } setEmptyState(false); const frag = d.createDocumentFragment(); const state = {cartSet}; favIds.forEach((id, idx)=>{ const course = byId.get(id) || { id:id, title:'Curso guardado', subtitle:'El curso no se encontró en el catálogo actual. Puedes mantenerlo o eliminarlo de tu lista.', price:0 }; frag.appendChild(buildCourseLI(id, course, idx, state)); }); elList.appendChild(frag); d.querySelectorAll('.line-clamp-3').forEach(p=>{ p.style.display='-webkit-box'; p.style.webkitLineClamp='3'; p.style.webkitBoxOrient='vertical'; p.style.overflow='hidden'; }); } btnTheme.addEventListener('click', themeToggle); btnHelp.addEventListener('click', ()=>{ const body=d.createElement('div'); body.className='space-y-3 text-sm text-slate-700'; body.innerHTML = '
    '+ '
    Organiza tus favoritos
    '+ ''+ '
    '+ '
    '+ '
    Privacidad
    '+ '
    Puedes gestionar cookies desde el bloque «Privacidad». El sitio funciona incluso sin cookies, pero algunas preferencias (como tema) se guardan mejor si las aceptas.
    '+ '
    '; openModal({title:'Cómo funciona', sub:'Seguimientos (favoritos) + drag & drop', bodyNode:body}); }); btnClear.addEventListener('click', ()=>{ const fav = readFavorites(); if(fav.length===0){ showStatus('No hay nada que vaciar.', 'info'); return; } const body=d.createElement('div'); body.className='space-y-3 text-sm text-slate-700'; body.innerHTML = '
    '+ '
    Vas a vaciar tus Seguimientos
    '+ '
    Se eliminarán '+fav.length+' cursos de la lista de favoritos.
    '+ '
    '; const actions=d.createElement('div'); actions.className='flex items-center justify-end gap-2'; const bCancel=d.createElement('button'); bCancel.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-900 hover:bg-slate-50'; bCancel.textContent='Cancelar'; bCancel.addEventListener('click', closeModal); const bOk=d.createElement('button'); bOk.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl bg-rose-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-rose-700'; bOk.textContent='Vaciar'; bOk.addEventListener('click', ()=>{ writeFavorites([]); closeModal(); showStatus('Seguimientos vaciados.', 'ok'); renderAll(); }); actions.appendChild(bCancel); actions.appendChild(bOk); openModal({title:'Confirmación', sub:'Acción irreversible', bodyNode:body, actionsNode:actions}); }); btnExport.addEventListener('click', async ()=>{ const favIds = readFavorites(); if(favIds.length===0){ showStatus('No hay favoritos para copiar.', 'info'); return; } let lines = []; try{ const cat = await ensureCatalog(); for(const id of favIds){ const c = cat.byId.get(id); const f = pickCourseFields(c||{}); lines.push('- '+f.title+' (ID: '+id+', '+fmtEUR(f.price)+')'); } }catch(e){ lines = favIds.map(id=>'- Curso (ID: '+id+')'); } const text = 'ZenVoria — Seguimientos\n' + lines.join('\n'); try{ await navigator.clipboard.writeText(text); showStatus('Lista copiada al portapapeles.', 'ok'); }catch(e){ const body=d.createElement('div'); body.className='space-y-2'; const ta=d.createElement('textarea'); ta.className='w-full h-44 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900'; ta.value=text; body.appendChild(d.createTextNode('Copia manualmente el contenido:')); body.appendChild(ta); const actions=d.createElement('div'); actions.className='flex items-center justify-end gap-2'; const b=d.createElement('button'); b.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white hover:bg-slate-800'; b.textContent='Cerrar'; b.addEventListener('click', closeModal); actions.appendChild(b); openModal({title:'Copiar lista', sub:'Portapapeles no disponible', bodyNode:body, actionsNode:actions}); setTimeout(()=>{ ta.focus(); ta.select(); }, 60); } }); btnSupport.addEventListener('click', ()=>{ const body=d.createElement('div'); body.className='space-y-4'; body.innerHTML = '
    Envíanos un mensaje y te ayudaremos a planificar tu ruta de aprendizaje.
    '+ '
    '+ '
    '+ '
    '+ '
    '+ ''+ '
    '; const actions=d.createElement('div'); actions.className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2'; const left=d.createElement('div'); left.className='text-xs text-slate-600'; left.textContent='Teléfono: +34 911 234 567 • Email: [email protected]'; const right=d.createElement('div'); right.className='flex items-center justify-end gap-2'; const bCancel=d.createElement('button'); bCancel.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-900 hover:bg-slate-50'; bCancel.textContent='Cancelar'; bCancel.addEventListener('click', closeModal); const bSend=d.createElement('button'); bSend.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl bg-brand px-4 py-2.5 text-sm font-semibold text-white hover:bg-brand-dark'; bSend.textContent='Enviar'; bSend.addEventListener('click', ()=>{ const name=d.getElementById('sptName').value.trim(); const email=d.getElementById('sptEmail').value.trim(); const msg=d.getElementById('sptMsg').value.trim(); const err=d.getElementById('sptErr'); const emailOk=/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i.test(email); let problems=[]; if(name.length<2) problems.push('Escribe tu nombre.'); if(!emailOk) problems.push('Indica un email válido.'); if(msg.length<10) problems.push('El mensaje debe tener al menos 10 caracteres.'); if(problems.length){ err.textContent=problems.join(' '); err.classList.remove('hidden'); return; } err.classList.add('hidden'); const body2=d.createElement('div'); body2.className='space-y-2 text-sm text-slate-700'; body2.innerHTML = '
    '+ '
    Mensaje listo
    '+ '
    Para mantener esta demo sin backend, abrimos tu cliente de correo con el contenido preparado.
    '+ '
    '; const subject=encodeURIComponent('Soporte ZenVoria — Seguimientos'); const bodyTxt=encodeURIComponent('Nombre: '+name+'\nEmail: '+email+'\n\n'+msg+'\n\n— Enviado desde fallows.html'); const mailto='mailto:[email protected]?subject='+subject+'&body='+bodyTxt; const actions2=d.createElement('div'); actions2.className='flex items-center justify-end gap-2'; const bBack=d.createElement('button'); bBack.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-900 hover:bg-slate-50'; bBack.textContent='Volver'; bBack.addEventListener('click', ()=>{ closeModal(); btnSupport.click(); }); const bOpen=d.createElement('a'); bOpen.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white hover:bg-slate-800'; bOpen.href=mailto; bOpen.textContent='Abrir email'; bOpen.addEventListener('click', ()=>{ setTimeout(closeModal, 60); }); actions2.appendChild(bBack); actions2.appendChild(bOpen); openModal({title:'Soporte', sub:'Validación correcta', bodyNode:body2, actionsNode:actions2}); }); right.appendChild(bCancel); right.appendChild(bSend); actions.appendChild(left); actions.appendChild(right); openModal({title:'Escribir al soporte', sub:'Formulario con validación', bodyNode:body, actionsNode:actions}); }); btnCheckout.addEventListener('click', ()=>openCartModal('checkout')); btnOpenCart.addEventListener('click', ()=>openCartModal('cart')); function openCartModal(mode){ const favIds = readFavorites(); const cartIds = readCart(); const favSet = new Set(favIds); const cartInFav = cartIds.filter(id=>favSet.has(id)); const body=d.createElement('div'); body.className='space-y-3'; const top=d.createElement('div'); top.className='rounded-xl border border-slate-200 bg-white/70 p-4'; top.innerHTML = '
    '+ '
    '+(mode==='checkout'?'Preparar compra':'Carrito')+'
    '+ '
    Elementos en carrito (dentro de tus favoritos): '+cartInFav.length+'
    '+ '
    Se guarda en tu navegador
    '+ '
    '; body.appendChild(top); const listWrap=d.createElement('div'); listWrap.className='rounded-xl border border-slate-200 bg-white/70 p-3 max-h-64 overflow-auto m7q1d'; const ul=d.createElement('ul'); ul.className='space-y-2 text-sm'; if(cartInFav.length===0){ const p=d.createElement('div'); p.className='text-sm text-slate-700 p-2'; p.textContent='No tienes cursos del carrito en tus Seguimientos. Añade alguno desde la lista.'; listWrap.appendChild(p); }else{ cartInFav.forEach(id=>{ const li=d.createElement('li'); li.className='flex items-center justify-between gap-3 rounded-xl border border-slate-200 bg-white px-3 py-2'; li.innerHTML = '
    '+escapeHTML(id)+'
    ID de curso
    '; const b=d.createElement('button'); b.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl border border-slate-200 bg-white px-3 py-2 text-xs font-semibold text-slate-900 hover:bg-slate-50'; b.textContent='Quitar'; b.addEventListener('click', ()=>{ const newCart = readCart().filter(x=>x!==id); writeCart(newCart); closeModal(); renderAll(); openCartModal(mode); }); li.appendChild(b); ul.appendChild(li); }); listWrap.appendChild(ul); } body.appendChild(listWrap); const actions=d.createElement('div'); actions.className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2'; const left=d.createElement('div'); left.className='text-xs text-slate-600'; left.textContent='Tip: usa “Copiar lista” para compartir tus favoritos.'; const right=d.createElement('div'); right.className='flex items-center justify-end gap-2'; const bClose=d.createElement('button'); bClose.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-900 hover:bg-slate-50'; bClose.textContent='Cerrar'; bClose.addEventListener('click', closeModal); const bClear=d.createElement('button'); bClear.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl bg-rose-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-rose-700'; bClear.textContent='Vaciar carrito'; bClear.addEventListener('click', ()=>{ const cart = readCart(); if(cart.length===0){ showStatus('El carrito ya está vacío.', 'info'); return; } writeCart([]); closeModal(); renderAll(); showStatus('Carrito vaciado.', 'ok'); }); right.appendChild(bClose); right.appendChild(bClear); actions.appendChild(left); actions.appendChild(right); openModal({title:(mode==='checkout'?'Preparar compra':'Carrito'), sub:'Resumen rápido', bodyNode:body, actionsNode:actions}); } btnDemoAdd.addEventListener('click', async ()=>{ try{ const cat = await ensureCatalog(); const ids = Array.from(cat.byId.keys()); if(ids.length===0) throw new Error('Catálogo vacío'); const sample = ids.slice(0, Math.min(3, ids.length)); writeFavorites(sample); showStatus('Ejemplo cargado con '+sample.length+' cursos.', 'ok'); renderAll(true); }catch(e){ const demo=['demo-101','demo-205','demo-330']; writeFavorites(demo); showStatus('Ejemplo local cargado (sin catálogo).', 'warn'); renderAll(true); } }); function cookieConsentState(){ const c = getCookie(CK_CONSENT); if(!c) return null; if(c==='accepted' || c==='rejected' || c==='custom') return c; return null; } function showCookieBannerIfNeeded(){ const state = cookieConsentState(); if(state) return; cookieBanner.classList.remove('hidden'); } function openCookieSettingsModal(){ const prefs = safeJSONParse(getCookie(CK_PREFS) || '{}', {}); const body=d.createElement('div'); body.className='space-y-4 text-sm text-slate-700'; body.innerHTML = '
    '+ '
    Configurar cookies
    '+ '
    Puedes cambiar estas opciones cuando quieras.
    '+ '
    '+ '
    '+ ''+ ''+ ''+ '
    '; const actions=d.createElement('div'); actions.className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2'; const left=d.createElement('div'); left.className='text-xs text-slate-600'; left.textContent='No usamos trackers reales en esta demo.'; const right=d.createElement('div'); right.className='flex items-center justify-end gap-2'; const bCancel=d.createElement('button'); bCancel.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-900 hover:bg-slate-50'; bCancel.textContent='Cerrar'; bCancel.addEventListener('click', closeModal); const bSave=d.createElement('button'); bSave.className='k1p4u r8v1n g9c2t xk9h7 inline-flex items-center justify-center rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white hover:bg-slate-800'; bSave.textContent='Guardar'; bSave.addEventListener('click', ()=>{ const p = { preferences: !!d.getElementById('ckPrefs')?.checked, analytics: !!d.getElementById('ckAnalytics')?.checked }; setCookie(CK_PREFS, JSON.stringify(p), 365); setCookie(CK_CONSENT, 'custom', 365); cookieBanner.classList.add('hidden'); closeModal(); showStatus('Preferencias de cookies guardadas.', 'ok'); }); right.appendChild(bCancel); right.appendChild(bSave); actions.appendChild(left); actions.appendChild(right); openModal({title:'Cookies', sub:'Preferencias', bodyNode:body, actionsNode:actions}); setTimeout(()=>{ const ckP=d.getElementById('ckPrefs'); const ckA=d.getElementById('ckAnalytics'); if(ckP) ckP.checked=!!prefs.preferences; if(ckA) ckA.checked=!!prefs.analytics; }, 10); } ckAccept.addEventListener('click', ()=>{ setCookie(CK_CONSENT,'accepted',365); setCookie(CK_PREFS, JSON.stringify({preferences:true, analytics:false}),365); cookieBanner.classList.add('hidden'); showStatus('Cookies aceptadas.', 'ok'); }); ckReject.addEventListener('click', ()=>{ setCookie(CK_CONSENT,'rejected',365); setCookie(CK_PREFS, JSON.stringify({preferences:false, analytics:false}),365); cookieBanner.classList.add('hidden'); showStatus('Cookies rechazadas. Algunas preferencias no se guardarán en cookies.', 'info'); }); ckSettings.addEventListener('click', openCookieSettingsModal); ckCloseX.addEventListener('click', ()=>{ setCookie(CK_CONSENT,'rejected',365); setCookie(CK_PREFS, JSON.stringify({preferences:false, analytics:false}),365); cookieBanner.classList.add('hidden'); }); btnCookiePrefs.addEventListener('click', openCookieSettingsModal); function timerInit(){ let end = Number(localStorage.getItem(LS_TIMER_END) || '0'); const now = Date.now(); if(!end || !isFinite(end) || end < now){ end = now + 10*60*1000; localStorage.setItem(LS_TIMER_END, String(end)); } timerInit._end=end; tickTimer(); window.clearInterval(timerInit._i); timerInit._i = window.setInterval(tickTimer, 250); } function tickTimer(){ const end = timerInit._end || 0; const now = Date.now(); let ms = Math.max(0, end - now); const m = Math.floor(ms/60000); const s = Math.floor((ms%60000)/1000); const txt = String(m).padStart(2,'0')+':'+String(s).padStart(2,'0'); timerEl.textContent = txt; if(ms===0){ timerEl.parentElement.classList.remove('border-slate-200','bg-white/70'); timerEl.parentElement.classList.add('border-rose-200','bg-rose-50/70'); }else{ timerEl.parentElement.classList.remove('border-rose-200','bg-rose-50/70'); timerEl.parentElement.classList.add('border-slate-200','bg-white/70'); } } btnRestartTimer.addEventListener('click', ()=>{ const end = Date.now() + 10*60*1000; localStorage.setItem(LS_TIMER_END, String(end)); timerInit._end = end; showStatus('Temporizador reiniciado.', 'ok'); }); function applyDarkLightTweaks(){ const mode = rootEl.getAttribute('data-theme') || 'light'; const isDark = mode==='dark'; const swap = (sel, light, dark) => { d.querySelectorAll(sel).forEach(el=>{ el.classList.remove(...light); el.classList.remove(...dark); el.classList.add(...(isDark?dark:light)); }); }; swap('.p2aql', ['bg-white/60'], ['bg-slate-900/45']); swap('.u6l0a', ['text-slate-900'], ['text-slate-50']); } const themeObserver = new MutationObserver(()=>{ applyDarkLightTweaks(); }); themeObserver.observe(rootEl, {attributes:true, attributeFilter:['data-theme']}); function enforceDarkPalette(){ const isDark = rootEl.getAttribute('data-theme')==='dark'; if(!isDark) return; d.querySelectorAll('.border-slate-200').forEach(el=>el.classList.add('border-slate-800')); d.querySelectorAll('.bg-white').forEach(el=>{ el.classList.remove('bg-white'); el.classList.add('bg-slate-900'); }); d.querySelectorAll('.bg-white\\/70,.bg-white\\/55,.bg-white\\/60,.bg-white\\/65,.bg-white\\/85,.bg-white\\/40').forEach(el=>{ el.className = el.className .replace(/\bbg-white\/70\b/g,'bg-slate-900/55') .replace(/\bbg-white\/55\b/g,'bg-slate-900/50') .replace(/\bbg-white\/60\b/g,'bg-slate-900/52') .replace(/\bbg-white\/65\b/g,'bg-slate-900/56') .replace(/\bbg-white\/85\b/g,'bg-slate-900/72') .replace(/\bbg-white\/40\b/g,'bg-slate-900/38'); }); d.querySelectorAll('.text-slate-900').forEach(el=>{ el.classList.remove('text-slate-900'); el.classList.add('text-slate-50'); }); d.querySelectorAll('.text-slate-800').forEach(el=>{ el.classList.remove('text-slate-800'); el.classList.add('text-slate-100'); }); d.querySelectorAll('.text-slate-700').forEach(el=>{ el.classList.remove('text-slate-700'); el.classList.add('text-slate-200'); }); d.querySelectorAll('.text-slate-600').forEach(el=>{ el.classList.remove('text-slate-600'); el.classList.add('text-slate-300'); }); d.querySelectorAll('.hover\\:bg-white').forEach(el=>{ el.classList.remove('hover:bg-white'); el.classList.add('hover:bg-slate-800'); }); d.querySelectorAll('.hover\\:bg-slate-50').forEach(el=>{ el.classList.remove('hover:bg-slate-50'); el.classList.add('hover:bg-slate-800'); }); d.querySelectorAll('.bg-slate-50').forEach(el=>{ el.classList.remove('bg-slate-50'); el.classList.add('bg-slate-950'); }); d.querySelectorAll('.bg-slate-100').forEach(el=>{ el.classList.remove('bg-slate-100'); el.classList.add('bg-slate-800'); }); d.querySelectorAll('.bg-sky-50').forEach(el=>{ el.classList.remove('bg-sky-50'); el.classList.add('bg-sky-950/35'); }); d.querySelectorAll('.bg-indigo-50').forEach(el=>{ el.classList.remove('bg-indigo-50'); el.classList.add('bg-indigo-950/35'); }); d.querySelectorAll('.bg-emerald-50').forEach(el=>{ el.classList.remove('bg-emerald-50'); el.classList.add('bg-emerald-950/30'); }); d.querySelectorAll('.bg-amber-50').forEach(el=>{ el.classList.remove('bg-amber-50'); el.classList.add('bg-amber-950/25'); }); d.querySelectorAll('.border-sky-100').forEach(el=>{ el.classList.remove('border-sky-100'); el.classList.add('border-sky-900/50'); }); d.querySelectorAll('.border-indigo-100').forEach(el=>{ el.classList.remove('border-indigo-100'); el.classList.add('border-indigo-900/50'); }); d.querySelectorAll('.border-emerald-100').forEach(el=>{ el.classList.remove('border-emerald-100'); el.classList.add('border-emerald-900/50'); }); d.querySelectorAll('.border-amber-100').forEach(el=>{ el.classList.remove('border-amber-100'); el.classList.add('border-amber-900/50'); }); d.querySelectorAll('.border-slate-300').forEach(el=>{ el.classList.remove('border-slate-300'); el.classList.add('border-slate-700'); }); d.querySelectorAll('.bg-slate-900').forEach(()=>{}); } const repaintDark = ()=>{ enforceDarkPalette(); }; themeObserver.observe(rootEl, {attributes:true, attributeFilter:['data-theme']}); function onAnyRender(){ enforceDarkPalette(); applyDarkLightTweaks(); } const originalRenderAll = renderAll; renderAll = async function(skipFetch){ await originalRenderAll(skipFetch); onAnyRender(); }; function init(){ themeInit(); mountPartials().then(()=>{ enforceDarkPalette(); }); setupDnD(); timerInit(); showCookieBannerIfNeeded(); renderAll(); window.addEventListener('storage', (e)=>{ if([LS_FAV, LS_CART, LS_THEME].includes(e.key)){ if(e.key===LS_THEME) themeApply(localStorage.getItem(LS_THEME) || 'light'); renderAll(true); } }); } init(); })();