feat: enhance module list rendering with expandable lessons and active lesson tracking
This commit is contained in:
30
src/app.js
30
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();
|
||||
|
||||
|
||||
@@ -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 = "<h3>CSS Lessons</h3>";
|
||||
|
||||
// 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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
87
src/main.css
87
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 */
|
||||
|
||||
Reference in New Issue
Block a user