Seguimientos
Tus cursos favoritos, listos para organizar. Arrastra para reordenar, elimina o añade al carrito.
Tu lista
0 cursos en seguimiento
Aún no tienes favoritos
Ve al catálogo, marca cursos como favoritos y volverán a aparecer aquí.
Ventana de decisión
Este temporizador simula una oferta: organiza tu lista antes de que finalice.
Consejo: puedes arrastrar por el asa de la izquierda. El orden se guarda en tu navegador.
Resumen
Lo esencial de tus seguimientos
Preparar compra
Se abre un resumen seguro sin salir de la página.
Contacto
¿Necesitas ayuda con tus cursos?
Privacidad
Preferencias de cookies
ZenVoria Cursos
Formación en promoción de redes sociales adaptada a España.
zenvoria.click
Privacidad
Términos
FAQ
Contacto
Preferencias de cookies
Contacto
Tel: +34 911 23 45 67
Usamos cookies para mejorar tu experiencia. Puedes aceptar o gestionar preferencias.
Aceptar
Configurar
×
Cookies
Usamos cookies para recordar tu tema, tu lista de favoritos y ofrecer una experiencia consistente. Puedes aceptar o configurar.
Configurar
Rechazar
Aceptar
';
elFooter.innerHTML = '
ZenVoria Cursos
Formación en promoción de redes sociales adaptada a España.
zenvoria.click
Privacidad
Términos
FAQ
Contacto
Preferencias de cookies
Contacto
Tel: +34 911 23 45 67
Usamos cookies para mejorar tu experiencia. Puedes aceptar o gestionar preferencias.
Aceptar
Configurar
×
';
}
}
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 = '';
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
'+
'
'+
'Arrastra un curso usando el asa para cambiar su posición. '+
'El botón Añadir lo añade al carrito (se guarda en tu navegador). '+
'El botón Eliminar lo quita de Seguimientos. Opcionalmente también del carrito. '+
'El orden se guarda en localStorage como favorites . '+
' '+
'
'+
''+
'
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.
'+
'
'+
''+
'Esenciales Necesarias para que la página funcione. '+
'Preferencias Recuerda tema y ajustes. '+
'Analítica Medición anónima (demo). '+
'
';
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();
})();