From 10620a3152393fac8b676f7cb52cc0faa428d0ef Mon Sep 17 00:00:00 2001 From: Daniel Onyejesi Date: Tue, 24 Mar 2026 01:13:55 -0400 Subject: [PATCH] Fix delete bar always visible, slide preview modal, Marp textarea hint Delete bar bug: - Root cause: .lh-delete-confirm-bar{display:flex} defined after .hidden{display:none} so flex overrode hidden (no !important on global .hidden rule) - Fix: remove display:flex from base rule, use .lh-delete-confirm-bar:not(.hidden){display:flex} so hidden class always wins regardless of CSS order Slide preview (replace window.open + document.write approach): - Backend /preview-slides now returns JSON: {css, slides[]} (individual
HTML) - In-page full-screen modal with dark overlay, white slide frame - Prev/Next arrow buttons, dot indicators, slide counter - Keyboard navigation (arrow keys, Escape to close) - Touch/swipe support for mobile - Overlay click closes modal - No popups required, works in all browsers Marp textarea: - Placeholder updated to guide users: "Click AI Generate above to create slides" - Makes it clear AI is the primary authoring path; textarea for review/edits --- public/components/cms.html | 30 ++++------- public/css/styles.css | 26 +++++++++- public/index.html | 21 ++++++++ public/js/learningHub.js | 101 ++++++++++++++++++++++++++++++++++--- src/routes/learningAI.js | 16 +++--- 5 files changed, 160 insertions(+), 34 deletions(-) diff --git a/public/components/cms.html b/public/components/cms.html index e54648f..70bd50e 100644 --- a/public/components/cms.html +++ b/public/components/cms.html @@ -229,26 +229,18 @@ - +# Topic Slide +- Key point one +- Key point two"> diff --git a/public/css/styles.css b/public/css/styles.css index 6740dab..9286080 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -711,13 +711,37 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(-- .lh-quiz-multi-hint{font-size:12px;color:var(--g500);margin-bottom:10px;font-style:italic;} /* ── Inline delete confirmation bar ──────────────────────── */ -.lh-delete-confirm-bar{display:flex;align-items:center;gap:10px;padding:10px 16px;background:#fef2f2;border:1.5px solid #fca5a5;border-radius:10px;margin-bottom:12px;font-size:13px;color:#991b1b;flex-wrap:wrap;} +/* display:flex is NOT set here — .hidden (display:none) must win */ +.lh-delete-confirm-bar{align-items:center;gap:10px;padding:10px 16px;background:#fef2f2;border:1.5px solid #fca5a5;border-radius:10px;margin-bottom:12px;font-size:13px;color:#991b1b;flex-wrap:wrap;} +.lh-delete-confirm-bar:not(.hidden){display:flex;} .lh-delete-confirm-bar i{color:var(--red);flex-shrink:0;} /* ── Marp / Presentation editor ──────────────────────────── */ .lh-marp-textarea{width:100%;min-height:320px;font-family:'Courier New',monospace;font-size:13px;line-height:1.6;padding:14px;border:1px solid var(--g300);border-radius:8px;resize:vertical;box-sizing:border-box;background:var(--g900);color:#e5e7eb;outline:none;} .lh-marp-textarea:focus{border-color:var(--blue);box-shadow:0 0 0 2px var(--blue-light);} +/* ── Slide preview modal ──────────────────────────────────── */ +.slide-preview-modal{position:fixed;inset:0;z-index:9000;display:flex;align-items:center;justify-content:center;} +.slide-preview-modal.hidden{display:none;} +.slide-preview-overlay{position:absolute;inset:0;background:rgba(0,0,0,0.85);} +.slide-preview-shell{position:relative;z-index:1;width:100%;height:100%;display:flex;flex-direction:column;padding:12px;} +.slide-preview-topbar{display:flex;align-items:center;justify-content:space-between;padding:0 0 10px;flex-shrink:0;} +.slide-preview-counter{font-size:14px;font-weight:700;color:white;background:rgba(255,255,255,0.15);padding:4px 14px;border-radius:20px;} +.slide-preview-close{background:rgba(255,255,255,0.15);border:none;color:white;padding:6px 16px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;transition:background 0.15s;} +.slide-preview-close:hover{background:rgba(255,255,255,0.25);} +.slide-preview-stage{flex:1;display:flex;align-items:center;justify-content:center;gap:12px;min-height:0;overflow:hidden;} +.slide-preview-frame{flex:1;max-width:900px;background:white;border-radius:8px;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,0.6);max-height:100%;display:flex;flex-direction:column;} +.slide-preview-frame section{all:unset;display:block;} +.slide-nav-btn{background:rgba(255,255,255,0.15);border:none;color:white;font-size:36px;line-height:1;padding:10px 14px;border-radius:10px;cursor:pointer;flex-shrink:0;transition:background 0.15s;user-select:none;} +.slide-nav-btn:hover{background:rgba(255,255,255,0.25);} +.slide-preview-dots{display:flex;justify-content:center;gap:6px;padding-top:10px;flex-shrink:0;flex-wrap:wrap;} +.slide-dot{width:10px;height:10px;border-radius:50%;border:none;background:rgba(255,255,255,0.3);cursor:pointer;padding:0;transition:background 0.15s;} +.slide-dot.active{background:white;} +@media(max-width:600px){ + .slide-nav-btn{font-size:24px;padding:8px 10px;} + .slide-preview-topbar span:nth-child(2){display:none;} +} + /* ── AI Generate panel ────────────────────────────────────── */ .lh-ai-panel{border:2px solid transparent;background:linear-gradient(white,white) padding-box,linear-gradient(135deg,#7c3aed,#2563eb) border-box;border-radius:12px;padding:20px;margin-bottom:16px;} .lh-ai-panel-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;} diff --git a/public/index.html b/public/index.html index 52b9554..430fa82 100644 --- a/public/index.html +++ b/public/index.html @@ -264,6 +264,27 @@
+ + + diff --git a/public/js/learningHub.js b/public/js/learningHub.js index bda6f4a..fc30ec7 100644 --- a/public/js/learningHub.js +++ b/public/js/learningHub.js @@ -23,6 +23,13 @@ // ── Event delegation ─────────────────────────────────────── document.addEventListener('click', function(e) { + // Slide preview modal controls (checked first — modal is outside CMS) + if (e.target.closest('#slide-preview-close') || e.target.id === 'slide-preview-overlay') { closeSlideModal(); return; } + if (e.target.closest('#slide-nav-prev')) { slideStep(-1); return; } + if (e.target.closest('#slide-nav-next')) { slideStep(1); return; } + var dot = e.target.closest('.slide-dot'); + if (dot) { renderSlide(parseInt(dot.dataset.idx)); return; } + // Feed item click var feedItem = e.target.closest('.lh-feed-item'); if (feedItem && feedItem.dataset.slug) { @@ -1360,23 +1367,103 @@ return ta ? ta.value.trim() : ''; } + // ── Slide preview modal ────────────────────────────────────── + var _previewSlides = []; + var _previewIdx = 0; + function previewSlides() { var md = getMarpMarkdown(); - if (!md) { showToast('No slide content yet', 'error'); return; } + if (!md) { showToast('No slide content yet. Use AI Generate first.', 'info'); return; } + + showLoading('Rendering slides…'); fetch('/api/admin/learning/preview-slides', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ markdown: md }) }) - .then(function(r) { return r.text(); }) - .then(function(html) { - var win = window.open('', '_blank'); - if (win) { win.document.write(html); win.document.close(); } - else showToast('Allow popups to preview slides', 'info'); + .then(function(r) { return r.json(); }) + .then(function(data) { + hideLoading(); + if (!data.success) { showToast(data.error || 'Preview failed', 'error'); return; } + openSlideModal(data.slides, data.css); }) - .catch(function(err) { showToast(err.message, 'error'); }); + .catch(function(err) { hideLoading(); showToast(err.message, 'error'); }); } + function openSlideModal(slides, css) { + _previewSlides = slides || []; + _previewIdx = 0; + + var modal = document.getElementById('slide-preview-modal'); + var cssEl = document.getElementById('slide-preview-css'); + var dotsEl = document.getElementById('slide-preview-dots'); + + if (!modal) return; + + // Inject Marp CSS once (scoped to preview frame) + if (cssEl) cssEl.textContent = css || ''; + + // Build dot indicators + if (dotsEl) { + dotsEl.innerHTML = _previewSlides.map(function(_, i) { + return ''; + }).join(''); + } + + modal.classList.remove('hidden'); + document.body.style.overflow = 'hidden'; + renderSlide(0); + + // Keyboard navigation + document._slideKeyHandler = function(e) { + if (e.key === 'ArrowRight' || e.key === 'ArrowDown') slideStep(1); + else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') slideStep(-1); + else if (e.key === 'Escape') closeSlideModal(); + }; + document.addEventListener('keydown', document._slideKeyHandler); + + // Touch/swipe + var touchStartX = 0; + modal._touchStart = function(e) { touchStartX = e.touches[0].clientX; }; + modal._touchEnd = function(e) { + var dx = e.changedTouches[0].clientX - touchStartX; + if (Math.abs(dx) > 40) slideStep(dx < 0 ? 1 : -1); + }; + modal.addEventListener('touchstart', modal._touchStart); + modal.addEventListener('touchend', modal._touchEnd); + } + + function renderSlide(idx) { + var content = document.getElementById('slide-preview-content'); + var counter = document.getElementById('slide-preview-counter'); + var dots = document.querySelectorAll('.slide-dot'); + var prev = document.getElementById('slide-nav-prev'); + var next = document.getElementById('slide-nav-next'); + + if (!content || !_previewSlides.length) return; + _previewIdx = Math.max(0, Math.min(idx, _previewSlides.length - 1)); + + content.innerHTML = _previewSlides[_previewIdx]; + if (counter) counter.textContent = (_previewIdx + 1) + ' / ' + _previewSlides.length; + if (prev) prev.style.opacity = _previewIdx === 0 ? '0.3' : '1'; + if (next) next.style.opacity = _previewIdx === _previewSlides.length - 1 ? '0.3' : '1'; + dots.forEach(function(d, i) { d.classList.toggle('active', i === _previewIdx); }); + } + + function slideStep(dir) { renderSlide(_previewIdx + dir); } + + function closeSlideModal() { + var modal = document.getElementById('slide-preview-modal'); + if (modal) modal.classList.add('hidden'); + document.body.style.overflow = ''; + if (document._slideKeyHandler) { + document.removeEventListener('keydown', document._slideKeyHandler); + document._slideKeyHandler = null; + } + } + + // slide modal nav is wired via the main document click delegation below + function downloadPptx() { var md = getMarpMarkdown(); if (!md) { showToast('No slide content yet', 'error'); return; } diff --git a/src/routes/learningAI.js b/src/routes/learningAI.js index 2ee0c5c..c5b5f7c 100644 --- a/src/routes/learningAI.js +++ b/src/routes/learningAI.js @@ -440,14 +440,16 @@ router.post('/preview-slides', async function(req, res) { var marp = new Marp({ html: false }); var { html, css } = marp.render(markdown); - var fullHtml = ` -${html}`; + // Extract individual
elements for page-by-page navigation + var slides = []; + var sectionReg = /]*>[\s\S]*?<\/section>/g; + var match; + while ((match = sectionReg.exec(html)) !== null) { + slides.push(match[0]); + } + if (slides.length === 0) slides.push(html); // fallback - res.setHeader('Content-Type', 'text/html'); - res.send(fullHtml); + res.json({ success: true, css: css, slides: slides }); } catch (err) { res.status(500).json({ error: err.message }); }