diff --git a/public/admin.html b/public/admin.html
index a9b2888..6175c1b 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -162,9 +162,24 @@
-
+
Lesson content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/admin.js b/public/admin.js
index 977d593..9f11c5f 100644
--- a/public/admin.js
+++ b/public/admin.js
@@ -337,7 +337,7 @@ async function openItemEditor(unitId, itemId) {
$('#ie-heading').textContent = `Edit: ${item.title}`;
$('#ie-type').value = item.type;
$('#ie-title').value = item.title;
- $('#ie-body-lesson').value = item.body;
+ $('#ie-body-lesson').innerHTML = item.body || '';
$('#ie-body-assignment').value = item.body;
$('#ie-points').value = item.points || 10;
$('#ie-due-date').value = item.due_date || '';
@@ -358,7 +358,7 @@ async function openItemEditor(unitId, itemId) {
$('#ie-heading').textContent = 'New item';
$('#ie-type').value = 'lesson';
$('#ie-title').value = '';
- $('#ie-body-lesson').value = '';
+ $('#ie-body-lesson').innerHTML = '';
$('#ie-body-assignment').value = '';
$('#ie-points').value = 10;
$('#ie-due-date').value = '';
@@ -386,6 +386,61 @@ function updateItemFieldVisibility() {
}
$('#ie-type').addEventListener('change', updateItemFieldVisibility);
+// ---------- rich text editor (lesson body) ----------
+
+function getLessonBody() {
+ const html = $('#ie-body-lesson').innerHTML.trim();
+ return html === '
' ? '' : html;
+}
+
+let rtesavedRange = null;
+
+// Save selection whenever the editor loses focus (e.g. when clicking a toolbar button)
+$('#ie-body-lesson').addEventListener('blur', () => {
+ const sel = window.getSelection();
+ rtesavedRange = sel.rangeCount > 0 ? sel.getRangeAt(0).cloneRange() : null;
+});
+
+function rteRestoreAndExec(cmd, val = null) {
+ $('#ie-body-lesson').focus();
+ if (rtesavedRange) {
+ const sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(rtesavedRange);
+ }
+ document.execCommand(cmd, false, val);
+}
+
+// Format buttons (bold, italic, heading, lists, etc.)
+document.querySelectorAll('.rte-btn[data-cmd]').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ rteRestoreAndExec(btn.dataset.cmd, btn.dataset.val || null);
+ });
+});
+
+// Link button
+$('#rte-link-btn').addEventListener('click', () => {
+ const savedRange = rtesavedRange;
+ const url = prompt('Enter URL (e.g. https://example.com):');
+ if (url) {
+ rteRestoreAndExec('createLink', url);
+ }
+});
+
+// YouTube embed button
+$('#rte-youtube-btn').addEventListener('click', () => {
+ const input = prompt('YouTube URL or video ID:');
+ if (!input) return;
+ const match = input.match(/(?:youtu\.be\/|youtube\.com\/(?:embed\/|watch\?v=))([a-zA-Z0-9_-]{11})/);
+ const id = match ? match[1] : input.trim();
+ if (id) {
+ rteRestoreAndExec(
+ 'insertHTML',
+ `
`
+ );
+ }
+});
+
// ---------- scan a page (photo -> lesson/quiz content via local Ollama) ----------
let scanImageBase64 = null;
@@ -459,7 +514,7 @@ $('#scan-go-btn').addEventListener('click', async () => {
const providerLabel = result._provider === 'cloud' ? 'cloud API' : 'local Ollama';
const fallbackNote = result._fallback ? ` (primary failed β used ${providerLabel} as fallback: ${result._primaryError})` : ` via ${providerLabel}`;
if (mode === 'lesson') {
- $('#ie-body-lesson').value = result.body || '';
+ $('#ie-body-lesson').innerHTML = result.body || '';
$('#scan-status').textContent = `β
Scanned${fallbackNote}. Review the text below before saving.`;
} else {
$('#ie-questions').innerHTML = '';
@@ -590,7 +645,7 @@ $('#item-form').addEventListener('submit', async (e) => {
unitId: Number($('#ie-unit-id').value),
type,
title,
- body: type === 'lesson' ? $('#ie-body-lesson').value : type === 'assignment' ? $('#ie-body-assignment').value : '',
+ body: type === 'lesson' ? getLessonBody() : type === 'assignment' ? $('#ie-body-assignment').value : '',
points: type === 'assignment' ? Number($('#ie-points').value) || 0 : 0,
refId: type === 'spelling_practice' || type === 'spelling_test' ? Number($('#ie-list').value)
: type === 'flashcards' ? Number($('#ie-deck').value) : null,
diff --git a/public/styles.css b/public/styles.css
index b1807fb..dd6df45 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -483,6 +483,47 @@ td.overdue, .overdue { background: var(--bad-bg) !important; }
.parent-comment { background: var(--good-bg); border-radius: 10px; padding: 0.75rem 1rem; margin: 0.75rem 0; color: var(--good); }
.status-banner.bad { background: var(--bad-bg); color: var(--bad); border-radius: 10px; padding: 0.75rem 1rem; margin: 0.75rem 0; }
+/* ---------- rich text editor (lesson input) ---------- */
+.rte-label {
+ font-weight: 700; font-size: 0.9rem; margin-bottom: 0.3rem;
+}
+.rte-toolbar {
+ display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center;
+ padding: 0.4rem 0.6rem;
+ background: #f0ead8;
+ border: 2px solid #d8d2c0;
+ border-bottom: none;
+ border-radius: 10px 10px 0 0;
+}
+.rte-btn {
+ font-size: 0.85rem; font-family: inherit; font-weight: 600;
+ background: var(--card); color: var(--ink);
+ border: 1.5px solid #c8c2b0; border-radius: 6px;
+ padding: 0.25rem 0.55rem; cursor: pointer; line-height: 1.4;
+}
+.rte-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
+.rte-sep {
+ display: inline-block; width: 1px; height: 1.4rem;
+ background: #c8c2b0; margin: 0 0.15rem; flex-shrink: 0;
+}
+.rte-editor {
+ min-height: 200px; outline: none; cursor: text;
+ border: 2px solid #d8d2c0; border-radius: 0 0 10px 10px;
+ padding: 0.75rem; margin-bottom: 0;
+ font-size: 1.05rem; line-height: 1.6;
+ white-space: pre-wrap;
+}
+.rte-editor:focus { border-color: var(--accent); }
+.rte-editor:empty::before {
+ content: attr(data-placeholder);
+ color: var(--muted); pointer-events: none; white-space: pre-wrap;
+}
+.rte-editor h2 { font-size: 1.3rem; margin: 0.6rem 0 0.3rem; }
+.rte-editor h3 { font-size: 1.1rem; margin: 0.5rem 0 0.25rem; }
+.rte-editor ul, .rte-editor ol { margin: 0.4rem 0 0.4rem 1.5rem; }
+.rte-editor a { color: var(--accent-dark); }
+.rte-editor img, .rte-editor iframe { max-width: 100%; border-radius: 10px; }
+
/* ---------- offline 3-state tasks ---------- */
.today-row-offline { background: var(--card); border: 1.5px solid #e2dccf; border-radius: 14px; padding: 0.75rem 1rem; margin-bottom: 0.5rem; text-align: left; }
.offline-main { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }