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:
parent
dc40e94749
commit
10620a3152
5 changed files with 160 additions and 34 deletions
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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;}
|
||||
|
|
|
|||
|
|
@ -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">‹</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">›</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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue