const tutorialsRoot = document.querySelector('[data-tutorials-root]');
const tutorialsDataNode = document.getElementById('tutorialsData');
if (tutorialsRoot && tutorialsDataNode) {
let tutorials = [];
try {
tutorials = JSON.parse(tutorialsDataNode.textContent || '[]');
} catch (error) {
tutorials = [];
}
if (Array.isArray(tutorials) && tutorials.length > 0) {
const stage = document.getElementById('tutorialStage');
const viewerFrame = document.getElementById('tutorialViewerFrame');
const titleNode = document.getElementById('tutorialTitle');
const descriptionNode = document.getElementById('tutorialDescription');
const sourceNode = document.getElementById('tutorialSource');
const providerNode = document.getElementById('tutorialProvider');
const openLink = document.getElementById('tutorialOpenLink');
const buttons = Array.from(tutorialsRoot.querySelectorAll('.tutorial-trigger'));
const groupNodes = Array.from(tutorialsRoot.querySelectorAll('.tutorial-group'));
const groupToggles = Array.from(tutorialsRoot.querySelectorAll('[data-group-toggle]'));
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let activeId = tutorialsRoot.getAttribute('data-initial-tutorial') || tutorials[0].id;
let switchToken = 0;
let readyTimeoutId = 0;
const getTutorialById = (id) => tutorials.find((item) => item.id === id);
const buildFallback = (item) => {
const wrapper = document.createElement('div');
const inner = document.createElement('div');
const heading = document.createElement('h3');
const text = document.createElement('p');
wrapper.className = 'tutorial-fallback';
heading.textContent = item?.name || 'Tutorial';
text.textContent = item?.fallback_message || 'Este tutorial aun no tiene un video asociado.';
inner.append(heading, text);
wrapper.append(inner);
return wrapper;
};
const buildMedia = (item) => {
const source = item?.source || {};
if (item?.source_type === 'local') {
const video = document.createElement('video');
const mediaSource = document.createElement('source');
video.className = 'tutorial-media';
video.controls = true;
video.preload = 'metadata';
mediaSource.src = item?.embed_url || '';
mediaSource.type = source.mime || 'video/mp4';
video.appendChild(mediaSource);
return {
element: video,
readyEvent: 'loadeddata',
};
}
const iframe = document.createElement('iframe');
iframe.className = 'tutorial-media';
iframe.src = item?.embed_url || '';
iframe.title = `Tutorial de ${item?.name || 'recurso'}`;
iframe.loading = 'eager';
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
iframe.allowFullscreen = true;
return {
element: iframe,
readyEvent: 'load',
};
};
const setGroupState = (groupKey, isOpen) => {
const group = groupNodes.find((node) => node.dataset.tutorialGroup === groupKey);
if (!group) return;
const toggle = group.querySelector('[data-group-toggle]');
const panel = group.querySelector('.tutorials-group-panel');
if (!toggle || !panel) return;
group.classList.toggle('is-open', isOpen);
toggle.setAttribute('aria-expanded', String(isOpen));
panel.hidden = !isOpen;
};
const ensureGroupOpen = (groupKey) => {
if (!groupKey) return;
setGroupState(groupKey, true);
};
const finishLoading = (token) => {
if (token !== switchToken || !stage || !viewerFrame) return;
stage.classList.remove('is-loading');
window.clearTimeout(readyTimeoutId);
window.setTimeout(() => {
if (token === switchToken) {
stage.classList.remove('is-switching');
}
}, 220);
viewerFrame.setAttribute('aria-busy', 'false');
};
const renderTutorial = (id, options = {}) => {
const item = getTutorialById(id);
if (!item || !stage || !viewerFrame || !titleNode || !descriptionNode || !sourceNode || !providerNode || !openLink) {
return;
}
const shouldScroll = Boolean(options.scrollIntoView);
switchToken += 1;
activeId = item.id;
buttons.forEach((button) => {
const isActive = button.dataset.tutorialId === item.id;
button.classList.toggle('is-active', isActive);
button.setAttribute('aria-selected', String(isActive));
});
ensureGroupOpen(item.category || '');
stage.style.setProperty('--tutorial-accent', item.accent || '#b5121b');
stage.setAttribute('aria-labelledby', `tutorial-tab-${item.id}`);
stage.classList.remove('has-error');
stage.classList.add('is-loading', 'is-switching');
titleNode.textContent = item.name || '';
descriptionNode.textContent = item.description || '';
sourceNode.textContent = item.source_label || 'Pendiente';
providerNode.textContent = item.provider || '';
const openUrl = item.open_url || '';
openLink.href = openUrl || '#';
openLink.textContent = item.source_type === 'local'
? 'Abrir video local'
: openUrl
? 'Abrir fuente original'
: 'Video pendiente';
openLink.classList.toggle('is-disabled', !openUrl);
if (openUrl) {
openLink.setAttribute('target', '_blank');
openLink.setAttribute('rel', 'noopener noreferrer');
openLink.removeAttribute('aria-disabled');
openLink.removeAttribute('tabindex');
} else {
openLink.removeAttribute('target');
openLink.removeAttribute('rel');
openLink.setAttribute('aria-disabled', 'true');
openLink.setAttribute('tabindex', '-1');
}
viewerFrame.setAttribute('aria-busy', 'true');
viewerFrame.innerHTML = '';
if (!item.has_media) {
viewerFrame.appendChild(buildFallback(item));
finishLoading(switchToken);
return;
}
const token = switchToken;
const media = buildMedia(item);
const onReady = () => finishLoading(token);
const onError = () => {
if (token !== switchToken) return;
stage.classList.remove('is-loading');
viewerFrame.setAttribute('aria-busy', 'false');
viewerFrame.innerHTML = '';
viewerFrame.appendChild(buildFallback(item));
};
media.element.addEventListener(media.readyEvent, onReady, { once: true });
media.element.addEventListener('error', onError, { once: true });
viewerFrame.appendChild(media.element);
window.clearTimeout(readyTimeoutId);
readyTimeoutId = window.setTimeout(onReady, item.source_type === 'local' ? 450 : 900);
if (shouldScroll && window.innerWidth < 960) {
stage.scrollIntoView({
behavior: reducedMotion.matches ? 'auto' : 'smooth',
block: 'start',
});
}
};
const moveSelection = (currentButton, direction) => {
const visibleButtons = buttons.filter((button) => !button.closest('.tutorials-group-panel')?.hidden);
const currentIndex = visibleButtons.indexOf(currentButton);
if (currentIndex === -1) return;
let nextIndex = currentIndex;
if (direction === 'next') {
nextIndex = (currentIndex + 1) % visibleButtons.length;
} else if (direction === 'prev') {
nextIndex = (currentIndex - 1 + visibleButtons.length) % visibleButtons.length;
} else if (direction === 'first') {
nextIndex = 0;
} else if (direction === 'last') {
nextIndex = visibleButtons.length - 1;
}
const nextButton = visibleButtons[nextIndex];
if (!nextButton) return;
nextButton.focus();
renderTutorial(nextButton.dataset.tutorialId || activeId);
};
buttons.forEach((button) => {
button.addEventListener('click', () => {
const id = button.dataset.tutorialId || activeId;
renderTutorial(id, { scrollIntoView: true });
});
button.addEventListener('keydown', (event) => {
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
event.preventDefault();
moveSelection(button, 'next');
}
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
event.preventDefault();
moveSelection(button, 'prev');
}
if (event.key === 'Home') {
event.preventDefault();
moveSelection(button, 'first');
}
if (event.key === 'End') {
event.preventDefault();
moveSelection(button, 'last');
}
});
});
groupToggles.forEach((toggle) => {
toggle.addEventListener('click', () => {
const group = toggle.closest('.tutorial-group');
const groupKey = group?.dataset.tutorialGroup || '';
const isOpen = toggle.getAttribute('aria-expanded') === 'true';
setGroupState(groupKey, !isOpen);
});
});
if (!getTutorialById(activeId)) {
renderTutorial(tutorials[0].id);
}
}
}