diff --git a/class_manager.js b/class_manager.js new file mode 100644 index 0000000..6ca3629 --- /dev/null +++ b/class_manager.js @@ -0,0 +1,85 @@ +// class_manager.js – Handles teacher class sessions and progress aggregation + +const CLASS_SESSIONS_KEY = 'learnsphere_class_sessions_v1'; + +function _loadClasses() { + try { + const raw = localStorage.getItem(CLASS_SESSIONS_KEY); + return raw ? JSON.parse(raw) : []; + } catch (e) { + console.warn('LearnSphere: Failed to load class sessions', e); + return []; + } +} + +function _saveClasses(classes) { + try { + localStorage.setItem(CLASS_SESSIONS_KEY, JSON.stringify(classes)); + } catch (e) { + console.warn('LearnSphere: Failed to save class sessions', e); + } +} + +function generateInviteCode(length = 6) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let code = ''; + for (let i = 0; i < length; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return code; +} + +function createClassSession({ name, topics = [], dueDate }) { + const classes = _loadClasses(); + const id = 'cls_' + Date.now(); + const inviteCode = generateInviteCode(); + const newClass = { + id, + name, + inviteCode, + topics, + dueDate: dueDate || null, + attempts: [], // each { studentId, quizId, score } + createdAt: new Date().toISOString() + }; + classes.push(newClass); + _saveClasses(classes); + return newClass; +} + +function recordClassAttempt(classId, { studentId = 'anonymous', quizId, score }) { + if (!classId) return; + const classes = _loadClasses(); + const cls = classes.find(c => c.id === classId); + if (!cls) return; + cls.attempts.push({ studentId, quizId, score, timestamp: Date.now() }); + _saveClasses(classes); +} + +function getClassStats(classId) { + const classes = _loadClasses(); + const cls = classes.find(c => c.id === classId); + if (!cls) return null; + const total = cls.attempts.length; + const avgScore = total ? (cls.attempts.reduce((a, b) => a + b.score, 0) / total).toFixed(2) : '-'; + const bestScore = total ? Math.max(...cls.attempts.map(a => a.score)).toFixed(2) : '-'; + return { ...cls, totalAttempts: total, avgScore, bestScore }; +} + +function getAllClassStats() { + const classes = _loadClasses(); + return classes.map(c => { + const total = c.attempts.length; + const avgScore = total ? (c.attempts.reduce((a, b) => a + b.score, 0) / total).toFixed(2) : '-'; + const bestScore = total ? Math.max(...c.attempts.map(a => a.score)).toFixed(2) : '-'; + return { id: c.id, name: c.name, inviteCode: c.inviteCode, totalAttempts: total, avgScore, bestScore }; + }); +} + +// Expose globally for other modules +window.classManager = { + createClassSession, + recordClassAttempt, + getClassStats, + getAllClassStats +}; diff --git a/exportProgress.js b/exportProgress.js index e44f6b4..f8efb6d 100644 --- a/exportProgress.js +++ b/exportProgress.js @@ -374,12 +374,80 @@ a.click(); a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 5000); } + // Snapshot utilities + async function generateSnapshot() { + const payload = buildProgressExportPayload({ roleContext: "learner" }); + const exportId = `snapshot_${Date.now()}_${Math.random().toString(16).slice(2)}`; + const encoder = new TextEncoder(); + const data = encoder.encode(JSON.stringify(payload)); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const payloadHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + const snapshot = { payload, payloadHash, generatedAt: new Date().toISOString() }; + localStorage.setItem(`snapshot_${exportId}`, JSON.stringify(snapshot)); + const index = JSON.parse(localStorage.getItem('snapshotIndex') || '[]'); + index.push({ exportId, generatedAt: snapshot.generatedAt, payloadHash }); + localStorage.setItem('snapshotIndex', JSON.stringify(index)); + return exportId; + } + + function downloadSnapshotAsImage(exportId) { + const snapshotStr = localStorage.getItem(`snapshot_${exportId}`); + if (!snapshotStr) { alert('Snapshot not found'); return; } + const snapshot = JSON.parse(snapshotStr); + const html = `
${JSON.stringify(snapshot.payload, null, 2)}
`; + const iframe = document.createElement('iframe'); + iframe.style.position = 'fixed'; iframe.style.right = '0'; iframe.style.bottom = '0'; iframe.style.width = '0'; iframe.style.height = '0'; + document.body.appendChild(iframe); + iframe.contentDocument.open(); iframe.contentDocument.write(html); iframe.contentDocument.close(); + setTimeout(() => { + html2canvas(iframe.contentDocument.body).then(canvas => { + const link = document.createElement('a'); + link.href = canvas.toDataURL('image/png'); + link.download = `${exportId}.png`; + link.click(); + iframe.remove(); + }); + }, 500); + } + + function downloadSnapshotAsPDF(exportId) { + const snapshotStr = localStorage.getItem(`snapshot_${exportId}`); + if (!snapshotStr) { alert('Snapshot not found'); return; } + const snapshot = JSON.parse(snapshotStr); + const html = `
${JSON.stringify(snapshot.payload, null, 2)}
`; + const iframe = document.createElement('iframe'); + iframe.style.position = 'fixed'; iframe.style.right = '0'; iframe.style.bottom = '0'; iframe.style.width = '0'; iframe.style.height = '0'; + document.body.appendChild(iframe); + iframe.contentDocument.open(); iframe.contentDocument.write(html); iframe.contentDocument.close(); + setTimeout(() => { + html2canvas(iframe.contentDocument.body).then(canvas => { + const imgData = canvas.toDataURL('image/png'); + const pdf = new jspdf.jsPDF(); + const imgProps = pdf.getImageProperties(imgData); + const pdfWidth = pdf.internal.pageSize.getWidth(); + const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; + pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight); + pdf.save(`${exportId}.pdf`); + iframe.remove(); + }); + }, 500); + } + + function listSnapshots() { + return JSON.parse(localStorage.getItem('snapshotIndex') || '[]'); + } + window.exportProgress = { buildProgressExportPayload, downloadJson, - }; -})(); + generateSnapshot, + downloadSnapshotAsImage, + downloadSnapshotAsPDF, + listSnapshots + };})(); diff --git a/my_progress.html b/my_progress.html index da99ddb..263b028 100644 --- a/my_progress.html +++ b/my_progress.html @@ -217,10 +217,13 @@

Export Progress

+
+ +