feat: enhance module list rendering with expandable lessons and active lesson tracking

This commit is contained in:
Michael Czechowski
2025-05-20 01:43:57 +02:00
parent 3f9158852d
commit 0065cf497e
3 changed files with 241 additions and 24 deletions

View File

@@ -1,5 +1,5 @@
import { LessonEngine } from "./impl/LessonEngine.js"; import { LessonEngine } from "./impl/LessonEngine.js";
import { renderLesson, renderModuleList, renderLevelIndicator, showFeedback } from "./helpers/renderer.js"; import { renderLesson, renderModuleList, renderLevelIndicator, showFeedback, updateActiveLessonInSidebar } from "./helpers/renderer.js";
import { validateUserCode } from "./helpers/validator.js"; import { validateUserCode } from "./helpers/validator.js";
import { loadModules } from "./config/lessons.js"; import { loadModules } from "./config/lessons.js";
@@ -89,7 +89,9 @@ function initFeedbackToggle() {
async function initializeModules() { async function initializeModules() {
try { try {
state.modules = await loadModules(); state.modules = await loadModules();
renderModuleList(elements.moduleList, state.modules, selectModule);
// Use the new renderModuleList function with both callbacks
renderModuleList(elements.moduleList, state.modules, selectModule, selectLesson);
// Select the first module or the last one user was on // Select the first module or the last one user was on
const lastModuleId = localStorage.getItem("codeCrispies.lastModuleId"); const lastModuleId = localStorage.getItem("codeCrispies.lastModuleId");
@@ -158,8 +160,8 @@ function selectModule(moduleId) {
state.currentModule = selectedModule; state.currentModule = selectedModule;
// Update module list UI // Update module list UI to highlight the active module
const moduleItems = elements.moduleList.querySelectorAll(".module-list-item"); const moduleItems = elements.moduleList.querySelectorAll(".module-header");
moduleItems.forEach((item) => { moduleItems.forEach((item) => {
item.classList.remove("active"); item.classList.remove("active");
if (item.dataset.moduleId === moduleId) { if (item.dataset.moduleId === moduleId) {
@@ -182,6 +184,23 @@ function selectModule(moduleId) {
resetSuccessIndicators(); resetSuccessIndicators();
} }
function selectLesson(moduleId, lessonIndex) {
// Select the module first if it's not already selected
if (!state.currentModule || state.currentModule.id !== moduleId) {
selectModule(moduleId);
}
// Update current lesson index
state.currentLessonIndex = lessonIndex;
// Update user progress
state.userProgress[moduleId].current = lessonIndex;
saveUserProgress();
// Load the lesson
loadCurrentLesson();
}
// Reset success indicators // Reset success indicators
function resetSuccessIndicators() { function resetSuccessIndicators() {
elements.codeEditor.classList.remove("success-highlight"); elements.codeEditor.classList.remove("success-highlight");
@@ -255,6 +274,9 @@ function loadCurrentLesson() {
// Update level indicator // Update level indicator
renderLevelIndicator(elements.levelIndicator, state.currentLessonIndex + 1, state.currentModule.lessons.length); renderLevelIndicator(elements.levelIndicator, state.currentLessonIndex + 1, state.currentModule.lessons.length);
// Update active lesson in sidebar
updateActiveLessonInSidebar(state.currentModule.id, state.currentLessonIndex);
// Update navigation buttons // Update navigation buttons
updateNavigationButtons(); updateNavigationButtons();

View File

@@ -7,40 +7,110 @@ let feedbackElement = null;
let feedbackTimeout = null; let feedbackTimeout = null;
/** /**
* Render the module list in the sidebar * Render the module list in the sidebar with expandable lessons
* @param {HTMLElement} container - The container element for the module list * @param {HTMLElement} container - The container element for the module list
* @param {Array} modules - The list of modules * @param {Array} modules - The list of modules
* @param {Function} onSelectModule - Callback when a module is selected * @param {Function} onSelectModule - Callback when a module is selected
* @param {Function} onSelectLesson - Callback when a lesson is selected
*/ */
export function renderModuleList(container, modules, onSelectModule) { export function renderModuleList(container, modules, onSelectModule, onSelectLesson) {
// Clear the container // Clear the container
container.innerHTML = "<h3>CSS Lessons</h3>"; container.innerHTML = "<h3>CSS Lessons</h3>";
// Create list items for each module // Get user progress from localStorage
modules.forEach((module) => {
const moduleItem = document.createElement("div");
moduleItem.classList.add("module-list-item");
moduleItem.dataset.moduleId = module.id;
moduleItem.textContent = module.title;
// Get user progress from localStorage to mark completed lessons
const progressData = localStorage.getItem("codeCrispies.Progress"); const progressData = localStorage.getItem("codeCrispies.Progress");
let progress = {};
if (progressData) { if (progressData) {
try { try {
const progress = JSON.parse(progressData); progress = JSON.parse(progressData);
if (progress[module.id] && progress[module.id].completed.length === module.lessons.length) {
moduleItem.classList.add("completed");
}
} catch (e) { } catch (e) {
console.error("Error parsing progress data:", e); console.error("Error parsing progress data:", e);
} }
} }
moduleItem.addEventListener("click", () => { // Create list items for each module
onSelectModule(module.id); modules.forEach((module) => {
// Create module container
const moduleContainer = document.createElement("div");
moduleContainer.classList.add("module-container");
// Create module header item (clickable to expand/collapse)
const moduleHeader = document.createElement("div");
moduleHeader.classList.add("module-list-item", "module-header");
moduleHeader.dataset.moduleId = module.id;
// Create module title with expand/collapse indicator
const moduleTitle = document.createElement("span");
moduleTitle.classList.add("module-title");
moduleTitle.textContent = module.title;
// Create expand/collapse icon
const expandIcon = document.createElement("span");
expandIcon.classList.add("expand-icon");
expandIcon.innerHTML = "▶"; // Right-pointing triangle
moduleHeader.appendChild(expandIcon);
moduleHeader.appendChild(moduleTitle);
// Check if the module is completed
if (progress[module.id] && progress[module.id].completed.length === module.lessons.length) {
moduleHeader.classList.add("completed");
}
// Lessons container (initially hidden)
const lessonsContainer = document.createElement("div");
lessonsContainer.classList.add("lessons-container");
lessonsContainer.style.display = "none"; // Initially collapsed
// Create list items for each lesson in this module
module.lessons.forEach((lesson, index) => {
const lessonItem = document.createElement("div");
lessonItem.classList.add("lesson-list-item");
lessonItem.dataset.moduleId = module.id;
lessonItem.dataset.lessonIndex = index;
lessonItem.textContent = lesson.title || `Lesson ${index + 1}`;
// Mark lesson as completed if in progress data
if (progress[module.id] && progress[module.id].completed.includes(index)) {
lessonItem.classList.add("completed");
}
// Mark lesson as current if it's the current lesson
if (progress[module.id] && progress[module.id].current === index) {
lessonItem.classList.add("current");
}
// Add click event to select lesson
lessonItem.addEventListener("click", () => {
// Update UI to show this lesson is selected
document.querySelectorAll(".lesson-list-item").forEach((item) => {
item.classList.remove("active");
});
lessonItem.classList.add("active");
// Call the onSelectLesson callback
onSelectLesson(module.id, index);
}); });
container.appendChild(moduleItem); lessonsContainer.appendChild(lessonItem);
});
// Toggle expand/collapse when clicking on module header
moduleHeader.addEventListener("click", () => {
// Toggle visibility of lessons container
const isExpanded = lessonsContainer.style.display !== "none";
lessonsContainer.style.display = isExpanded ? "none" : "block";
// Update expand/collapse icon
expandIcon.innerHTML = isExpanded ? "▶" : "▼";
});
// Add module header and lessons container to module container
moduleContainer.appendChild(moduleHeader);
moduleContainer.appendChild(lessonsContainer);
// Add the complete module container to the sidebar
container.appendChild(moduleContainer);
}); });
} }
@@ -132,3 +202,41 @@ export function clearFeedback() {
} }
feedbackElement = null; feedbackElement = null;
} }
/**
* Update the active lesson in the sidebar
* @param {string} moduleId - The ID of the module
* @param {number} lessonIndex - The index of the lesson
*/
export function updateActiveLessonInSidebar(moduleId, lessonIndex) {
// Remove active class from all lessons
document.querySelectorAll(".lesson-list-item").forEach((item) => {
item.classList.remove("active");
});
// Find and activate the current lesson
const selector = `.lesson-list-item[data-module-id="${moduleId}"][data-lesson-index="${lessonIndex}"]`;
const currentLessonItem = document.querySelector(selector);
if (currentLessonItem) {
currentLessonItem.classList.add("active");
// Make sure parent module is expanded
const parentLessonsContainer = currentLessonItem.parentElement;
if (parentLessonsContainer && parentLessonsContainer.classList.contains("lessons-container")) {
parentLessonsContainer.style.display = "block";
// Update expand icon
const moduleHeader = parentLessonsContainer.previousElementSibling;
if (moduleHeader) {
const expandIcon = moduleHeader.querySelector(".expand-icon");
if (expandIcon) {
expandIcon.innerHTML = "▼"; // Down arrow when expanded
}
}
// Scroll to ensure the item is visible
currentLessonItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
}

View File

@@ -794,6 +794,93 @@ input:checked + .toggle-slider:before {
} }
} }
/* Module and Lesson List Styles */
.module-container {
margin-bottom: 8px;
}
.module-header {
display: flex;
align-items: center;
cursor: pointer;
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.2s;
font-weight: 600;
}
.module-header:hover {
background-color: var(--hover-color);
}
.expand-icon {
display: inline-block;
margin-right: 8px;
font-size: 10px;
transition: transform 0.2s;
}
.lessons-container {
margin-left: 16px;
border-left: 2px solid var(--border-color);
padding-left: 8px;
}
.lesson-list-item {
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
font-size: 0.9em;
margin: 4px 0;
}
.lesson-list-item:hover {
background-color: var(--primary-bg-medium);
}
.lesson-list-item.active {
background-color: var(--primary-bg-medium);
color: var(--dark-text);
font-weight: bold;
}
.lesson-list-item.completed::before {
content: "✓";
margin-right: 6px;
color: var(--success-color);
}
.module-header.completed::before {
content: "✓";
margin-right: 6px;
color: var(--success-color);
}
/* Improve scrolling for the sidebar */
.sidebar .module-list {
max-height: calc(100vh - 200px);
padding-right: 5px;
}
/* Add smooth scrolling */
.sidebar {
scroll-behavior: smooth;
}
/* Mobile adjustments */
@media (max-width: 768px) {
.lessons-container {
margin-left: 8px;
padding-left: 6px;
}
.lesson-list-item,
.module-header {
padding: 8px 6px;
}
}
/* ================= RESPONSIVE DESIGN ================= */ /* ================= RESPONSIVE DESIGN ================= */
/* Base responsive layout */ /* Base responsive layout */