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 <section> 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
This commit is contained in:
Daniel Onyejesi 2026-03-24 01:13:55 -04:00
parent dc40e94749
commit 10620a3152
5 changed files with 160 additions and 34 deletions

View file

@ -229,26 +229,18 @@
<button id="btn-lh-download-pptx" class="btn-sm" style="background:#065f46;color:white;font-size:12px;border:none;border-radius:6px;padding:5px 12px;cursor:pointer;"><i class="fas fa-file-powerpoint"></i> Download PPTX</button>
</div>
</div>
<textarea id="lh-marp-editor" class="lh-marp-textarea" placeholder="---
marp: true
theme: default
<textarea id="lh-marp-editor" class="lh-marp-textarea" placeholder="✨ Click 'AI Generate' above to create your slides automatically.
After generation, you can edit the Marp markdown here if needed.
Each slide is separated by ---
Example structure:
# Title Slide
Subtitle here
---
# Slide Title
Your content here
---
# Second Slide
- Bullet point one
- Bullet point two
- Bullet point three
---
# Thank You"></textarea>
# Topic Slide
- Key point one
- Key point two"></textarea>
</div>
<!-- Quiz builder -->

View file

@ -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;}

View file

@ -264,6 +264,27 @@
<!-- TOAST -->
<div id="toast-container" class="toast-container"></div>
<!-- SLIDE PREVIEW MODAL -->
<div id="slide-preview-modal" class="slide-preview-modal hidden" role="dialog" aria-modal="true">
<div class="slide-preview-overlay" id="slide-preview-overlay"></div>
<div class="slide-preview-shell">
<div class="slide-preview-topbar">
<span class="slide-preview-counter" id="slide-preview-counter">1 / 1</span>
<span style="font-size:13px;color:rgba(255,255,255,0.6);">Use arrow keys or swipe to navigate</span>
<button class="slide-preview-close" id="slide-preview-close" aria-label="Close preview">✕ Close</button>
</div>
<div class="slide-preview-stage" id="slide-preview-stage">
<button class="slide-nav-btn slide-nav-prev" id="slide-nav-prev" aria-label="Previous slide">&#8249;</button>
<div class="slide-preview-frame" id="slide-preview-frame">
<style id="slide-preview-css"></style>
<div id="slide-preview-content"></div>
</div>
<button class="slide-nav-btn slide-nav-next" id="slide-nav-next" aria-label="Next slide">&#8250;</button>
</div>
<div class="slide-preview-dots" id="slide-preview-dots"></div>
</div>
</div>
<!-- SCRIPTS (defer for faster initial paint) -->
<script src="/vendor/tiptap.bundle.js"></script>
<script defer src="/js/milestonesData.js"></script>

View file

@ -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 '<button class="slide-dot" data-idx="' + i + '" aria-label="Go to slide ' + (i+1) + '"></button>';
}).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; }

View file

@ -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 = `<!DOCTYPE html><html><head><meta charset="UTF-8">
<style>${css}
body{margin:0;background:#1a1a2e;display:flex;flex-wrap:wrap;gap:16px;padding:24px;justify-content:center;}
section{box-shadow:0 4px 20px rgba(0,0,0,0.4);flex-shrink:0;}
</style></head><body>${html}</body></html>`;
// Extract individual <section> elements for page-by-page navigation
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); // 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 });
}