Render presentations as slides in Learning Hub viewer

- New GET /api/learning/content/:slug/slides (authMiddleware only)
  Renders Marp markdown via marp-core, returns {css, slides[]} for
  any authenticated user — no moderator access required

- showViewer: when content_type === 'presentation', hides the body card
  and shows a presentation card with title, slide count, and View Slides button
  Auto-opens the slide modal immediately on load; button allows re-opening

- openSlidesFromSlug(): fetches slides from user-accessible endpoint
  then opens the existing slide preview modal (arrow keys, swipe, dots)

- Viewer already correctly hides feed/categories/search when viewing content;
  "Back to Feed" button returns to the main list
This commit is contained in:
Daniel Onyejesi 2026-03-24 03:04:14 -04:00
parent e5d79b8a9a
commit e3da7c5bb0
3 changed files with 88 additions and 11 deletions

View file

@ -20,6 +20,8 @@
<!-- Content viewer (hidden until item clicked) -->
<div id="lh-viewer" class="lh-viewer hidden">
<button id="lh-back" class="btn-sm btn-ghost" style="margin-bottom:12px;"><i class="fas fa-arrow-left"></i> Back to Feed</button>
<!-- Presentation viewer — shown instead of body card for slide content -->
<div id="lh-presentation-viewer" class="hidden" style="margin-bottom:12px;"></div>
<div class="card">
<div class="card-header">
<h3 id="lh-viewer-title"></h3>

View file

@ -287,18 +287,54 @@
if (item.created_at) parts.push(new Date(item.created_at).toLocaleDateString());
metaEl.textContent = parts.join(' | ');
}
if (bodyEl) {
// Render body as HTML (admin creates it); presentations store Marp markdown
bodyEl.innerHTML = sanitizeHtml(item.body || '');
}
var presViewer = document.getElementById('lh-presentation-viewer');
var bodyCard = bodyEl ? bodyEl.closest('.card') : null;
// Quiz
if (quizSection) {
if (item.questions && item.questions.length > 0) {
quizSection.classList.remove('hidden');
renderQuiz(item.questions);
} else {
quizSection.classList.add('hidden');
if (item.content_type === 'presentation') {
// Hide body card, show presentation card with View Slides button
if (bodyCard) bodyCard.style.display = 'none';
if (presViewer) {
var slideCount = (item.body || '').split(/\n---\n/).length;
presViewer.className = ''; // remove hidden
presViewer.innerHTML =
'<div class="card" style="text-align:center;padding:32px 24px;">' +
'<div style="font-size:42px;margin-bottom:12px;">📊</div>' +
'<h3 style="margin:0 0 4px;color:var(--g800);">' + esc(item.title) + '</h3>' +
'<p style="font-size:13px;color:var(--g500);margin:0 0 20px;">' + slideCount + ' slides' + (item.subject ? ' · ' + esc(item.subject) : '') + '</p>' +
'<button id="btn-lh-view-slides" class="btn-primary" style="padding:11px 28px;font-size:14px;border-radius:10px;border:none;cursor:pointer;background:var(--blue);color:white;font-weight:600;">' +
'<i class="fas fa-play-circle"></i> View Slides' +
'</button>' +
'</div>';
// Auto-load and open slides
document.getElementById('btn-lh-view-slides').addEventListener('click', function() {
openSlidesFromSlug(item.slug);
});
// Open automatically on load
openSlidesFromSlug(item.slug);
}
// Quiz for presentations (optional)
if (quizSection) {
if (item.questions && item.questions.length > 0) {
quizSection.classList.remove('hidden');
renderQuiz(item.questions);
} else {
quizSection.classList.add('hidden');
}
}
} else {
// Normal content — show body card, hide presentation card
if (bodyCard) bodyCard.style.display = '';
if (presViewer) presViewer.className = 'hidden';
if (bodyEl) bodyEl.innerHTML = sanitizeHtml(item.body || '');
if (quizSection) {
if (item.questions && item.questions.length > 0) {
quizSection.classList.remove('hidden');
renderQuiz(item.questions);
} else {
quizSection.classList.add('hidden');
}
}
}
if (resultsSection) resultsSection.classList.add('hidden');
@ -1473,6 +1509,19 @@
var _previewSlides = [];
var _previewIdx = 0;
// Open slide modal for a published presentation in the user-facing viewer
function openSlidesFromSlug(slug) {
showLoading('Loading slides…');
fetch('/api/learning/content/' + encodeURIComponent(slug) + '/slides', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
if (!data.success) { showToast(data.error || 'Could not load slides', 'error'); return; }
openSlideModal(data.slides, data.css);
})
.catch(function(err) { hideLoading(); showToast(err.message, 'error'); });
}
function previewSlides() {
var md = getMarpMarkdown();
if (!md) { showToast('No slide content yet. Use AI Generate first.', 'info'); return; }

View file

@ -115,6 +115,32 @@ router.get('/content/:slug', async function(req, res) {
} catch (err) { console.error('[LearningHub]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
// ============================================================
// RENDER PRESENTATION SLIDES (user-accessible)
// ============================================================
router.get('/content/:slug/slides', async function(req, res) {
try {
var item = await db.get(
'SELECT body, content_type FROM learning_content WHERE slug = ? AND published = true',
[req.params.slug]
);
if (!item) return res.status(404).json({ error: 'Content not found' });
if (item.content_type !== 'presentation') return res.status(400).json({ error: 'Not a presentation' });
var { Marp } = require('@marp-team/marp-core');
var marp = new Marp({ html: false });
var { html, css } = marp.render(item.body || '');
var slides = [];
var sectionReg = /<section[^>]*>[\s\S]*?<\/section>/g;
var match;
while ((match = sectionReg.exec(html)) !== null) slides.push(match[0]);
if (slides.length === 0) slides.push(html);
res.json({ success: true, css: css, slides: slides });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// ============================================================
// SUBMIT QUIZ ANSWERS
// ============================================================