/** * Blog Presentation Mode * Renders blog posts as full-viewport slides with presenter notes * Zero dependencies — pure JS */ /* eslint-disable no-unused-vars */ /** * Check if presentation mode is requested and initialize * Called from blog-post.js after post data is loaded */ function initPresentationMode(post) { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('mode') !== 'presentation') return false; const slides = buildSlides(post); if (slides.length === 0) return false; renderPresentation(slides); return true; } /** * Build slides from post data * Option A: curated slides from post.presentation * Option B: auto-extract from HTML content using

breaks */ function buildSlides(post) { const slides = []; // Title slide is always first const authorName = post.author_name || (post.author && post.author.name) || 'Tractatus Team'; const publishedDate = post.published_at ? new Date(post.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : ''; slides.push({ type: 'title', heading: post.title, subtitle: post.excerpt || '', meta: [authorName, publishedDate].filter(Boolean).join(' — '), notes: '' }); // Option A: curated presentation data if (post.presentation && post.presentation.enabled && post.presentation.slides && post.presentation.slides.length > 0) { for (const slide of post.presentation.slides) { slides.push({ type: 'content', heading: slide.heading, bullets: slide.bullets || [], notes: slide.notes || '' }); } return slides; } // Option B: auto-extract from content HTML const contentHtml = post.content_html || post.content || ''; if (!contentHtml) return slides; // Parse the HTML to extract sections by

const parser = new DOMParser(); const doc = parser.parseFromString(`
${contentHtml}
`, 'text/html'); const wrapper = doc.body.firstChild; let currentHeading = null; let currentElements = []; const flushSection = () => { if (!currentHeading) return; // Extract bullet points from paragraphs const bullets = []; for (const el of currentElements) { if (el.tagName === 'UL' || el.tagName === 'OL') { const items = el.querySelectorAll('li'); for (const item of items) { const text = item.textContent.trim(); if (text) bullets.push(text); } } else if (el.tagName === 'P') { const text = el.textContent.trim(); if (text) { const firstSentence = text.match(/^[^.!?]+[.!?]/); bullets.push(firstSentence ? firstSentence[0] : text.substring(0, 120)); } } else if (el.tagName === 'BLOCKQUOTE') { const text = el.textContent.trim(); if (text) bullets.push(text); } } // Limit to 6 bullets per slide for readability const slideBullets = bullets.slice(0, 6); // Use full section text as notes const allText = currentElements .map(el => el.textContent.trim()) .filter(Boolean) .join(' '); const notes = allText.length > 200 ? `${allText.substring(0, 500)}...` : allText; slides.push({ type: 'content', heading: currentHeading, bullets: slideBullets, notes }); }; for (const node of wrapper.childNodes) { if (node.nodeType !== 1) continue; // skip text nodes if (node.tagName === 'H2') { flushSection(); currentHeading = node.textContent.trim(); currentElements = []; } else if (currentHeading) { currentElements.push(node); } } flushSection(); return slides; } /** * Escape HTML for safe insertion */ function escapeForPresentation(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Render the presentation UI */ function renderPresentation(slides) { // Hide the normal article view document.body.classList.add('presentation-active'); // Create presentation container const container = document.createElement('div'); container.className = 'presentation-mode'; container.id = 'presentation-container'; // Progress bar const progress = document.createElement('div'); progress.className = 'slide-progress'; progress.id = 'slide-progress'; container.appendChild(progress); // Slides container const slidesEl = document.createElement('div'); slidesEl.className = 'presentation-slides'; for (let i = 0; i < slides.length; i++) { const slide = slides[i]; const slideEl = document.createElement('div'); slideEl.className = `presentation-slide${slide.type === 'title' ? ' slide-title' : ''}`; slideEl.dataset.index = i; if (i === 0) slideEl.classList.add('active'); if (slide.type === 'title') { const subtitleHtml = slide.subtitle ? `

${escapeForPresentation(slide.subtitle)}

` : ''; const metaHtml = slide.meta ? `

${escapeForPresentation(slide.meta)}

` : ''; slideEl.innerHTML = `

${escapeForPresentation(slide.heading)}

${subtitleHtml}${metaHtml}`; } else { let bulletsHtml = ''; if (slide.bullets && slide.bullets.length > 0) { const items = slide.bullets .map(b => `
  • ${escapeForPresentation(b)}
  • `) .join(''); bulletsHtml = ``; } slideEl.innerHTML = `

    ${escapeForPresentation(slide.heading)}

    ${bulletsHtml}`; } slidesEl.appendChild(slideEl); } container.appendChild(slidesEl); // Notes panel const notesPanel = document.createElement('div'); notesPanel.className = 'presentation-notes'; notesPanel.id = 'presentation-notes'; notesPanel.innerHTML = '
    ' + '
    Presenter Notes
    ' + '
    ' + '
    '; container.appendChild(notesPanel); // Navigation bar const nav = document.createElement('div'); nav.className = 'presentation-nav'; nav.innerHTML = '
    ' + '' + '' + '
    ' + `1 / ${slides.length}` + '
    ' + '' + '' + '
    '; container.appendChild(nav); // Keyboard hints overlay const hints = document.createElement('div'); hints.className = 'presentation-hints'; hints.id = 'presentation-hints'; hints.innerHTML = '

    Keyboard Shortcuts

    ' + '
    ' + '
    ← / →
    Previous / Next slide
    ' + '
    N
    Toggle presenter notes
    ' + '
    H / ?
    Show this help
    ' + '
    Esc
    Exit presentation
    ' + '
    F
    Toggle fullscreen
    ' + '
    '; container.appendChild(hints); document.body.appendChild(container); // Start presentation controller startController(slides, container); } /** * Presentation controller — manages slide state and navigation */ function startController(slides, container) { let currentIndex = 0; let notesOpen = false; let touchStartX = 0; let touchStartY = 0; const updateUI = () => { document.getElementById('slide-counter').textContent = `${currentIndex + 1} / ${slides.length}`; const pct = ((currentIndex + 1) / slides.length) * 100; document.getElementById('slide-progress').style.width = `${pct}%`; document.getElementById('pres-prev').disabled = (currentIndex === 0); document.getElementById('pres-next').disabled = (currentIndex === slides.length - 1); }; const updateNotes = () => { const notes = slides[currentIndex].notes || ''; document.getElementById('presentation-notes-text').textContent = notes; }; const goTo = index => { if (index < 0 || index >= slides.length) return; const slideEls = container.querySelectorAll('.presentation-slide'); const currentEl = slideEls[currentIndex]; const nextEl = slideEls[index]; // Direction-aware animation currentEl.classList.remove('active'); currentEl.classList.add('exiting-left'); nextEl.style.transform = index > currentIndex ? 'translateX(40px)' : 'translateX(-40px)'; // Trigger reflow then animate in void nextEl.offsetWidth; // eslint-disable-line no-void nextEl.classList.add('active'); nextEl.style.transform = ''; setTimeout(() => { currentEl.classList.remove('exiting-left'); }, 400); currentIndex = index; updateUI(); updateNotes(); }; const next = () => goTo(currentIndex + 1); const prev = () => goTo(currentIndex - 1); const exit = () => { document.removeEventListener('keydown', onKeyDown); container.removeEventListener('touchstart', onTouchStart); container.removeEventListener('touchend', onTouchEnd); document.body.style.overflow = ''; const el = document.getElementById('presentation-container'); if (el) el.remove(); document.body.classList.remove('presentation-active'); // Remove mode=presentation from URL without reload const url = new URL(window.location); url.searchParams.delete('mode'); window.history.replaceState({}, '', url.toString()); }; const toggleNotes = () => { notesOpen = !notesOpen; const panel = document.getElementById('presentation-notes'); if (notesOpen) { panel.classList.add('open'); } else { panel.classList.remove('open'); } }; const toggleFullscreen = () => { if (!document.fullscreenElement) { container.requestFullscreen().catch(() => {}); } else { document.exitFullscreen().catch(() => {}); } }; const showHints = () => { const hintsEl = document.getElementById('presentation-hints'); hintsEl.classList.add('visible'); setTimeout(() => { hintsEl.classList.remove('visible'); }, 3000); }; const onKeyDown = e => { switch (e.key) { case 'ArrowRight': case 'ArrowDown': case ' ': e.preventDefault(); next(); break; case 'ArrowLeft': case 'ArrowUp': e.preventDefault(); prev(); break; case 'Escape': e.preventDefault(); exit(); break; case 'n': case 'N': e.preventDefault(); toggleNotes(); break; case 'f': case 'F': e.preventDefault(); toggleFullscreen(); break; case 'h': case 'H': case '?': e.preventDefault(); showHints(); break; case 'Home': e.preventDefault(); goTo(0); break; case 'End': e.preventDefault(); goTo(slides.length - 1); break; } }; const onTouchStart = e => { touchStartX = e.changedTouches[0].screenX; touchStartY = e.changedTouches[0].screenY; }; const onTouchEnd = e => { const dx = e.changedTouches[0].screenX - touchStartX; const dy = e.changedTouches[0].screenY - touchStartY; // Only trigger if horizontal swipe is dominant and > 50px if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) { if (dx < 0) { next(); } else { prev(); } } }; // Bind events document.addEventListener('keydown', onKeyDown); container.addEventListener('touchstart', onTouchStart, { passive: true }); container.addEventListener('touchend', onTouchEnd, { passive: true }); document.getElementById('pres-prev').addEventListener('click', prev); document.getElementById('pres-next').addEventListener('click', next); document.getElementById('pres-exit').addEventListener('click', exit); document.getElementById('pres-notes').addEventListener('click', toggleNotes); // Click on slide area to advance container.querySelector('.presentation-slides').addEventListener('click', e => { if (e.target.closest('.presentation-nav') || e.target.closest('.presentation-notes')) return; next(); }); // Initial state updateUI(); updateNotes(); showHints(); document.body.style.overflow = 'hidden'; }