Skip to content

Commit ded85e9

Browse files
committed
feat: enhance module list rendering with expandable lessons and active lesson tracking
1 parent b408d8f commit ded85e9

File tree

3 files changed

+242
-25
lines changed

3 files changed

+242
-25
lines changed

src/app.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { LessonEngine } from "./impl/LessonEngine.js";
2-
import { renderLesson, renderModuleList, renderLevelIndicator, showFeedback } from "./helpers/renderer.js";
2+
import { renderLesson, renderModuleList, renderLevelIndicator, showFeedback, updateActiveLessonInSidebar } from "./helpers/renderer.js";
33
import { validateUserCode } from "./helpers/validator.js";
44
import { loadModules } from "./config/lessons.js";
55

@@ -89,7 +89,9 @@ function initFeedbackToggle() {
8989
async function initializeModules() {
9090
try {
9191
state.modules = await loadModules();
92-
renderModuleList(elements.moduleList, state.modules, selectModule);
92+
93+
// Use the new renderModuleList function with both callbacks
94+
renderModuleList(elements.moduleList, state.modules, selectModule, selectLesson);
9395

9496
// Select the first module or the last one user was on
9597
const lastModuleId = localStorage.getItem("codeCrispies.lastModuleId");
@@ -158,8 +160,8 @@ function selectModule(moduleId) {
158160

159161
state.currentModule = selectedModule;
160162

161-
// Update module list UI
162-
const moduleItems = elements.moduleList.querySelectorAll(".module-list-item");
163+
// Update module list UI to highlight the active module
164+
const moduleItems = elements.moduleList.querySelectorAll(".module-header");
163165
moduleItems.forEach((item) => {
164166
item.classList.remove("active");
165167
if (item.dataset.moduleId === moduleId) {
@@ -182,6 +184,23 @@ function selectModule(moduleId) {
182184
resetSuccessIndicators();
183185
}
184186

187+
function selectLesson(moduleId, lessonIndex) {
188+
// Select the module first if it's not already selected
189+
if (!state.currentModule || state.currentModule.id !== moduleId) {
190+
selectModule(moduleId);
191+
}
192+
193+
// Update current lesson index
194+
state.currentLessonIndex = lessonIndex;
195+
196+
// Update user progress
197+
state.userProgress[moduleId].current = lessonIndex;
198+
saveUserProgress();
199+
200+
// Load the lesson
201+
loadCurrentLesson();
202+
}
203+
185204
// Reset success indicators
186205
function resetSuccessIndicators() {
187206
elements.codeEditor.classList.remove("success-highlight");
@@ -255,6 +274,9 @@ function loadCurrentLesson() {
255274
// Update level indicator
256275
renderLevelIndicator(elements.levelIndicator, state.currentLessonIndex + 1, state.currentModule.lessons.length);
257276

277+
// Update active lesson in sidebar
278+
updateActiveLessonInSidebar(state.currentModule.id, state.currentLessonIndex);
279+
258280
// Update navigation buttons
259281
updateNavigationButtons();
260282

src/helpers/renderer.js

Lines changed: 129 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,110 @@ let feedbackElement = null;
77
let feedbackTimeout = null;
88

99
/**
10-
* Render the module list in the sidebar
10+
* Render the module list in the sidebar with expandable lessons
1111
* @param {HTMLElement} container - The container element for the module list
1212
* @param {Array} modules - The list of modules
1313
* @param {Function} onSelectModule - Callback when a module is selected
14+
* @param {Function} onSelectLesson - Callback when a lesson is selected
1415
*/
15-
export function renderModuleList(container, modules, onSelectModule) {
16+
export function renderModuleList(container, modules, onSelectModule, onSelectLesson) {
1617
// Clear the container
1718
container.innerHTML = "<h3>CSS Lessons</h3>";
1819

20+
// Get user progress from localStorage
21+
const progressData = localStorage.getItem("codeCrispies.Progress");
22+
let progress = {};
23+
if (progressData) {
24+
try {
25+
progress = JSON.parse(progressData);
26+
} catch (e) {
27+
console.error("Error parsing progress data:", e);
28+
}
29+
}
30+
1931
// Create list items for each module
2032
modules.forEach((module) => {
21-
const moduleItem = document.createElement("div");
22-
moduleItem.classList.add("module-list-item");
23-
moduleItem.dataset.moduleId = module.id;
24-
moduleItem.textContent = module.title;
25-
26-
// Get user progress from localStorage to mark completed lessons
27-
const progressData = localStorage.getItem("codeCrispies.Progress");
28-
if (progressData) {
29-
try {
30-
const progress = JSON.parse(progressData);
31-
if (progress[module.id] && progress[module.id].completed.length === module.lessons.length) {
32-
moduleItem.classList.add("completed");
33-
}
34-
} catch (e) {
35-
console.error("Error parsing progress data:", e);
36-
}
33+
// Create module container
34+
const moduleContainer = document.createElement("div");
35+
moduleContainer.classList.add("module-container");
36+
37+
// Create module header item (clickable to expand/collapse)
38+
const moduleHeader = document.createElement("div");
39+
moduleHeader.classList.add("module-list-item", "module-header");
40+
moduleHeader.dataset.moduleId = module.id;
41+
42+
// Create module title with expand/collapse indicator
43+
const moduleTitle = document.createElement("span");
44+
moduleTitle.classList.add("module-title");
45+
moduleTitle.textContent = module.title;
46+
47+
// Create expand/collapse icon
48+
const expandIcon = document.createElement("span");
49+
expandIcon.classList.add("expand-icon");
50+
expandIcon.innerHTML = "▶"; // Right-pointing triangle
51+
52+
moduleHeader.appendChild(expandIcon);
53+
moduleHeader.appendChild(moduleTitle);
54+
55+
// Check if the module is completed
56+
if (progress[module.id] && progress[module.id].completed.length === module.lessons.length) {
57+
moduleHeader.classList.add("completed");
3758
}
3859

39-
moduleItem.addEventListener("click", () => {
40-
onSelectModule(module.id);
60+
// Lessons container (initially hidden)
61+
const lessonsContainer = document.createElement("div");
62+
lessonsContainer.classList.add("lessons-container");
63+
lessonsContainer.style.display = "none"; // Initially collapsed
64+
65+
// Create list items for each lesson in this module
66+
module.lessons.forEach((lesson, index) => {
67+
const lessonItem = document.createElement("div");
68+
lessonItem.classList.add("lesson-list-item");
69+
lessonItem.dataset.moduleId = module.id;
70+
lessonItem.dataset.lessonIndex = index;
71+
lessonItem.textContent = lesson.title || `Lesson ${index + 1}`;
72+
73+
// Mark lesson as completed if in progress data
74+
if (progress[module.id] && progress[module.id].completed.includes(index)) {
75+
lessonItem.classList.add("completed");
76+
}
77+
78+
// Mark lesson as current if it's the current lesson
79+
if (progress[module.id] && progress[module.id].current === index) {
80+
lessonItem.classList.add("current");
81+
}
82+
83+
// Add click event to select lesson
84+
lessonItem.addEventListener("click", () => {
85+
// Update UI to show this lesson is selected
86+
document.querySelectorAll(".lesson-list-item").forEach((item) => {
87+
item.classList.remove("active");
88+
});
89+
lessonItem.classList.add("active");
90+
91+
// Call the onSelectLesson callback
92+
onSelectLesson(module.id, index);
93+
});
94+
95+
lessonsContainer.appendChild(lessonItem);
4196
});
4297

43-
container.appendChild(moduleItem);
98+
// Toggle expand/collapse when clicking on module header
99+
moduleHeader.addEventListener("click", () => {
100+
// Toggle visibility of lessons container
101+
const isExpanded = lessonsContainer.style.display !== "none";
102+
lessonsContainer.style.display = isExpanded ? "none" : "block";
103+
104+
// Update expand/collapse icon
105+
expandIcon.innerHTML = isExpanded ? "▶" : "▼";
106+
});
107+
108+
// Add module header and lessons container to module container
109+
moduleContainer.appendChild(moduleHeader);
110+
moduleContainer.appendChild(lessonsContainer);
111+
112+
// Add the complete module container to the sidebar
113+
container.appendChild(moduleContainer);
44114
});
45115
}
46116

@@ -132,3 +202,41 @@ export function clearFeedback() {
132202
}
133203
feedbackElement = null;
134204
}
205+
206+
/**
207+
* Update the active lesson in the sidebar
208+
* @param {string} moduleId - The ID of the module
209+
* @param {number} lessonIndex - The index of the lesson
210+
*/
211+
export function updateActiveLessonInSidebar(moduleId, lessonIndex) {
212+
// Remove active class from all lessons
213+
document.querySelectorAll(".lesson-list-item").forEach((item) => {
214+
item.classList.remove("active");
215+
});
216+
217+
// Find and activate the current lesson
218+
const selector = `.lesson-list-item[data-module-id="${moduleId}"][data-lesson-index="${lessonIndex}"]`;
219+
const currentLessonItem = document.querySelector(selector);
220+
221+
if (currentLessonItem) {
222+
currentLessonItem.classList.add("active");
223+
224+
// Make sure parent module is expanded
225+
const parentLessonsContainer = currentLessonItem.parentElement;
226+
if (parentLessonsContainer && parentLessonsContainer.classList.contains("lessons-container")) {
227+
parentLessonsContainer.style.display = "block";
228+
229+
// Update expand icon
230+
const moduleHeader = parentLessonsContainer.previousElementSibling;
231+
if (moduleHeader) {
232+
const expandIcon = moduleHeader.querySelector(".expand-icon");
233+
if (expandIcon) {
234+
expandIcon.innerHTML = "▼"; // Down arrow when expanded
235+
}
236+
}
237+
238+
// Scroll to ensure the item is visible
239+
currentLessonItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
240+
}
241+
}
242+
}

src/main.css

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,93 @@ input:checked + .toggle-slider:before {
794794
}
795795
}
796796

797+
/* Module and Lesson List Styles */
798+
.module-container {
799+
margin-bottom: 8px;
800+
}
801+
802+
.module-header {
803+
display: flex;
804+
align-items: center;
805+
cursor: pointer;
806+
padding: 8px 12px;
807+
border-radius: 4px;
808+
transition: background-color 0.2s;
809+
font-weight: 600;
810+
}
811+
812+
.module-header:hover {
813+
background-color: var(--hover-color);
814+
}
815+
816+
.expand-icon {
817+
display: inline-block;
818+
margin-right: 8px;
819+
font-size: 10px;
820+
transition: transform 0.2s;
821+
}
822+
823+
.lessons-container {
824+
margin-left: 16px;
825+
border-left: 2px solid var(--border-color);
826+
padding-left: 8px;
827+
}
828+
829+
.lesson-list-item {
830+
padding: 6px 12px;
831+
border-radius: 4px;
832+
cursor: pointer;
833+
transition: background-color 0.2s;
834+
font-size: 0.9em;
835+
margin: 4px 0;
836+
}
837+
838+
.lesson-list-item:hover {
839+
background-color: var(--primary-bg-medium);
840+
}
841+
842+
.lesson-list-item.active {
843+
background-color: var(--primary-bg-medium);
844+
color: var(--dark-text);
845+
font-weight: bold;
846+
}
847+
848+
.lesson-list-item.completed::before {
849+
content: "✓";
850+
margin-right: 6px;
851+
color: var(--success-color);
852+
}
853+
854+
.module-header.completed::before {
855+
content: "✓";
856+
margin-right: 6px;
857+
color: var(--success-color);
858+
}
859+
860+
/* Improve scrolling for the sidebar */
861+
.sidebar .module-list {
862+
max-height: calc(100vh - 200px);
863+
padding-right: 5px;
864+
}
865+
866+
/* Add smooth scrolling */
867+
.sidebar {
868+
scroll-behavior: smooth;
869+
}
870+
871+
/* Mobile adjustments */
872+
@media (max-width: 768px) {
873+
.lessons-container {
874+
margin-left: 8px;
875+
padding-left: 6px;
876+
}
877+
878+
.lesson-list-item,
879+
.module-header {
880+
padding: 8px 6px;
881+
}
882+
}
883+
797884
/* ================= RESPONSIVE DESIGN ================= */
798885

799886
/* Base responsive layout */

0 commit comments

Comments
 (0)