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:
parent
e5d79b8a9a
commit
e3da7c5bb0
3 changed files with 88 additions and 11 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue