diff --git a/badges.css b/badges.css new file mode 100644 index 0000000..588eead --- /dev/null +++ b/badges.css @@ -0,0 +1,2 @@ +/* Optional future shared badge styling; current implementation uses inline styles in badges.js */ + diff --git a/badges.js b/badges.js new file mode 100644 index 0000000..d298dfa --- /dev/null +++ b/badges.js @@ -0,0 +1,253 @@ +/* + * badges.js β€” Unified β€œAchievements & Badges” system (Milestones) + * + * Computes milestones from existing quiz stats stored by quizProgress.js: + * localStorage key: learnsphere_quiz_progress_v1 + * + * Badges are persisted once unlocked in: + * localStorage key: learnsphere_achievements_v1 + */ + +(function () { + const QUIZ_PROGRESS_KEY = "learnsphere_quiz_progress_v1"; // for visibility in devtools + const ACHIEVEMENTS_KEY = "learnsphere_achievements_v1"; + + const BADGES = [ + { + id: "first_quiz_attempt", + title: "First quiz attempt", + description: "Complete your first quiz.", + icon: "🏁", + getProgress: (stats) => { + const firstAttempt = (stats.attemptCount || 0) >= 1; + return { + unlocked: firstAttempt, + progressText: firstAttempt ? "Unlocked" : "0/1" + }; + } + }, + { + id: "five_topics_completed", + title: "5 topics completed", + description: "Attempt quizzes in at least 5 topics.", + icon: "πŸ“š", + getProgress: (stats) => { + const target = 5; + const done = stats.topicAttemptedCount || 0; + return { + unlocked: done >= target, + progressText: `${Math.min(done, target)}/${target}` + }; + } + }, + { + id: "seven_day_streak", + title: "7-day practice streak", + description: "Practice every day for 7 days.", + icon: "πŸ”₯", + getProgress: (stats) => { + const target = 7; + const done = stats.currentStreak || 0; + return { + unlocked: done >= target, + progressText: `${Math.min(done, target)}/${target}` + }; + } + }, + { + id: "ninety_percent_accuracy", + title: "90%+ accuracy", + description: "Maintain 90% accuracy across your attempts.", + icon: "🎯", + getProgress: (stats) => { + const target = 0.9; + const total = stats.overallTotalAnswers || 0; + const acc = stats.overallAccuracy; + const unlocked = typeof acc === "number" && acc >= target && total > 0; + + // For progress text, show either current acc or 0/1. + let progressText; + if (total <= 0 || typeof acc !== "number") { + progressText = "No attempts"; + } else { + progressText = `${Math.round(acc * 100)}%`; + } + + return { + unlocked, + progressText + }; + } + } + ]; + + function loadAchievements() { + try { + const raw = localStorage.getItem(ACHIEVEMENTS_KEY); + if (!raw) return { unlocked: {} }; + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== "object") return { unlocked: {} }; + if (!parsed.unlocked || typeof parsed.unlocked !== "object") parsed.unlocked = {}; + return parsed; + } catch { + return { unlocked: {} }; + } + } + + function saveAchievements(ach) { + try { + localStorage.setItem(ACHIEVEMENTS_KEY, JSON.stringify(ach)); + } catch (e) { + console.warn("LearnSphere: Could not save achievements.", e); + } + } + + function safeNumber(n) { + return typeof n === "number" && !Number.isNaN(n) ? n : null; + } + + function buildStatsFromQuizProgress() { + if (!window.quizProgress) { + return { + attemptCount: 0, + topicAttemptedCount: 0, + currentStreak: 0, + overallAccuracy: null, + overallTotalAnswers: 0 + }; + } + + // Streak + const streak = window.quizProgress.getStreak ? window.quizProgress.getStreak() : { currentStreak: 0 }; + + // Overall accuracy + const overall = window.quizProgress.getOverallAccuracy ? window.quizProgress.getOverallAccuracy() : { accuracy: null, total: 0 }; + + // Topic completion proxy: count of topics with at least 1 quiz attempt + const byTopic = window.quizProgress.getAllTopicStats ? window.quizProgress.getAllTopicStats() : {}; + const topics = window.quizProgress.QUIZ_TOPICS || []; + let topicAttemptedCount = 0; + for (const t of topics) { + const a = byTopic[t.id]; + const attempts = a?.attempts || 0; + if (attempts >= 1) topicAttemptedCount++; + } + + // Attempt count (best effort): derived from topic stats totals + // (quizProgress stores attempt list, but we don't have direct getter) + // So estimate by summing per-topic attempts. + let attemptCount = 0; + for (const tId of Object.keys(byTopic || {})) { + attemptCount += (byTopic[tId]?.attempts || 0); + } + + return { + attemptCount, + topicAttemptedCount, + currentStreak: safeNumber(streak?.currentStreak) ?? 0, + overallAccuracy: overall?.accuracy == null ? null : safeNumber(overall.accuracy), + overallTotalAnswers: safeNumber(overall?.total) ?? 0 + }; + } + + function unlockNewBadges(ach, stats) { + let changed = false; + + for (const badge of BADGES) { + const already = !!ach.unlocked[badge.id]; + const prog = badge.getProgress(stats); + if (!already && prog.unlocked) { + ach.unlocked[badge.id] = { unlockedAt: new Date().toISOString() }; + changed = true; + } + } + + if (changed) saveAchievements(ach); + return ach; + } + + function badgeCardHTML(badge, unlocked, progressText) { + const dim = unlocked ? "" : "opacity:0.55; filter: grayscale(0.3);"; + const border = unlocked ? "border-color: rgba(102,252,241,0.55);" : "border-color: rgba(255,255,255,0.12);"; + const shadow = unlocked ? "0 10px 26px rgba(102,252,241,0.16)" : "none"; + + return ` +
+
+ +
+
${badge.title}
+
${badge.description}
+
+
+
+ ${unlocked ? `Unlocked βœ“` : `Locked β€’ ${progressText}`} +
+
+ `; + } + + function ensureStyles(containerEl) { + if (!containerEl) return; + if (containerEl.dataset.badgesStylesApplied === "true") return; + containerEl.dataset.badgesStylesApplied = "true"; + + // Minimal inline styles so we don't touch global CSS too much. + const style = document.createElement("style"); + style.textContent = ` + .badges-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-top: 10px; + } + @media (max-width: 560px) { .badges-grid { grid-template-columns: 1fr; } } + + .badge-card { + background: rgba(255,255,255,0.04); + border: 1px solid rgba(255,255,255,0.12); + border-radius: 12px; + padding: 12px; + transition: transform 0.15s ease, border-color 0.15s ease, opacity 0.15s ease; + } + .badge-card:hover { transform: translateY(-2px); } + .badge-top { display:flex; gap:10px; align-items:flex-start; } + .badge-icon { width:26px; text-align:center; } + + `; + document.head.appendChild(style); + } + + function renderBadges(containerId) { + const container = document.getElementById(containerId); + if (!container) return; + + ensureStyles(container); + + const stats = buildStatsFromQuizProgress(); + const ach = loadAchievements(); + unlockNewBadges(ach, stats); + + const unlockedSet = ach.unlocked || {}; + + container.innerHTML = ` +
+ ${BADGES.map((badge) => { + const unlocked = !!unlockedSet[badge.id]; + const prog = badge.getProgress(stats); + const progressText = prog.progressText || ""; + return `
${badgeCardHTML(badge, unlocked, progressText)}
`; + }).join("")} +
+
+ Badges are based on your quiz attempts, streak, and accuracy. +
+ `; + } + + window.achievements = { + BADGES, + renderBadges + }; +})(); + diff --git a/home.html b/home.html index 3ea53b5..6fe05ad 100644 --- a/home.html +++ b/home.html @@ -146,6 +146,8 @@ } + + @@ -183,8 +185,18 @@

πŸ“Š Your Study Progress

+ +
+

πŸ… Achievements & Badges

+
+
+ Unlock milestones based on quiz attempts, practice streak, and accuracy. +
+
+
+

πŸ€– Ask Your AI Tutor

πŸ€– Ask Your AI Tutor
+ + + + diff --git a/home.js b/home.js index 4993ba6..7df5b7e 100644 --- a/home.js +++ b/home.js @@ -25,7 +25,15 @@ function escapeHTML(str) { return div.innerHTML; } +// ── Achievements (badges.js) ─────────────────────────────────────────────── +document.addEventListener("DOMContentLoaded", () => { + if (window.achievements?.renderBadges) { + window.achievements.renderBadges("badgesContainerHome"); + } +}); + // ── Chatbot ─────────────────────────────────────────────────────────────────── + /** * Appends a message bubble to the chat box. * diff --git a/index.html b/index.html index a24e633..1bb6f94 100644 --- a/index.html +++ b/index.html @@ -26,6 +26,9 @@ + + + @@ -95,6 +98,17 @@

Real results.

+ + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..dd65576 --- /dev/null +++ b/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "LearnSphere", + "short_name": "LearnSphere", + "start_url": "/index.html", + "scope": "/", + "display": "standalone", + "background_color": "#0f1115", + "theme_color": "#0f1115", + "icons": [ + { + "src": "/student.png", + "sizes": "256x256", + "type": "image/png" + } + ] +} + diff --git a/my_progress.html b/my_progress.html index 242acda..02b6b37 100644 --- a/my_progress.html +++ b/my_progress.html @@ -8,7 +8,10 @@ + +