feat: add landing pages and section navigation

- Add home landing page with section cards (CSS, HTML, Tailwind)
- Add section landing pages with module grid and progress tracking
- Implement extended URL routing for pages, sections, and lessons
- Create sections.js configuration for module categorization
- Exclude welcome/goodbye modules from progress stats
- Add main navigation links in header (desktop only)
- Update logo click to navigate to home landing

Routes:
- # → Home landing
- #css, #html, #tailwind → Section landing pages
- #module/index → Lesson (unchanged)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
2026-01-14 23:15:34 +01:00
parent 5083032735
commit 165ed3d73f
13 changed files with 679 additions and 42 deletions

View File

@@ -3,7 +3,8 @@ import { CodeEditor } from "./impl/CodeEditor.js";
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
import { loadModules } from "./config/lessons.js";
import { initI18n, t, getLanguage, setLanguage, applyTranslations } from "./i18n.js";
import { parseHash, updateHash, replaceHash, getShareableUrl } from "./helpers/router.js";
import { parseHash, updateHash, replaceHash, getShareableUrl, RouteType, navigateTo } from "./helpers/router.js";
import { sections, getSection, getModuleSection, getModulesBySection } from "./config/sections.js";
// Simplified state - LessonEngine now manages lesson state and progress
const state = {
@@ -22,6 +23,19 @@ const elements = {
logoLink: document.getElementById("logo-link"),
langSelect: document.getElementById("lang-select"),
helpBtn: document.getElementById("help-btn"),
mainNav: document.getElementById("main-nav"),
// Page containers
landingPage: document.getElementById("landing-page"),
sectionPage: document.getElementById("section-page"),
gameLayout: document.getElementById("main-content"),
// Section page elements
sectionTitle: document.getElementById("section-title"),
sectionDescription: document.getElementById("section-description"),
sectionProgressFill: document.getElementById("section-progress-fill"),
sectionProgressText: document.getElementById("section-progress-text"),
moduleGrid: document.getElementById("module-grid"),
// Left panel
instructionsSection: document.querySelector(".instructions"),
@@ -293,23 +307,8 @@ function initializeModules() {
// Use the new renderModuleList function with both callbacks
renderModuleList(elements.moduleList, modules, selectModule, selectLesson);
// Check URL first for shareable links
const urlState = parseHash();
if (urlState) {
// URL takes priority - navigate to specified lesson
navigateToLesson(urlState.moduleId, urlState.lessonIndex, false);
} else {
// No URL - use saved progress (existing logic)
const progressData = lessonEngine.loadUserProgress();
const lastModuleId = progressData?.lastModuleId;
if (lastModuleId && modules.find((m) => m.id === lastModuleId)) {
selectModule(lastModuleId);
} else if (modules.length > 0) {
selectModule(modules[0].id);
}
}
// Handle route (home, section, or lesson)
handleRoute(false);
updateProgressDisplay();
clearLoadingTimeout();
@@ -325,6 +324,9 @@ function selectModule(moduleId) {
const success = lessonEngine.setModuleById(moduleId);
if (!success) return;
// Show lesson UI
showLessonUI();
// Update URL
const engineState = lessonEngine.getCurrentState();
updateHash(moduleId, engineState.lessonIndex);
@@ -355,6 +357,9 @@ function selectLesson(moduleId, lessonIndex) {
lessonEngine.setLessonByIndex(lessonIndex);
// Show lesson UI
showLessonUI();
// Update URL
updateHash(moduleId, lessonIndex);
@@ -814,7 +819,7 @@ async function copyShareUrl() {
}
}
// ================= URL ROUTING =================
// ================= URL ROUTING & PAGE SWITCHING =================
function initRouter() {
// Handle browser back/forward
@@ -822,13 +827,196 @@ function initRouter() {
}
function handlePopState() {
const parsed = parseHash();
if (parsed) {
navigateToLesson(parsed.moduleId, parsed.lessonIndex, false);
handleRoute(false);
}
/**
* Main route handler - switches between page types
*/
function handleRoute(shouldUpdateUrl = true) {
const route = parseHash();
if (!route) {
// Invalid route - go to home
navigateTo("");
showLandingPage();
return;
}
switch (route.type) {
case RouteType.HOME:
showLandingPage();
break;
case RouteType.SECTION:
showSectionPage(route.sectionId);
break;
case RouteType.REFERENCE:
// Reference pages - TODO: implement later
showLandingPage();
break;
case RouteType.LESSON:
navigateToLesson(route.moduleId, route.lessonIndex, shouldUpdateUrl);
break;
default:
showLandingPage();
}
updateNavHighlight(route);
}
/**
* Hide all page containers
*/
function hideAllPages() {
elements.landingPage?.classList.add("hidden");
elements.sectionPage?.classList.add("hidden");
elements.gameLayout?.classList.add("hidden");
}
/**
* Show home landing page
*/
function showLandingPage() {
hideAllPages();
elements.landingPage?.classList.remove("hidden");
// Update section progress on landing page
updateLandingProgress();
}
/**
* Update progress indicators on landing page
*/
function updateLandingProgress() {
["css", "html", "tailwind"].forEach((sectionId) => {
const progressEl = document.getElementById(`${sectionId}-progress`);
if (progressEl) {
const sectionModules = getModulesBySection(lessonEngine.modules, sectionId);
let completed = 0;
let total = 0;
sectionModules.forEach((m) => {
total += m.lessons.length;
const progress = lessonEngine.userProgress[m.id];
if (progress?.completed) {
completed += progress.completed.length;
}
});
if (total > 0) {
progressEl.textContent = `${completed}/${total} lessons`;
} else {
progressEl.textContent = "";
}
}
});
}
/**
* Show section landing page
*/
function showSectionPage(sectionId) {
hideAllPages();
elements.sectionPage?.classList.remove("hidden");
const section = getSection(sectionId);
if (!section) {
showLandingPage();
return;
}
// Update section header
if (elements.sectionTitle) elements.sectionTitle.textContent = section.title;
if (elements.sectionDescription) elements.sectionDescription.textContent = section.description;
// Get modules for this section
const sectionModules = getModulesBySection(lessonEngine.modules, sectionId);
// Calculate section progress
let completed = 0;
let total = 0;
sectionModules.forEach((m) => {
total += m.lessons.length;
const progress = lessonEngine.userProgress[m.id];
if (progress?.completed) {
completed += progress.completed.length;
}
});
// Update progress bar
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
if (elements.sectionProgressFill) elements.sectionProgressFill.style.width = `${percent}%`;
if (elements.sectionProgressText) elements.sectionProgressText.textContent = `${completed} of ${total} lessons complete`;
// Render module cards
renderModuleGrid(sectionModules, sectionId);
}
/**
* Render module cards in section page
*/
function renderModuleGrid(modules, sectionId) {
if (!elements.moduleGrid) return;
elements.moduleGrid.innerHTML = "";
modules.forEach((module) => {
const card = document.createElement("a");
card.href = `#${module.id}/0`;
card.className = "module-card";
// Calculate module progress
const progress = lessonEngine.userProgress[module.id];
const completed = progress?.completed?.length || 0;
const total = module.lessons.length;
card.innerHTML = `
<h3>${module.title}</h3>
<div class="module-card-meta">
<span>${total} lessons</span>
<span class="module-card-progress">${completed > 0 ? `${completed}/${total}` : ""}</span>
</div>
`;
elements.moduleGrid.appendChild(card);
});
}
/**
* Show lesson UI (game layout)
*/
function showLessonUI() {
hideAllPages();
elements.gameLayout?.classList.remove("hidden");
}
/**
* Update nav link highlighting
*/
function updateNavHighlight(route) {
if (!elements.mainNav) return;
const navLinks = elements.mainNav.querySelectorAll(".nav-link");
navLinks.forEach((link) => {
link.classList.remove("active");
if (route?.type === RouteType.SECTION && link.dataset.section === route.sectionId) {
link.classList.add("active");
} else if (route?.type === RouteType.LESSON) {
// Highlight section based on module's inferred section
const module = lessonEngine.modules.find((m) => m.id === route.moduleId);
if (module) {
const moduleSection = getModuleSection(module);
if (link.dataset.section === moduleSection) {
link.classList.add("active");
}
}
}
});
}
function navigateToLesson(moduleId, lessonIndex, shouldUpdateUrl = true) {
// Show lesson UI
showLessonUI();
// Validate moduleId exists
const module = lessonEngine.modules.find((m) => m.id === moduleId);
if (!module) {
@@ -915,13 +1103,11 @@ function init() {
elements.closeSidebar.addEventListener("click", closeSidebar);
elements.sidebarBackdrop.addEventListener("click", closeSidebar);
// Logo click - navigate to welcome
// Logo click - navigate to home landing
elements.logoLink.addEventListener("click", (e) => {
e.preventDefault();
lessonEngine.setModuleById("welcome");
updateHash("welcome", 0);
loadCurrentLesson();
updateModuleHighlight("welcome");
navigateTo("");
showLandingPage();
});
// Language select