Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions badges.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,23 @@
progressText
};
}
},
{
id: "daily_goal_hero",
title: "Daily Goal Hero",
description: "Complete your daily learning goal today.",
icon: "🔥",
getProgress: (stats) => {
const unlocked = stats.dailyGoalCompleted || false;
return {
unlocked,
progressText: unlocked ? "Unlocked" : "0/1"
};
}
}
];


function loadAchievements() {
try {
const raw = localStorage.getItem(ACHIEVEMENTS_KEY);
Expand Down Expand Up @@ -182,7 +196,40 @@
}

// Streak
const streak = window.quizProgress.getStreak ? window.quizProgress.getStreak() : { currentStreak: 0 };
let currentStreak = 0;
if (window.studyProgress && typeof window.studyProgress.loadStreakState === "function") {
currentStreak = window.studyProgress.loadStreakState().currentStreak || 0;
} else if (window.quizProgress && typeof window.quizProgress.getStreak === "function") {
const streak = window.quizProgress.getStreak();
currentStreak = streak.currentStreak || 0;
}

// Daily Goal
let dailyGoalCompleted = false;
if (window.studyProgress && typeof window.studyProgress.loadStreakState === "function") {
const state = window.studyProgress.loadStreakState();
const qDone = state.dailyGoalProgress.quizzesCompleted || 0;
const rDone = state.dailyGoalProgress.questionsReviewed || 0;
if (qDone >= 1 || rDone >= 10) {
dailyGoalCompleted = true;
}
} else {
const STREAK_KEY = "learnsphere_streak_state_v1";
try {
const raw = localStorage.getItem(STREAK_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && parsed.dailyGoalProgress) {
const qDone = parsed.dailyGoalProgress.quizzesCompleted || 0;
const rDone = parsed.dailyGoalProgress.questionsReviewed || 0;
if (qDone >= 1 || rDone >= 10) {
dailyGoalCompleted = true;
}
}
}
} catch (e) {}
}


// Overall accuracy
const overall = window.quizProgress.getOverallAccuracy ? window.quizProgress.getOverallAccuracy() : { accuracy: null, total: 0 };
Expand Down Expand Up @@ -230,14 +277,16 @@
return {
attemptCount,
topicAttemptedCount,
currentStreak: safeNumber(streak?.currentStreak) ?? 0,
currentStreak: safeNumber(currentStreak) ?? 0,
dailyGoalCompleted,
overallAccuracy: overall?.accuracy == null ? null : safeNumber(overall.accuracy),
overallTotalAnswers: safeNumber(overall?.total) ?? 0,
hasWeekendAttempt,
hasWeeklyAttempt
};
}


function ensureToastStyles() {
if (document.getElementById("badge-toast-styles")) return;
const style = document.createElement("style");
Expand Down
177 changes: 170 additions & 7 deletions dashboardProgress.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,44 @@
}

function renderKpis(root) {
const streak = window.quizProgress?.getStreak?.();
const overall = window.quizProgress?.getOverallAccuracy?.();
let currentStreak = 0;
let streakMetaText = "No practice yet.";
let dailyGoalPct = 0;
let dailyGoalMetaText = "Complete 1 quiz or review 10 questions.";

if (window.studyProgress && typeof window.studyProgress.loadStreakState === "function") {
const streakState = window.studyProgress.loadStreakState();
currentStreak = streakState.currentStreak || 0;
const last = streakState.lastActiveDate;
streakMetaText = last ? `Last active: ${last}` : "No activity yet.";

const qDone = streakState.dailyGoalProgress.quizzesCompleted || 0;
const rDone = streakState.dailyGoalProgress.questionsReviewed || 0;
const quizGoalProgress = qDone / 1;
const reviewGoalProgress = rDone / 10;
dailyGoalPct = Math.min(100, Math.round(Math.max(quizGoalProgress, reviewGoalProgress) * 100));
dailyGoalMetaText = dailyGoalPct >= 100
? `🎉 Goal Achieved! (${qDone}/1 quiz, ${rDone}/10 reviewed)`
: `${qDone}/1 quiz, ${rDone}/10 reviewed today`;
} else {
const streak = window.quizProgress?.getStreak?.();
currentStreak = streak?.currentStreak || 0;
const last = streak?.lastPracticeDate;
streakMetaText = last ? `Last practice: ${last}` : "No practice yet.";
}

const streakValue = root.querySelector("#streakValue");
const streakMeta = root.querySelector("#streakMeta");

if (streakValue) streakValue.textContent = String(streak?.currentStreak || 0);
if (streakMeta) {
const last = streak?.lastPracticeDate;
streakMeta.textContent = last ? `Last practice: ${last}` : "No practice yet.";
}
if (streakValue) streakValue.textContent = String(currentStreak);
if (streakMeta) streakMeta.textContent = streakMetaText;

const dailyGoalValue = root.querySelector("#dailyGoalValue");
const dailyGoalMeta = root.querySelector("#dailyGoalMeta");
if (dailyGoalValue) dailyGoalValue.textContent = `${dailyGoalPct}%`;
if (dailyGoalMeta) dailyGoalMeta.textContent = dailyGoalMetaText;

const overall = window.quizProgress?.getOverallAccuracy?.();
const overallAccuracyValue = root.querySelector("#overallAccuracyValue");
const overallAccuracyMeta = root.querySelector("#overallAccuracyMeta");

Expand Down Expand Up @@ -214,6 +240,142 @@
});
}

function renderMastery(root) {
const masteryListEl = root.querySelector("#masterySkillsList");
const weakSkillsEl = root.querySelector("#weakSkillsRecommendations");
if (!masteryListEl || !weakSkillsEl) return;

const mastery = window.quizProgress.getMasteryStats();
const taxonomy = window.quizProgress.SKILL_TAXONOMY || {};

const skillMap = new Map();
for (const [qText, tax] of Object.entries(taxonomy)) {
if (!skillMap.has(tax.skillId)) {
skillMap.set(tax.skillId, {
skillId: tax.skillId,
label: tax.label,
topicId: tax.topicId,
quizUrl: tax.quizUrl,
attempts: 0,
correct: 0
});
}
}

let totalAttempts = 0;
for (const [sId, m] of Object.entries(mastery)) {
totalAttempts += m.attempts || 0;
if (skillMap.has(sId)) {
const entry = skillMap.get(sId);
entry.attempts = m.attempts || 0;
entry.correct = m.correct || 0;
} else {
skillMap.set(sId, {
skillId: sId,
label: sId.replace("-general", " General Concepts"),
topicId: sId.split("-")[0],
quizUrl: null,
attempts: m.attempts || 0,
correct: m.correct || 0
});
}
}

const skillsArray = Array.from(skillMap.values());

skillsArray.sort((a, b) => {
if (a.attempts !== b.attempts) {
return b.attempts - a.attempts;
}
return a.label.localeCompare(b.label);
});

if (totalAttempts === 0) {
masteryListEl.innerHTML = `<div class="muted" style="padding: 12px; text-align: center; font-size:13px; color:rgba(255,255,255,0.72)">No mastery data recorded yet.</div>`;
} else {
masteryListEl.innerHTML = "";
skillsArray.forEach(s => {
if (s.attempts === 0) return;

const accuracy = s.attempts > 0 ? s.correct / s.attempts : 0;
const pctValue = Math.round(accuracy * 100);

let barColor = "#ef4444";
if (pctValue >= 80) {
barColor = "#66fcf1";
} else if (pctValue >= 50) {
barColor = "#f59e0b";
}

const row = document.createElement("div");
row.className = "topic-row";
row.style.marginBottom = "8px";
row.style.display = "flex";
row.style.alignItems = "center";
row.style.justifyContent = "space-between";
row.style.padding = "10px 12px";
row.style.borderRadius = "10px";
row.style.background = "rgba(255,255,255,0.03)";
row.style.border = "1px solid rgba(255,255,255,0.06)";
row.innerHTML = `
<div style="min-width: 210px; font-weight: 600; font-size: 0.9rem; color: #fff;">${s.label}</div>
<div class="bar" aria-label="mastery bar" style="flex: 1; height: 8px; background: rgba(255, 255, 255, 0.05); margin: 0 12px; border-radius: 999px; overflow: hidden;">
<i style="display: block; height: 100%; width:${pctValue}%; background:${barColor}; border-radius: 999px;"></i>
</div>
<div style="min-width: 90px; text-align:right; font-size: 0.9rem;">
<div style="font-weight:700; color: ${barColor};">${pctValue}%</div>
<div class="muted" style="font-size:11px; color:rgba(255,255,255,0.72);">${s.correct}/${s.attempts} correct</div>
</div>
`;
masteryListEl.appendChild(row);
});

if (masteryListEl.children.length === 0) {
masteryListEl.innerHTML = `<div class="muted" style="padding: 12px; text-align: center; font-size:13px; color:rgba(255,255,255,0.72)">No concept stats available yet.</div>`;
}
}

const weakSkills = window.quizProgress.getWeakestSkills({ limit: 3 });

weakSkillsEl.innerHTML = "";
if (!weakSkills || weakSkills.length === 0) {
weakSkillsEl.innerHTML = `<div class="muted" style="font-size:13px; color:rgba(255,255,255,0.72)">No weak concepts identified yet.</div>`;
} else {
weakSkills.forEach(ws => {
const accuracyPct = ws.attempts > 0 ? `${Math.round(ws.accuracy * 100)}%` : "Not attempted";

const card = document.createElement("div");
card.style.background = "rgba(255, 255, 255, 0.02)";
card.style.border = "1px solid rgba(255, 255, 255, 0.06)";
card.style.borderRadius = "8px";
card.style.padding = "12px 14px";
card.style.display = "flex";
card.style.justifyContent = "space-between";
card.style.alignItems = "center";

let statusColor = "#ff5e5e";
if (ws.attempts === 0) {
statusColor = "#a0aec0";
} else if (ws.accuracy >= 0.8) {
statusColor = "#66fcf1";
} else if (ws.accuracy >= 0.5) {
statusColor = "#f59e0b";
}

card.innerHTML = `
<div style="display:flex; flex-direction:column; gap:4px; text-align:left;">
<div style="font-weight: 600; font-size: 0.95rem; color: #fff;">${ws.label}</div>
<div style="font-size: 0.8rem; color: rgba(255, 255, 255, 0.65);">
Accuracy: <span style="font-weight:bold; color: ${statusColor}">${accuracyPct}</span>
${ws.attempts > 0 ? `(${ws.attempts} attempts)` : ""}
</div>
</div>
`;
weakSkillsEl.appendChild(card);
});
}
}

function renderAll(root) {
// Guard: quizProgress must be loaded
if (!window.quizProgress) {
Expand All @@ -226,6 +388,7 @@
renderAccuracyChart(root);
renderRecommendations(root);
renderTopicStats(root);
renderMastery(root);
}

function initByRole() {
Expand Down
23 changes: 22 additions & 1 deletion home.html
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,15 @@
<body>

<div class="dashboard-container">
<h1 class="title">Welcome to LearnSphere</h1>
<div style="display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 10px; flex-wrap: wrap;">
<h1 class="title" style="margin: 0;">Welcome to LearnSphere</h1>
<div id="headerStreakBadge" style="display: inline-flex; align-items: center; gap: 6px; background: rgba(255, 69, 0, 0.1); border: 1px solid rgba(255, 69, 0, 0.35); padding: 6px 14px; border-radius: 20px; font-weight: 700; color: #ff4500; font-size: 0.9rem; vertical-align: middle; box-shadow: 0 4px 15px rgba(255, 69, 0, 0.15); transition: transform 0.2s;">
🔥 <span id="headerStreakCount">0</span> Day Streak
</div>
</div>
<p class="description">Choose a subject to start learning:</p>


<div class="button-container" role="navigation" aria-label="Subject navigation">
<a href="sub/maths.html" class="button">📐 Maths</a>
<a href="sub/physics.html" class="button">⚛️ Physics</a>
Expand Down Expand Up @@ -299,6 +305,21 @@ <h2 id="badges-heading">🏅 Achievements & Badges</h2>
<p id="streakNextMilestoneText" style="font-size: 0.8rem; color: var(--text-muted); margin: 8px 0 0 0;"></p>
</div>

<!-- Daily Goal Progress card -->
<div id="dailyGoalCard" style="background: var(--progress-item-bg); border-radius: 10px; padding: 16px; margin-bottom: 20px; border-left: 4px solid var(--accent-color); text-align: left;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 1.05rem; font-weight: 700; color: var(--accent-color); display: flex; align-items: center; gap: 6px;">
<span>🎯</span> Daily Learning Goal
</span>
<span id="dailyGoalPercentText" style="font-size: 1.1rem; font-weight: 800; color: #fff;">0%</span>
</div>
<div class="progress-bar-track" style="margin-top: 12px; height: 10px; background: rgba(255,255,255,0.06); border-radius: 5px; overflow: hidden;">
<div id="dailyGoalProgressBarFill" style="height: 100%; width: 0%; background: linear-gradient(90deg, #66fcf1, #45a29e); border-radius: 5px; transition: width 0.5s ease;"></div>
</div>
<p id="dailyGoalStatusText" style="font-size: 0.8rem; color: var(--text-muted); margin: 8px 0 0 0;">Complete 1 quiz or review 10 questions to hit your goal today!</p>
</div>


<div id="badgesContainerHome"></div>
<div style="font-size:0.85rem; color: var(--text-muted); margin-top:10px;">
Unlock milestones based on quiz attempts, practice streak, and accuracy.
Expand Down
40 changes: 32 additions & 8 deletions home.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ document.addEventListener("DOMContentLoaded", () => {
window.achievements.renderBadges("badgesContainerHome");
}

// Render Daily Streak stats
if (window.quizProgress && typeof window.quizProgress.getStreak === "function") {
const streak = window.quizProgress.getStreak();
const days = streak.currentStreak || 0;
// Render Daily Streak & Daily Goal stats
if (window.studyProgress && typeof window.studyProgress.loadStreakState === "function") {
const streakState = window.studyProgress.loadStreakState();
const days = streakState.currentStreak || 0;

// Update streak elements
const headerCountEl = document.getElementById("headerStreakCount");
if (headerCountEl) headerCountEl.textContent = String(days);

const daysEl = document.getElementById("streakDaysText");
const barFillEl = document.getElementById("streakProgressBarFill");
Expand All @@ -51,16 +55,13 @@ document.addEventListener("DOMContentLoaded", () => {
const pct = Math.min(100, Math.round((days / nextMilestone) * 100));
if (barFillEl) barFillEl.style.width = `${pct}%`;

// Streak dates are stored in quizProgress.js as local YYYY-MM-DD.
// Avoid toLocaleDateString formatting mismatches across browsers/timezones.
const d = new Date();
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
const todayStr = `${yyyy}-${mm}-${dd}`;
const practicedToday = streak.lastPracticeDate === todayStr;
const practicedToday = streakState.lastActiveDate === todayStr;


if (descEl) {
if (practicedToday) {
descEl.innerHTML = `🔥 Streak safe for today! Practice tomorrow to build toward your <strong>${nextMilestone}-day</strong> milestone.`;
Expand All @@ -70,6 +71,29 @@ document.addEventListener("DOMContentLoaded", () => {
descEl.innerHTML = `🏁 Start a quiz today to begin your daily practice streak! Next milestone: <strong>3 days</strong>.`;
}
}

// Render Daily Goal card
const dailyGoalPercentEl = document.getElementById("dailyGoalPercentText");
const dailyGoalBarFillEl = document.getElementById("dailyGoalProgressBarFill");
const dailyGoalStatusEl = document.getElementById("dailyGoalStatusText");

const qDone = streakState.dailyGoalProgress.quizzesCompleted || 0;
const rDone = streakState.dailyGoalProgress.questionsReviewed || 0;

const quizGoalProgress = qDone / 1;
const reviewGoalProgress = rDone / 10;
const goalPercent = Math.min(100, Math.round(Math.max(quizGoalProgress, reviewGoalProgress) * 100));

if (dailyGoalPercentEl) dailyGoalPercentEl.textContent = `${goalPercent}%`;
if (dailyGoalBarFillEl) dailyGoalBarFillEl.style.width = `${goalPercent}%`;

if (dailyGoalStatusEl) {
if (goalPercent >= 100) {
dailyGoalStatusEl.innerHTML = `🎉 <strong>Daily Goal Achieved!</strong> Great job keeping up your learning momentum today! (${qDone}/1 quiz, ${rDone}/10 questions reviewed)`;
} else {
dailyGoalStatusEl.textContent = `Completed ${qDone}/1 quiz or reviewed ${rDone}/10 questions today. Keep going!`;
}
}
}

// Check for explain_mistake redirect
Expand Down
Loading
Loading