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 { 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();
|
||||||
|
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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 ================= */
|
/* ================= RESPONSIVE DESIGN ================= */
|
||||||
|
|
||||||
/* Base responsive layout */
|
/* Base responsive layout */
|
||||||
|
|||||||
Reference in New Issue
Block a user