Skip to content

Commit 120a227

Browse files
authored
Merge pull request #86 from CrackCode-dev/Ama
feat(careermap): fix chapter unlock, button responsiveness & completion flow with progress
2 parents 70a99c0 + 7d54976 commit 120a227

6 files changed

Lines changed: 118 additions & 99 deletions

File tree

crackcode/client/src/components/careermap/QuizCard/index.jsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react";
1+
import { useState, useEffect, useRef } from "react";
22
import ContentCard from "../../ui/Card";
33
import Button from "../../ui/Button";
44
import AnswerOptions from "./AnswerOptions";
@@ -10,6 +10,37 @@ export default function QuizCard({ variant = "mcq", question, index, isLast = fa
1010
const [fillValue, setFillValue] = useState("");
1111
const [revealed, setRevealed] = useState(false);
1212

13+
const latestRef = useRef({});
14+
latestRef.current = { revealed, selected, question, onNext, variant, fillValue };
15+
16+
// Handle Enter key for both MCQ and fill-in-the-blank questions
17+
useEffect(() => {
18+
const handleKeyDown = (e) => {
19+
if (e.key !== "Enter") return;
20+
const { revealed, selected, question, onNext, variant, fillValue } = latestRef.current;
21+
22+
if (variant === "mcq" && revealed && question) {
23+
const correct = selected === question.correct;
24+
onNext?.({ correct });
25+
}
26+
27+
if (variant === "fill" && !revealed && fillValue.trim()) {
28+
document.querySelector("[data-check-btn]")?.click();
29+
}
30+
31+
if (variant === "fill" && revealed) {
32+
const answers = question.answer?.split(",").map(a => a.trim().toLowerCase()) || [];
33+
const normalized = fillValue.trim().toLowerCase();
34+
const correct = answers.some(ans =>
35+
ans === normalized ||
36+
(ans.split(/\s+/).length === 2 && ans.split(/\s+/).includes(normalized))
37+
);
38+
onNext?.({ correct });
39+
}
40+
};
41+
window.addEventListener("keydown", handleKeyDown);
42+
return () => window.removeEventListener("keydown", handleKeyDown);
43+
}, []);
1344

1445
if (!question) return null;
1546

@@ -99,6 +130,8 @@ export default function QuizCard({ variant = "mcq", question, index, isLast = fa
99130
revealed={revealed}
100131
isCorrect={fillIsCorrect}
101132
correctAnswer={question.answer}
133+
onReveal={handleRevealFill}
134+
onNext={handleNext}
102135
/>
103136
)}
104137

@@ -112,7 +145,7 @@ export default function QuizCard({ variant = "mcq", question, index, isLast = fa
112145

113146
{/* Fill: Check answer */}
114147
{variant === "fill" && !revealed && (
115-
<Button variant="primary" size="lg" fullWidth disabled={!canReveal} onClick={handleRevealFill}>
148+
<Button data-check-btn variant="primary" size="lg" fullWidth disabled={!canReveal} onClick={handleRevealFill}>
116149
Check Answer
117150
</Button>
118151
)}

crackcode/client/src/pages/careermap/CareerChapterSelection.jsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const CareerChapterSelectionPage = () => {
2727
const [passedChapters, setPassedChapters] = useState({});
2828
const [chapterScores, setChapterScores] = useState({});
2929
const [questionCounts, setQuestionCounts] = useState({});
30+
const [loaded, setLoaded] = useState(false);
3031

3132
useEffect(() => {
3233
// Fetch progress from DB for this career
@@ -42,26 +43,24 @@ const CareerChapterSelectionPage = () => {
4243
scoreMap[ch.chapterId] = ch.easyScore + ch.mediumScore + ch.hardScore;
4344
});
4445
} else {
45-
// No DB data — fall back to localStorage
4646
baseChapters.forEach((ch) => {
47-
passedMap[ch.id] =
48-
localStorage.getItem(`${careerId}_${ch.id}_passed`) === "true" ||
49-
localStorage.getItem(`${careerId}_${ch.id}_completed`) === "true";
47+
passedMap[ch.id] = false;
5048
});
5149
}
5250

5351
setPassedChapters(passedMap);
5452
setChapterScores(scoreMap);
53+
setLoaded(true);
5554
})
5655
.catch(() => {
57-
// DB fetch failed — fall back to localStorage for all chapters
56+
// DB fetch failed — show all chapters as locked
5857
const fallback = {};
5958
baseChapters.forEach((ch) => {
60-
fallback[ch.id] =
61-
localStorage.getItem(`${careerId}_${ch.id}_passed`) === "true" ||
62-
localStorage.getItem(`${careerId}_${ch.id}_completed`) === "true";
59+
fallback[ch.id] = false
60+
6361
});
6462
setPassedChapters(fallback);
63+
setLoaded(true);
6564
});
6665

6766
// Fetch live question counts per chapter from the API
@@ -76,7 +75,7 @@ const CareerChapterSelectionPage = () => {
7675

7776
}, [careerId]);
7877

79-
// Lock/unlock chapters based on previous chapter completion in localStorage
78+
// Lock/unlock chapters based on previous chapter completion from DB
8079
const chapters = baseChapters.map((chapter, index) => ({
8180
...chapter,
8281
isUnlocked: index === 0 ? true : !!passedChapters[baseChapters[index - 1].id],
@@ -137,6 +136,19 @@ const CareerChapterSelectionPage = () => {
137136
</div>
138137
</div>
139138

139+
{/* Show Career completion banner*/}
140+
{loaded && chapters.every((ch) => passedChapters[ch.id]) && (
141+
<div className="w-full bg-green-100 border border-green-500 rounded-2xl px-6 py-6 flex flex-col items-center gap-2 text-center mb-10">
142+
<div className="text-4xl">🎉</div>
143+
<p className="text-green-500 text-xl font-extrabold">Career Completed!</p>
144+
<p className="text-black text-sm">
145+
You've completed all 4 chapters in the{" "}
146+
<span className="text-green-500 font-semibold">{title}</span> career path.
147+
Stay focused and never give up!
148+
</p>
149+
</div>
150+
)}
151+
140152
{/* Roadmap List */}
141153
<div className="flex flex-col">
142154
{chapters.map((chapter, index) => {

crackcode/client/src/pages/careermap/CareerQuizPage.jsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,19 @@ export default function CareerQuizPage() {
116116
}
117117

118118
if (currentQ + 1 >= total) {
119-
// Require more than 12 correct answers (i.e., at least 13) to pass and unlock next chapter
119+
// Require more than 12 correct answers (at least 13) to pass and unlock next chapter
120120
const passed = (newEasy + newMedium + newHard) > 12;
121121

122122
console.log("Quiz finished:", { careerId, chapterId, newEasy, newMedium, newHard, passed });
123123

124+
const finalPassed = passed ;
125+
124126
if (!progressSentRef.current) {
125127
progressSentRef.current = true;
126-
updateProgress(careerId, chapterId, newEasy, newMedium, newHard, passed)
128+
updateProgress(careerId, chapterId, newEasy, newMedium, newHard, finalPassed)
127129
.then((res) => console.log("✅ Progress saved:", res))
128130
.catch((err) => console.error("❌ Progress update failed:", err));
129131
}
130-
131-
if (passed) {
132-
localStorage.setItem(`${careerId}_${chapterId}_passed`, "true");
133-
}
134132
setFinished(true);
135133
} else {
136134
setCurrentQ((q) => q + 1);

crackcode/client/src/pages/careermap/Results.jsx

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,43 @@ import Button from "../../components/ui/Button";
22
import { useNavigate } from "react-router-dom";
33
import { useEffect, useState } from "react";
44
import { getChapterByCareerId } from "./CareerChapters";
5+
import { fetchProgress } from "../../services/api/careermapService";
56

67
export default function ResultsPage({ score, total, title, subtitle, careerId, currentChapterId, onRestart }) {
78
const navigate = useNavigate();
89
const [nextChapter, setNextChapter] = useState(null);
10+
const [allChaptersDone, setAllChaptersDone] = useState(false);
911
const [loaded, setLoaded] = useState(false);
1012

1113
// Check if current chapter is passed and if a next chapter exists
1214
useEffect(() => {
1315
const chapters = getChapterByCareerId(careerId);
1416
const currentIndex = chapters.findIndex((c) => c.id === currentChapterId);
1517
const nextIndex = currentIndex + 1;
16-
const currentPassed = localStorage.getItem(`${careerId}_${currentChapterId}_passed`) === "true";
17-
18-
// Only unlock next chapter if current is passed and next exists
19-
if (currentPassed && nextIndex < chapters.length) {
20-
setNextChapter(chapters[nextIndex]);
21-
} else {
22-
setNextChapter(null);
23-
}
24-
setLoaded(true);
25-
}, [careerId, currentChapterId]);
2618

27-
const isLastChapter = loaded && !nextChapter && score >= 8;
19+
// Fetch progress from DB to determine chapter unlock and completion state
20+
fetchProgress(careerId).then((progress) => {
21+
const passedMap = {};
22+
progress.chapters?.forEach((ch) => {
23+
passedMap[ch.chapterId] = ch.passed;
24+
});
25+
26+
const currentPassed = !!passedMap[currentChapterId];
27+
// Only unlock next chapter if current is passed and next exists
28+
if (currentPassed && nextIndex < chapters.length) {
29+
setNextChapter(chapters[nextIndex]);
30+
} else {
31+
setNextChapter(null);
32+
}
33+
34+
const allDone = chapters.every((ch) => !!passedMap[ch.id]);
35+
36+
// Update completion states
37+
setAllChaptersDone(allDone);
38+
setLoaded(true);
39+
}).catch(() => setLoaded(true));
40+
41+
}, [careerId, currentChapterId]);
2842

2943
// Navigate to next chapter
3044
const handleNextClick = () => {
@@ -40,7 +54,7 @@ export default function ResultsPage({ score, total, title, subtitle, careerId, c
4054
return (
4155
<div className="min-h-screen bg-(--bg) flex flex-col items-center px-6 py-8">
4256

43-
57+
4458

4559
<div className="w-full max-w-5xl text-center mb-4">
4660
<h1 className="text-4xl font-extrabold text-(--text) tracking-tight leading-tight">
@@ -93,21 +107,20 @@ export default function ResultsPage({ score, total, title, subtitle, careerId, c
93107
</div>
94108

95109
{/* Show unlock if next chapter is available */}
96-
{nextChapter && score >= 8 && (
97-
<div className="w-full bg-green-950/60 border border-green-500 rounded-2xl px-6 py-4 text-center">
98-
<p className="text-green-400 font-semibold">Chapter unlocked!</p>
110+
{loaded && nextChapter && (
111+
<div className="w-full bg-green-100 border border-green-500 rounded-2xl px-6 py-4 text-center">
112+
<p className="text-green-500 font-semibold">Chapter unlocked !</p>
99113
</div>
100114
)}
101115

102116
{/* Show completion message if last chapter is passed */}
103-
{isLastChapter && (
104-
<div className="w-full bg-green-950/40 border border-green-500 rounded-2xl px-6 py-6 flex flex-col items-center gap-2 text-center">
117+
{loaded && allChaptersDone && !nextChapter && (
118+
<div className="w-full bg-green-100 border border-green-500 rounded-2xl px-6 py-6 flex flex-col items-center gap-2 text-center">
105119
<div className="text-4xl">🎉</div>
106-
<p className="text-green-400 text-xl font-extrabold">Career Path Completed!</p>
107-
<p className="text-(--muted) text-sm">
120+
<p className="text-green-500 text-xl font-extrabold">All Chapters Completed!</p>
121+
<p className="text-black text-sm">
108122
You've successfully completed all chapters in the{" "}
109-
<span className="text-green-400 font-semibold">{title}</span> career path.
110-
Stay focused and never give up!
123+
<span className="text-green-500 font-semibold">{title}</span> career path.
111124
</p>
112125
</div>
113126
)}
@@ -117,8 +130,7 @@ export default function ResultsPage({ score, total, title, subtitle, careerId, c
117130
Try Again
118131
</Button>
119132

120-
{score < 8 ? (
121-
// Low score — show Back to Chapters
133+
{!loaded || (!nextChapter && !allChaptersDone) ? (
122134
<Button variant="outline" size="lg" fullWidth onClick={() => navigate(`/careermap/${careerId}`)}>
123135
Back to Chapters
124136
</Button>

crackcode/server/src/modules/Career Map/progress/progress.model.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ const progressSchema = new mongoose.Schema({
3838
mediumCompleted: { type: Boolean, default: false },
3939
hardCompleted: { type: Boolean, default: false },
4040

41-
totalQuestions: { type: Number, default: 60 }
41+
totalQuestions: { type: Number, default: 60 },
42+
careerRewarded: { type: Boolean, default: false } // True when user has received career completion reward
4243

4344
}, { timestamps: true });
4445

crackcode/server/src/modules/Career Map/progress/progress.service.js

Lines changed: 21 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Progress from "./progress.model.js";
22
import User from "../../auth/User.model.js";
3-
import { checkAndUnlockMultipleBadges } from "../../badges/badge.service.js";
43

54
// Helper = recompute overall totals from all chapters
65
const recomputeTotals = (chapters) => {
@@ -24,9 +23,9 @@ export const updateChapterProgress = async (userId, career, chapterId, easyScore
2423
easyScore = Math.max(0, parseInt(easyScore) || 0);
2524
mediumScore = Math.max(0, parseInt(mediumScore) || 0);
2625
hardScore = Math.max(0, parseInt(hardScore) || 0);
27-
26+
2827
const newTotal = easyScore + mediumScore + hardScore;
29-
28+
3029
// Sanity check: total score shouldn't exceed 100 (reasonable quiz limit)
3130
if (newTotal > 100) {
3231
throw new Error(`Invalid score: total (${newTotal}) exceeds maximum of 100. Possible cheating attempt.`);
@@ -101,73 +100,37 @@ export const updateChapterProgress = async (userId, career, chapterId, easyScore
101100
{ returnDocument: "after" }
102101
);
103102

104-
// AWARD XP/TOKENS only if newly passed (prevents duplicate rewards)
105-
let tokensAwarded = 0;
106-
let xpAwarded = 0;
107-
108-
if (passed && !previouslyPassed) {
109-
// First time passing: award for ALL correct answers
110-
tokensAwarded = newTotal;
111-
xpAwarded = newTotal * 3;
112-
103+
// Award XP based on first-time or retry pass
104+
if (passed) {
105+
const xp = previouslyPassed ? 5 : 10;
113106
try {
114107
await User.findByIdAndUpdate(userId, {
115-
$inc: {
116-
tokens: tokensAwarded,
117-
totalXP: xpAwarded,
118-
casesSolved: tokensAwarded
119-
}
108+
$inc: { totalXP: xp }
120109
});
121-
122-
// Record this in progress so we don't re-award on retry
123-
await Progress.updateOne(
124-
{ userId, career, "chapters.chapterId": chapterId },
125-
{ $set: { "chapters.$.rewardedQuestions": newTotal } }
126-
);
127-
console.log(`✅ First-time pass reward: ${tokensAwarded} tokens, ${xpAwarded} XP to user ${userId} for ${career}/${chapterId}`);
128-
129-
// Check and unlock relevant badges
130-
const badgesToCheck = ['beginner', 'cases_5', 'cases_10', 'cases_25'];
131-
const newlyUnlocked = await checkAndUnlockMultipleBadges(userId, badgesToCheck);
132-
if (newlyUnlocked.length > 0) {
133-
console.log(`✅ New badges unlocked: ${newlyUnlocked.join(', ')}`);
134-
}
110+
console.log(`✅ Chapter ${previouslyPassed ? 'retry' : 'first'} pass: +${xp} XP to user ${userId} for ${career}/${chapterId}`);
135111
} catch (err) {
136-
console.error(`❌ Failed to award first-time pass rewards for user ${userId}:`, err.message);
112+
console.error(`❌ Failed to award chapter XP:`, err.message);
137113
}
138-
} else if (passed && previouslyPassed && newTotal > previousRewarded) {
139-
// Already passed before: only award for NEW correct answers (improvement bonus)
140-
tokensAwarded = newTotal - previousRewarded;
141-
xpAwarded = tokensAwarded * 3;
142-
114+
}
115+
116+
// Award 1 token when all 4 chapters passed for the first time
117+
progress = await Progress.findOne({ userId, career });
118+
const allChaptersPassed = progress.chapters.length === 4 &&
119+
progress.chapters.every(ch => ch.passed);
120+
121+
if (allChaptersPassed && !progress.careerRewarded) {
143122
try {
144123
await User.findByIdAndUpdate(userId, {
145-
$inc: {
146-
tokens: tokensAwarded,
147-
totalXP: xpAwarded,
148-
casesSolved: tokensAwarded
149-
}
124+
$inc: { tokens: 1 }
150125
});
151-
152-
// Update rewardedQuestions to track the new high score
153126
await Progress.updateOne(
154-
{ userId, career, "chapters.chapterId": chapterId },
155-
{ $set: { "chapters.$.rewardedQuestions": newTotal } }
127+
{ userId, career },
128+
{ $set: { careerRewarded: true } }
156129
);
157-
console.log(`✅ Improvement bonus: +${tokensAwarded} tokens, +${xpAwarded} XP to user ${userId} for ${career}/${chapterId}`);
158-
159-
// Check and unlock relevant badges
160-
const badgesToCheck = ['beginner', 'cases_5', 'cases_10', 'cases_25'];
161-
const newlyUnlocked = await checkAndUnlockMultipleBadges(userId, badgesToCheck);
162-
if (newlyUnlocked.length > 0) {
163-
console.log(`✅ New badges unlocked: ${newlyUnlocked.join(', ')}`);
164-
}
130+
console.log(`✅ Career complete: +1 token to user ${userId} for ${career}`);
165131
} catch (err) {
166-
console.error(`❌ Failed to award improvement bonus for user ${userId}:`, err.message);
132+
console.error(`❌ Failed to award career token:`, err.message);
167133
}
168-
} else if (!passed) {
169-
// Did not pass - no XP/tokens awarded
170-
console.log(`ℹ️ Quiz not passed (${newTotal} correct, need >12): no rewards for user ${userId}`);
171134
}
172135

173136
// Refetch and return updated document

0 commit comments

Comments
 (0)