+ Upload New Material +
+ + ++ + Materials + +
+ +No materials yet
+Upload the first file using the form above.
+Failed to load materials
+ +diff --git a/migrations/0015_add_course_materials.sql b/migrations/0015_add_course_materials.sql new file mode 100644 index 0000000..85f2e2e --- /dev/null +++ b/migrations/0015_add_course_materials.sql @@ -0,0 +1,17 @@ +-- Migration 0015: Add course_materials table for activity file attachments + +CREATE TABLE IF NOT EXISTS course_materials ( + id TEXT PRIMARY KEY, + activity_id TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + file_key TEXT NOT NULL, + uploaded_by TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE, + FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL +); + +CREATE INDEX IF NOT EXISTS idx_materials_activity ON course_materials(activity_id); +CREATE INDEX IF NOT EXISTS idx_materials_uploader ON course_materials(uploaded_by); +CREATE INDEX IF NOT EXISTS idx_materials_created ON course_materials(activity_id, created_at DESC); diff --git a/public/activity-detail.html b/public/activity-detail.html index a589bb0..b731bb0 100644 --- a/public/activity-detail.html +++ b/public/activity-detail.html @@ -134,12 +134,20 @@
No materials uploaded yet.
'; + return; + } + listEl.innerHTML = mats.slice(0, 5).map(m => ` +${esc(m.title)}
+ ${m.description ? `${esc(m.description)}
` : ''} +Failed to load materials.
'; + } + } + + async function downloadDetailMaterial(actId, mid) { + if (!token) { + alert('Please log in to download materials.'); + window.location.href = '/login'; + return; + } + try { + const res = await fetch(`/api/activities/${encodeURIComponent(actId)}/materials/${encodeURIComponent(mid)}/download`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Download failed'); + const payload = data.data || data; + let url = payload.download_url || ''; + const filename = payload.filename || payload.title || 'download'; + if (url.startsWith('/api/r2/')) { + const proxied = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }); + if (!proxied.ok) throw new Error('Download failed'); + const blob = await proxied.blob(); + const objectUrl = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = objectUrl; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(objectUrl); + } else { + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.target = '_blank'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + } catch (e) { + alert(e.message || 'Download failed'); + } } function renderSessions(data) { diff --git a/public/course-materials.html b/public/course-materials.html new file mode 100644 index 0000000..d22f918 --- /dev/null +++ b/public/course-materials.html @@ -0,0 +1,654 @@ + + + + + +Loading activity…
+No materials yet
+Upload the first file using the form above.
+Failed to load materials
+ ++ Are you sure you want to delete this material? This action cannot be undone. +
+