From 0065cf497e79fdfa282d0e574845bdad3a5ba425 Mon Sep 17 00:00:00 2001 From: Michael Czechowski Date: Tue, 20 May 2025 01:43:57 +0200 Subject: [PATCH] feat: enhance module list rendering with expandable lessons and active lesson tracking --- src/app.js | 30 ++++++-- src/helpers/renderer.js | 148 ++++++++++++++++++++++++++++++++++------ src/main.css | 87 +++++++++++++++++++++++ 3 files changed, 241 insertions(+), 24 deletions(-) diff --git a/src/app.js b/src/app.js index 8fe3c6e..e1ff4e4 100644 --- a/src/app.js +++ b/src/app.js @@ -1,5 +1,5 @@ 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 { loadModules } from "./config/lessons.js"; @@ -89,7 +89,9 @@ function initFeedbackToggle() { async function initializeModules() { try { 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 const lastModuleId = localStorage.getItem("codeCrispies.lastModuleId"); @@ -158,8 +160,8 @@ function selectModule(moduleId) { state.currentModule = selectedModule; - // Update module list UI - const moduleItems = elements.moduleList.querySelectorAll(".module-list-item"); + // Update module list UI to highlight the active module + const moduleItems = elements.moduleList.querySelectorAll(".module-header"); moduleItems.forEach((item) => { item.classList.remove("active"); if (item.dataset.moduleId === moduleId) { @@ -182,6 +184,23 @@ function selectModule(moduleId) { 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 function resetSuccessIndicators() { elements.codeEditor.classList.remove("success-highlight"); @@ -255,6 +274,9 @@ function loadCurrentLesson() { // Update level indicator renderLevelIndicator(elements.levelIndicator, state.currentLessonIndex + 1, state.currentModule.lessons.length); + // Update active lesson in sidebar + updateActiveLessonInSidebar(state.currentModule.id, state.currentLessonIndex); + // Update navigation buttons updateNavigationButtons(); diff --git a/src/helpers/renderer.js b/src/helpers/renderer.js index 707a370..c24ff08 100644 --- a/src/helpers/renderer.js +++ b/src/helpers/renderer.js @@ -7,40 +7,110 @@ let feedbackElement = 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 {Array} modules - The list of modules * @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 container.innerHTML = "

CSS Lessons

"; + // Get user progress from localStorage + const progressData = localStorage.getItem("codeCrispies.Progress"); + let progress = {}; + if (progressData) { + try { + progress = JSON.parse(progressData); + } catch (e) { + console.error("Error parsing progress data:", e); + } + } + // Create list items for each module modules.forEach((module) => { - const moduleItem = document.createElement("div"); - moduleItem.classList.add("module-list-item"); - moduleItem.dataset.moduleId = module.id; - moduleItem.textContent = module.title; + // Create module container + const moduleContainer = document.createElement("div"); + moduleContainer.classList.add("module-container"); - // Get user progress from localStorage to mark completed lessons - const progressData = localStorage.getItem("codeCrispies.Progress"); - if (progressData) { - try { - const progress = JSON.parse(progressData); - if (progress[module.id] && progress[module.id].completed.length === module.lessons.length) { - moduleItem.classList.add("completed"); - } - } catch (e) { - console.error("Error parsing progress data:", e); - } + // 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"); } - moduleItem.addEventListener("click", () => { - onSelectModule(module.id); + // 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); + }); + + lessonsContainer.appendChild(lessonItem); }); - container.appendChild(moduleItem); + // 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; } + +/** + * 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" }); + } + } +} diff --git a/src/main.css b/src/main.css index 53791f4..36f65e3 100644 --- a/src/main.css +++ b/src/main.css @@ -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 ================= */ /* Base responsive layout */