import { LessonEngine } from "./impl/LessonEngine.js"; import { CodeEditor, crispyEditorTheme } 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, RouteType, navigateTo } from "./helpers/router.js"; import { sections, getSection, getModuleSection, getModulesBySection } from "./config/sections.js"; import { getRandomTemplate } from "./config/playground-templates.js"; // CodeMirror imports for syntax highlighting import { EditorState } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import { html } from "@codemirror/lang-html"; import { css } from "@codemirror/lang-css"; // Analytics tracking helper (Umami v2.13.2) function track(eventName, eventData = {}) { if (typeof umami !== "undefined" && umami.track) { umami.track(eventName, eventData); } } // Simplified state - LessonEngine now manages lesson state and progress const state = { userSettings: { disableFeedbackErrors: false, skipResetCodeConfirmation: false }, showExpected: false, animationTimeout: null, lastPlaygroundTemplate: null }; // Track CodeMirror views for cleanup let sectionCodeViews = []; // Read-only CodeMirror theme for code examples const readOnlyTheme = EditorView.theme( { "&": { fontSize: "13px" }, ".cm-content": { fontFamily: "'JetBrains Mono', 'Fira Code', monospace", padding: "12px 0" }, ".cm-line": { padding: "0 12px" }, ".cm-gutters": { display: "none" } }, { dark: true } ); /** * Highlight all code blocks in the section page using CodeMirror */ function highlightSectionCodeBlocks() { // Clean up previous views sectionCodeViews.forEach((view) => view.destroy()); sectionCodeViews = []; // Find all code blocks in section page const codeBlocks = elements.sectionIntro?.querySelectorAll(".code-block") || []; codeBlocks.forEach((block) => { const pre = block.querySelector("pre"); const code = block.querySelector("code"); if (!pre || !code) return; const content = code.textContent || ""; // Detect language from content const isHTML = content.includes("<") && content.includes(">"); const langExtension = isHTML ? html() : css(); // Create read-only CodeMirror view const state = EditorState.create({ doc: content, extensions: [langExtension, crispyEditorTheme, readOnlyTheme, EditorState.readOnly.of(true), EditorView.lineWrapping] }); const view = new EditorView({ state, parent: block }); // Remove original pre/code pre.remove(); sectionCodeViews.push(view); }); } // DOM elements - updated for new layout const elements = { // Header menuBtn: document.getElementById("menu-btn"), 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"), sectionIntro: document.getElementById("section-intro"), // Reference page elements referencePage: document.getElementById("reference-page"), referenceNav: document.getElementById("reference-nav"), referenceBody: document.getElementById("reference-body"), // Left panel instructionsSection: document.querySelector(".instructions"), editorSection: document.querySelector(".editor-section"), modulePill: document.getElementById("module-pill"), moduleName: document.querySelector(".module-name"), lessonTitle: document.getElementById("lesson-title"), lessonTitleRow: document.querySelector(".lesson-title-row"), lessonDescription: document.getElementById("lesson-description"), taskInstruction: document.getElementById("task-instruction"), codeInput: document.getElementById("code-input"), runBtn: document.getElementById("run-btn"), undoBtn: document.getElementById("undo-btn"), redoBtn: document.getElementById("redo-btn"), resetCodeBtn: document.getElementById("reset-code-btn"), randomTemplateBtn: document.getElementById("random-template-btn"), hintArea: document.getElementById("hint-area"), editorContent: document.querySelector(".editor-content"), codeEditor: document.querySelector(".code-editor"), // Right panel previewArea: document.getElementById("preview-area"), showExpectedBtn: document.getElementById("show-expected-btn"), expectedOverlay: document.getElementById("expected-overlay"), previewWrapper: document.querySelector(".preview-wrapper"), previewSection: document.querySelector(".preview-section"), prevBtn: document.getElementById("prev-btn"), nextBtn: document.getElementById("next-btn"), levelIndicator: document.getElementById("level-indicator"), headerLevelPill: document.getElementById("header-level-pill"), // Sidebar sidebarDrawer: document.getElementById("sidebar-drawer"), sidebarBackdrop: document.getElementById("sidebar-backdrop"), closeSidebar: document.getElementById("close-sidebar"), moduleList: document.getElementById("module-list"), footerLessonLinks: document.getElementById("footer-lesson-links"), progressFill: document.getElementById("progress-fill"), progressText: document.getElementById("progress-text"), resetBtn: document.getElementById("reset-btn"), disableFeedbackToggle: document.getElementById("disable-feedback-toggle"), // Dialogs helpDialog: document.getElementById("help-dialog"), helpDialogClose: document.getElementById("help-dialog-close"), resetDialog: document.getElementById("reset-dialog"), resetDialogClose: document.getElementById("reset-dialog-close"), cancelReset: document.getElementById("cancel-reset"), confirmReset: document.getElementById("confirm-reset"), resetCodeDialog: document.getElementById("reset-code-dialog"), resetCodeDialogClose: document.getElementById("reset-code-dialog-close"), cancelResetCode: document.getElementById("cancel-reset-code"), confirmResetCode: document.getElementById("confirm-reset-code"), resetCodeDontShow: document.getElementById("reset-code-dont-show"), // Share dialog shareBtn: document.getElementById("share-btn"), shareDialog: document.getElementById("share-dialog"), shareDialogClose: document.getElementById("share-dialog-close"), shareUrlInput: document.getElementById("share-url-input"), copyUrlBtn: document.getElementById("copy-url-btn"), copyFeedback: document.getElementById("copy-feedback") }; // Initialize the lesson engine - now the single source of truth const lessonEngine = new LessonEngine(); // Code editor instance (initialized later) let codeEditor = null; let currentMode = "css"; // ================= SIDEBAR FUNCTIONS ================= // Track element that opened sidebar for focus return let sidebarTrigger = null; function openSidebar() { track("sidebar_open"); // Store trigger element for focus return sidebarTrigger = document.activeElement; elements.sidebarDrawer.classList.add("open"); elements.sidebarBackdrop.classList.add("visible"); // Move focus to close button for keyboard users elements.closeSidebar.focus(); } function closeSidebar() { elements.sidebarDrawer.classList.remove("open"); elements.sidebarBackdrop.classList.remove("visible"); // Return focus to trigger element if (sidebarTrigger && typeof sidebarTrigger.focus === "function") { sidebarTrigger.focus(); sidebarTrigger = null; } } // ================= EXPECTED RESULT TOGGLE ================= function toggleExpectedResult() { state.showExpected = !state.showExpected; if (state.showExpected) { elements.expectedOverlay.classList.add("visible"); elements.showExpectedBtn.textContent = t("hideExpected"); elements.showExpectedBtn.classList.add("btn-primary"); } else { elements.expectedOverlay.classList.remove("visible"); elements.showExpectedBtn.textContent = t("showExpected"); elements.showExpectedBtn.classList.remove("btn-primary"); } } // ================= LANGUAGE TOGGLE ================= function changeLanguage(newLang) { track("language_change", { language: newLang }); // Add transition class before any updates elements.editorSection?.classList.add("transitioning"); setLanguage(newLang); applyTranslations(); // Reload lessons in new language const engineState = lessonEngine.getCurrentState(); const currentModuleId = engineState.module?.id; const currentLessonIndex = engineState.lessonIndex; const modules = loadModules(newLang); lessonEngine.setModules(modules); renderModuleList(elements.moduleList, modules, selectModule, selectLesson); // Restore position in current module/lesson if (currentModuleId) { lessonEngine.setModuleById(currentModuleId); lessonEngine.setLessonByIndex(currentLessonIndex); loadCurrentLesson(); } updateProgressDisplay(); // Remove transition class after all updates requestAnimationFrame(() => { elements.editorSection?.classList.remove("transitioning"); }); } // ================= HINT SYSTEM ================= function showHint(message, step, total, isSuccess = false) { const hintClass = isSuccess ? "hint hint-success" : "hint"; elements.hintArea.innerHTML = `
${step}/${total} ${message}
`; } function clearHint() { elements.hintArea.innerHTML = ""; } function showSuccessHint(message) { elements.hintArea.innerHTML = `
${message}
`; } // ================= PROGRESS DISPLAY ================= function updateProgressDisplay() { const stats = lessonEngine.getProgressStats(); elements.progressFill.style.width = `${stats.percentComplete}%`; elements.progressText.textContent = t("progressText", { percent: stats.percentComplete, completed: stats.totalCompleted, total: stats.totalLessons }); } // ================= USER SETTINGS ================= function loadUserSettings() { const savedSettings = localStorage.getItem("codeCrispies.settings"); if (savedSettings) { try { const settings = JSON.parse(savedSettings); state.userSettings = { ...state.userSettings, ...settings }; elements.disableFeedbackToggle.checked = !state.userSettings.disableFeedbackErrors; } catch (e) { console.error("Error loading user settings:", e); } } } function saveUserSettings() { localStorage.setItem("codeCrispies.settings", JSON.stringify(state.userSettings)); } // ================= LESSON CACHE ================= let cachedUserCode = null; function restoreLessonCache() { try { const cached = localStorage.getItem("codeCrispies.lessonCache"); if (cached) { const data = JSON.parse(cached); if (data.moduleTitle && elements.moduleName) { elements.moduleName.textContent = data.moduleTitle; // Remove data-i18n so applyTranslations won't overwrite elements.moduleName.removeAttribute("data-i18n"); } if (data.lessonTitle && elements.lessonTitle) { elements.lessonTitle.textContent = data.lessonTitle; elements.lessonTitle.removeAttribute("data-i18n"); } if (data.lessonDescription && elements.lessonDescription) { elements.lessonDescription.innerHTML = data.lessonDescription; } if (data.taskInstruction && elements.taskInstruction) { elements.taskInstruction.innerHTML = data.taskInstruction; } if (data.levelIndicator && elements.levelIndicator) { elements.levelIndicator.innerHTML = data.levelIndicator; } // Store userCode to apply after editor init if (data.userCode) { cachedUserCode = data.userCode; } } } catch (e) { // Ignore cache errors } } // ================= MODULE INITIALIZATION ================= let loadingTimeout = null; function showLoadingFallback() { // Only show if no lesson is loaded yet if (!elements.lessonTitle.textContent) { elements.lessonDescription.innerHTML = `

${t("loadingFallbackText")}

`; } } function clearLoadingTimeout() { if (loadingTimeout) { clearTimeout(loadingTimeout); loadingTimeout = null; } } function initializeModules() { try { const modules = loadModules(getLanguage()); lessonEngine.setModules(modules); // Use the new renderModuleList function with both callbacks renderModuleList(elements.moduleList, modules, selectModule, selectLesson); // Handle route (home, section, or lesson) handleRoute(false); updateProgressDisplay(); clearLoadingTimeout(); } catch (error) { console.error("Failed to load modules:", error); showLoadingFallback(); } } // ================= MODULE/LESSON SELECTION ================= function selectModule(moduleId) { const success = lessonEngine.setModuleById(moduleId); if (!success) return; track("module_start", { module: moduleId }); // Show lesson UI showLessonUI(); // Update URL const engineState = lessonEngine.getCurrentState(); updateHash(moduleId, engineState.lessonIndex); // 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) { item.classList.add("active"); } }); loadCurrentLesson(); resetSuccessIndicators(); // Close sidebar after selection on mobile if (window.innerWidth <= 768) { closeSidebar(); } } function selectLesson(moduleId, lessonIndex) { const currentState = lessonEngine.getCurrentState(); if (!currentState.module || currentState.module.id !== moduleId) { lessonEngine.setModuleById(moduleId); } lessonEngine.setLessonByIndex(lessonIndex); track("lesson_select", { module: moduleId, lesson: lessonIndex }); // Show lesson UI showLessonUI(); // Update URL updateHash(moduleId, lessonIndex); loadCurrentLesson(); // Close sidebar after selection on mobile if (window.innerWidth <= 768) { closeSidebar(); } } // ================= LESSON LOADING ================= function resetSuccessIndicators() { // Clear any pending animation timeout if (state.animationTimeout) { clearTimeout(state.animationTimeout); state.animationTimeout = null; } elements.codeEditor.classList.remove("success-highlight"); elements.lessonTitle.classList.remove("success-text"); elements.nextBtn.classList.remove("success"); elements.taskInstruction.classList.remove("success-instruction"); elements.runBtn.classList.remove("success"); elements.previewWrapper?.classList.remove("matched"); elements.previewWrapper?.classList.remove("completed-glow"); elements.previewSection?.classList.remove("matched"); // Remove completion badge if present const badge = document.querySelector(".completion-badge"); if (badge) badge.remove(); // Reset Run button text const runBtnText = elements.runBtn.querySelector("span"); if (runBtnText) runBtnText.textContent = t("run"); } function updateEditorForMode(mode) { const editorLabel = document.querySelector(".editor-label"); const modeConfig = { html: { placeholder: "Type HTML here... Try: nav>ul>li*3 then press Tab", label: "HTML Editor", cmMode: "html" }, tailwind: { placeholder: t("tailwindPlaceholder"), label: "Tailwind Classes", cmMode: "css" }, css: { placeholder: "Enter your CSS code here...", label: "CSS Editor", cmMode: "css" }, playground: { placeholder: "\n\n", label: "HTML & CSS", cmMode: "html" } }; const config = modeConfig[mode] || modeConfig.css; if (editorLabel) editorLabel.textContent = config.label; // Update CodeMirror mode if needed if (codeEditor && currentMode !== config.cmMode) { currentMode = config.cmMode; codeEditor.setMode(config.cmMode); } } function loadCurrentLesson() { const engineState = lessonEngine.getCurrentState(); if (!engineState.module || !engineState.lesson) { return; } const lesson = engineState.lesson; const mode = lesson.mode || engineState.module?.mode || "css"; const isPlayground = lesson.mode === "playground"; // Handle playground mode - hide instructions, full height editor, show random button if (isPlayground) { elements.instructionsSection?.classList.add("hidden"); elements.editorSection?.classList.add("playground-mode"); elements.randomTemplateBtn?.classList.remove("hidden"); } else { elements.instructionsSection?.classList.remove("hidden"); elements.editorSection?.classList.remove("playground-mode"); elements.randomTemplateBtn?.classList.add("hidden"); } // Add transition class for smooth content swap elements.editorSection?.classList.add("transitioning"); // Update UI based on mode updateEditorForMode(mode); // Update module name in pill if (elements.moduleName && engineState.module) { elements.moduleName.textContent = engineState.module.title; } // Reset any success indicators resetSuccessIndicators(); // Clear hints clearHint(); // Hide expected overlay state.showExpected = false; elements.expectedOverlay.classList.remove("visible"); elements.showExpectedBtn.textContent = t("showExpected"); elements.showExpectedBtn.classList.remove("btn-primary"); // Update UI renderLesson( elements.lessonTitle, elements.lessonDescription, elements.taskInstruction, elements.previewArea, null, // editorPrefix no longer used null, // codeInput no longer used (using CodeMirror) null, // editorSuffix no longer used lesson ); // Set user code in CodeMirror (clear history to prevent undo/redo across lessons) if (codeEditor) { codeEditor.setValueAndClearHistory(engineState.userCode); } // Update Run button text based on completion status if (engineState.isCompleted) { elements.runBtn.querySelector("span").textContent = t("rerun"); // Add completion badge if not present if (!document.querySelector(".completion-badge")) { const badge = document.createElement("span"); badge.className = "completion-badge"; badge.textContent = t("completed"); elements.lessonTitleRow.appendChild(badge); } // Show gradient border for completed lessons elements.previewWrapper?.classList.add("completed-glow"); } else { elements.runBtn.querySelector("span").textContent = t("run"); // Remove completion badge and border if exists const badge = document.querySelector(".completion-badge"); if (badge) badge.remove(); elements.previewWrapper?.classList.remove("completed-glow"); } // Update level indicator renderLevelIndicator(elements.levelIndicator, engineState.lessonIndex + 1, engineState.totalLessons); // Header pill shows module name + level (clickable link to return to lesson) if (elements.headerLevelPill && engineState.module) { const label = t("lessonLabel"); elements.headerLevelPill.innerHTML = `${engineState.module.title} ${label} ${engineState.lessonIndex + 1} / ${engineState.totalLessons}`; elements.headerLevelPill.href = `#${engineState.module.id}/${engineState.lessonIndex}`; } // Update active lesson in sidebar updateActiveLessonInSidebar(engineState.module.id, engineState.lessonIndex); // Update navigation buttons updateNavigationButtons(); // Update progress display updateProgressDisplay(); // Focus on the code editor if (codeEditor) { codeEditor.focus(); } // Render the expected/solution preview lessonEngine.renderExpectedPreview(); // Remove transition class after content is updated requestAnimationFrame(() => { elements.editorSection?.classList.remove("transitioning"); }); // Cache lesson display data for instant restore on reload try { localStorage.setItem( "codeCrispies.lessonCache", JSON.stringify({ moduleTitle: engineState.module?.title, lessonTitle: lesson.title, lessonDescription: lesson.description, taskInstruction: lesson.task, levelIndicator: elements.levelIndicator?.innerHTML, userCode: engineState.userCode, mode: mode }) ); } catch (e) { // Ignore storage errors } } // ================= LIVE PREVIEW ================= let previewTimer = null; function handleEditorChange(code) { if (previewTimer) { clearTimeout(previewTimer); } previewTimer = setTimeout(() => { runCode(); }, 800); } // ================= NAVIGATION ================= function updateNavigationButtons() { const engineState = lessonEngine.getCurrentState(); elements.prevBtn.disabled = !engineState.canGoPrev; elements.nextBtn.disabled = !engineState.canGoNext; elements.prevBtn.classList.toggle("btn-disabled", !engineState.canGoPrev); elements.nextBtn.classList.toggle("btn-disabled", !engineState.canGoNext); } function nextLesson() { const prevModuleId = lessonEngine.getCurrentState().module?.id; const success = lessonEngine.nextLesson(); if (success) { const newState = lessonEngine.getCurrentState(); track("lesson_nav", { direction: "next", module: newState.module.id, lesson: newState.lessonIndex }); // Update URL updateHash(newState.module.id, newState.lessonIndex); if (newState.module.id !== prevModuleId) { updateModuleHighlight(newState.module.id); updateSectionColor(getModuleSection(newState.module)); } loadCurrentLesson(); } } function prevLesson() { const prevModuleId = lessonEngine.getCurrentState().module?.id; const success = lessonEngine.previousLesson(); if (success) { const newState = lessonEngine.getCurrentState(); track("lesson_nav", { direction: "prev", module: newState.module.id, lesson: newState.lessonIndex }); // Update URL updateHash(newState.module.id, newState.lessonIndex); if (newState.module.id !== prevModuleId) { updateModuleHighlight(newState.module.id); updateSectionColor(getModuleSection(newState.module)); } loadCurrentLesson(); } } function updateModuleHighlight(moduleId) { const moduleItems = elements.moduleList.querySelectorAll(".module-header"); moduleItems.forEach((item) => { item.classList.remove("active"); if (item.dataset.moduleId === moduleId) { item.classList.add("active"); } }); } // ================= CODE EXECUTION ================= function resetCode() { // Reset editor to initial code for current lesson lessonEngine.reset(); const engineState = lessonEngine.getCurrentState(); const isPlayground = engineState.lesson?.mode === "playground"; track("reset_code", { module: engineState.module?.id, lesson: engineState.lessonIndex }); if (codeEditor && engineState.lesson) { // In playground mode, restore the last template if available if (isPlayground && state.lastPlaygroundTemplate) { codeEditor.setValue(state.lastPlaygroundTemplate.code); lessonEngine.applyUserCode(state.lastPlaygroundTemplate.code, true); } else { codeEditor.setValue(engineState.lesson.initialCode || ""); } } // Clear hints and success indicators clearHint(); resetSuccessIndicators(); // Update sidebar to remove completed status from this lesson const sidebarItem = document.querySelector( `.lesson-list-item[data-module-id="${engineState.module?.id}"][data-lesson-index="${engineState.lessonIndex}"]` ); if (sidebarItem) { sidebarItem.classList.remove("completed"); } // Update progress display updateProgressDisplay(); } function loadRandomTemplate() { const template = getRandomTemplate(); if (codeEditor && template) { track("playground_template", { template: template.name }); state.lastPlaygroundTemplate = template; codeEditor.setValue(template.code); // Apply the code to the preview lessonEngine.applyUserCode(template.code, true); } } function runCode() { const userCode = codeEditor ? codeEditor.getValue() : ""; const engineState = lessonEngine.getCurrentState(); const isPlayground = engineState.lesson?.mode === "playground"; // Rotate the Run button icon const runButtonImg = document.querySelector("#run-btn img"); if (runButtonImg) { const currentRotation = parseInt(runButtonImg.style.transform?.match(/\d+/)?.[0] || "0"); runButtonImg.style.transform = `rotate(${currentRotation + 180}deg)`; } // Apply the code to the preview via LessonEngine lessonEngine.applyUserCode(userCode, true); // Skip validation for playground mode if (isPlayground) { return; } // Validate code using LessonEngine const validationResult = lessonEngine.validateCode(); if (validationResult.isValid) { // Track lesson completion track("lesson_complete", { module: engineState.module?.id, lesson: engineState.lessonIndex }); // Show success hint showSuccessHint(validationResult.message || t("successMessage")); // Update Run button elements.runBtn.querySelector("span").textContent = t("rerun"); elements.runBtn.classList.add("success"); // Add completion badge if (!document.querySelector(".completion-badge")) { const badge = document.createElement("span"); badge.className = "completion-badge"; badge.textContent = t("completed"); elements.lessonTitleRow.appendChild(badge); } // Add success visual indicators elements.codeEditor.classList.add("success-highlight"); elements.lessonTitle.classList.add("success-text"); elements.nextBtn.classList.add("success"); elements.taskInstruction.classList.add("success-instruction"); // Show match animation (rotating gradient glow) const crispyQuotes = [ "Crissssssssspy!", "You did it!", "Good job!", "Nailed it!", "Perfect!", "Well done!", "Awesome!", "Nice work!", //"0x2B 0x31", "+1" ]; const randomQuote = crispyQuotes[Math.floor(Math.random() * crispyQuotes.length)]; elements.previewWrapper?.style.setProperty("--crispy-quote", `"${randomQuote}"`); elements.previewWrapper?.classList.add("matched"); elements.previewSection?.classList.add("matched"); state.animationTimeout = setTimeout(() => { elements.previewWrapper?.classList.remove("matched"); elements.previewSection?.classList.remove("matched"); // Keep the gradient border visible after animation elements.previewWrapper?.classList.add("completed-glow"); state.animationTimeout = null; }, 3500); updateNavigationButtons(); updateProgressDisplay(); } else { // Reset success indicators resetSuccessIndicators(); // Show hint with step progress const step = validationResult.validCases + 1; const total = validationResult.totalCases; // Only show hints if enabled if (!state.userSettings.disableFeedbackErrors) { showHint(validationResult.message || t("keepTrying"), step, total); } } } // ================= DIALOGS ================= function showHelp() { track("help_open"); elements.helpDialog.showModal(); } function closeHelpDialog() { elements.helpDialog.close(); } function showResetConfirmation() { elements.resetDialog.showModal(); } function closeResetDialog() { elements.resetDialog.close(); } function handleResetConfirm() { track("reset_progress"); lessonEngine.clearProgress(); closeResetDialog(); closeSidebar(); // Reload first module const modules = lessonEngine.modules; if (modules.length > 0) { selectModule(modules[0].id); } updateProgressDisplay(); } function showResetCodeConfirmation() { // Reset the checkbox state each time dialog is shown elements.resetCodeDontShow.checked = false; elements.resetCodeDialog.showModal(); } function closeResetCodeDialog() { elements.resetCodeDialog.close(); } function handleResetCodeConfirm() { // Save preference if checkbox is checked if (elements.resetCodeDontShow.checked) { state.userSettings.skipResetCodeConfirmation = true; saveUserSettings(); } closeResetCodeDialog(); resetCode(); } function handleResetCodeClick() { if (state.userSettings.skipResetCodeConfirmation) { resetCode(); } else { showResetCodeConfirmation(); } } // ================= SHARE DIALOG ================= function showShareDialog() { track("share_open"); const engineState = lessonEngine.getCurrentState(); if (engineState.module && engineState.lesson !== null) { const shareUrl = getShareableUrl(engineState.module.id, engineState.lessonIndex); elements.shareUrlInput.value = shareUrl; elements.copyFeedback.hidden = true; } elements.shareDialog.showModal(); } function closeShareDialog() { elements.shareDialog.close(); } async function copyShareUrl() { track("share_copy"); try { await navigator.clipboard.writeText(elements.shareUrlInput.value); elements.copyFeedback.hidden = false; setTimeout(() => { elements.copyFeedback.hidden = true; }, 2000); } catch (err) { // Fallback for older browsers elements.shareUrlInput.select(); document.execCommand("copy"); elements.copyFeedback.hidden = false; setTimeout(() => { elements.copyFeedback.hidden = true; }, 2000); } } // ================= SECTION EDUCATIONAL CONTENT ================= const sectionContent = { css: `

CSS (Cascading Style Sheets) is a stylesheet language that controls the visual presentation of HTML documents. While HTML defines the structure and content, CSS handles colors, typography, spacing, and layout. The "cascading" in CSS means rules can override each other based on specificity—allowing you to set defaults and then refine them for specific elements.

Introduced in 1996 to separate content from presentation, CSS enables one stylesheet to style multiple HTML pages, keeping design consistent and maintainable. Modern CSS includes powerful layout systems like Flexbox and Grid, custom properties (variables), and animations—all without JavaScript.

Selectors & Properties

CSS uses selectors to target HTML elements and apply styles. The most common selector is the class selector (.classname), which targets elements with a specific class attribute. You can also use element selectors (p, div), ID selectors (#id), and combinators to select nested elements.

Properties define what aspect of the element to style. Common properties include color for text color, background for backgrounds, padding for internal spacing, and margin for external spacing. Each property accepts specific value types like colors, lengths, or keywords.

Practice CSS Selectors Selectors Reference →
.button {
  background: steelblue;
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 4px;
}

.button:hover {
  background: darkslateblue;
}

The Box Model

Every HTML element is rendered as a rectangular box with four distinct layers. The content area holds your text or images. padding creates space inside the element between the content and border. The border wraps around the padding. Finally, margin creates space outside the element, separating it from neighbors.

By default, width only sets the content width. Adding padding and border increases the total size. Use box-sizing: border-box to include padding and border in the declared width, making layouts much more predictable.

Learn the CSS Box Model
.card {
  width: 300px;
  padding: 1rem;
  border: 2px solid gray;
  margin: 1rem;
  box-sizing: border-box;
}
/* Total width stays 300px */

Flexbox Layout

Flexbox is a one-dimensional layout system for arranging items in rows or columns. Apply display: flex to a container to enable it. Child elements become flex items that can grow, shrink, and align automatically.

Control alignment with justify-content (main axis: flex-start, center, space-between) and align-items (cross axis: stretch, center, flex-end). The gap property adds consistent spacing between items without margins.

Master CSS Flexbox Layout Flexbox Reference →
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
}

.nav-links {
  display: flex;
  gap: 2rem;
}

CSS Grid

CSS Grid is a two-dimensional layout system for creating complex row and column layouts. Enable it with display: grid, then define columns using grid-template-columns. The repeat() function creates multiple tracks, and fr units distribute available space proportionally.

Grid excels at page layouts and card grids. Use grid-template-columns: repeat(3, 1fr) for three equal columns, or repeat(auto-fill, minmax(250px, 1fr)) for responsive columns that wrap automatically.

Explore CSS Grid Layout Grid Reference →
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.responsive-grid {
  grid-template-columns:
    repeat(auto-fill, minmax(250px, 1fr));
}

Units & Variables

CSS supports multiple unit types for different use cases. Use px for fixed sizes, rem for scalable typography (relative to root font size), % for parent-relative sizing, and vh/vw for viewport-relative dimensions. Prefer rem for accessibility—it respects user font preferences.

CSS custom properties (variables) store reusable values. Define them with --name: value in :root for global access, then use them anywhere with var(--name). This makes themes and consistent design systems easy to maintain.

Study CSS Units & Variables
:root {
  --primary: steelblue;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --radius: 8px;
}

.card {
  background: var(--primary);
  padding: var(--spacing-md);
  border-radius: var(--radius);
}
`, html: `

HTML (HyperText Markup Language) is not a programming language—it's a markup language that describes the structure and content of web documents. Invented by Tim Berners-Lee in 1989, HTML uses tags like <p>, <h1>, and <a> to define paragraphs, headings, and links. The browser reads this markup and renders it as a visual page.

HTML documents form a hierarchical tree called the DOM (Document Object Model). Elements have parent-child relationships: a <ul> contains <li> children, a <form> contains <input> elements. Modern HTML5 includes native interactive elements like <dialog>, <details>, and form validation—features that previously required JavaScript.

Semantic Structure

HTML5 introduced semantic elements that convey meaning about content structure. Use <header> for introductory content, <nav> for navigation links, <main> for primary content, <article> for self-contained compositions, <section> for thematic groupings, and <footer> for closing content.

Semantic markup improves accessibility—screen readers announce element roles. It also helps SEO as search engines better understand your content hierarchy. Replace generic <div> containers with appropriate semantic elements whenever possible.

Learn HTML Semantic Elements HTML Reference →
<article>
  <header>
    <h1>Article Title</h1>
    <time datetime="2024-01-15">
      January 15, 2024
    </time>
  </header>
  <p>Article content...</p>
  <footer>
    <p>Written by Author</p>
  </footer>
</article>

Forms & Validation

HTML forms collect user input with elements like <input>, <select>, <textarea>, and <button>. Always pair inputs with <label> elements using matching for and id attributes—this is crucial for accessibility and usability.

Native validation attributes eliminate JavaScript for common cases: required prevents empty submissions, type="email" validates email format, minlength/maxlength control text length, and pattern accepts custom regex patterns. The browser handles error messages automatically.

Build HTML Forms
<form>
  <label for="email">Email</label>
  <input type="email" id="email"
         name="email" required
         placeholder="you@example.com">

  <label for="phone">Phone</label>
  <input type="tel" id="phone"
         pattern="[0-9]{3}-[0-9]{4}">

  <button type="submit">Send</button>
</form>

Interactive Elements

Modern HTML includes powerful interactive components that work without JavaScript. The <details> element creates native accordions—click <summary> to toggle visibility. Add the open attribute to start expanded. Multiple details elements create FAQ-style interfaces instantly.

The <dialog> element creates accessible modal dialogs. Call .showModal() in JavaScript to open it with backdrop and focus trapping built-in. Use <datalist> with inputs to provide autocomplete suggestions from a predefined list.

Try Interactive HTML Elements
<details>
  <summary>What is HTML?</summary>
  <p>HTML is the standard markup
     language for web pages.</p>
</details>

<dialog id="confirm">
  <h2>Confirm Action</h2>
  <p>Are you sure?</p>
  <button onclick="this.closest('dialog').close()">
    Close
  </button>
</dialog>

Tables & Lists

Use <table> exclusively for tabular data, never for page layout. Structure tables with <thead> for header rows, <tbody> for data rows, and optionally <tfoot> for summaries. Mark header cells with <th> (not <td>) and add scope="col" or scope="row" for accessibility.

Lists come in three flavors: <ul> for unordered bullet lists, <ol> for numbered sequences, and <dl> for definition lists (term/description pairs). Nest lists for hierarchical content like navigation menus or category trees.

Structure Data with HTML Tables
<table>
  <thead>
    <tr>
      <th scope="col">Product</th>
      <th scope="col">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Widget</td>
      <td>$9.99</td>
    </tr>
  </tbody>
</table>
`, tailwind: `

Tailwind CSS is a utility-first CSS framework that takes a radically different approach to styling. Instead of writing custom CSS classes like .card or .button, you compose designs using small, single-purpose utility classes directly in your HTML: class="p-4 bg-white rounded shadow".

This approach solves common CSS problems: no more specificity battles, no unused styles, no inventing class names. Tailwind's consistent spacing scale (p-1 through p-12), color palette (blue-500, gray-100), and responsive prefixes (md:, lg:) make building consistent, responsive interfaces fast and predictable.

For the underlying CSS concepts, see the CSS Section and CSS Reference.

Utility-First Basics

Tailwind CSS uses small, single-purpose utility classes applied directly in HTML. Instead of writing .btn { background: blue; padding: 1rem; } in a stylesheet, you write class="bg-blue-500 p-4" on the element. Each class does exactly one thing, making styles predictable and composable.

This approach eliminates context-switching between HTML and CSS files. Common utilities include text-lg for font size, font-bold for weight, rounded for border radius, and shadow for box shadows. Hover states use the hover: prefix like hover:bg-blue-600.

Start with Tailwind CSS Basics Coming Soon
<button class="bg-blue-500 text-white
               px-4 py-2 rounded-lg
               font-semibold shadow
               hover:bg-blue-600
               active:bg-blue-700">
  Click me
</button>

Spacing & Sizing

Tailwind's spacing scale is consistent and memorable. The pattern is simple: p-4 means padding of 1rem (16px), p-2 is 0.5rem, p-8 is 2rem. The same numbers work for margin (m-4), gap (gap-4), and space utilities. Use directional variants like px-4 (horizontal) or pt-2 (top only).

Width and height follow patterns too: w-full for 100%, w-1/2 for 50%, w-64 for fixed 16rem, h-screen for viewport height. Combine max-w-xl with mx-auto for centered containers with maximum widths.

Learn Tailwind Spacing & Sizing Coming Soon
<div class="max-w-xl mx-auto p-6">
  <div class="space-y-4">
    <div class="w-full h-32 bg-gray-200
                rounded-lg">
      Full width card
    </div>
    <div class="w-1/2 p-4 bg-gray-100">
      Half width, padded
    </div>
  </div>
</div>

Flexbox & Grid

Tailwind's layout utilities map directly to CSS flexbox and grid. Enable flex with flex, then control direction (flex-row, flex-col), alignment (items-center, justify-between), and wrapping (flex-wrap). The gap-4 utility adds consistent spacing between items.

For grid layouts, use grid with column definitions like grid-cols-3 for three equal columns or grid-cols-[200px_1fr] for custom track sizes. The col-span-2 utility makes items span multiple columns.

Build Layouts with Tailwind Coming Soon
<nav class="flex justify-between
            items-center p-4">
  <a href="/" class="font-bold">Logo</a>
  <ul class="flex gap-6">
    <li>Home</li>
    <li>About</li>
  </ul>
</nav>

<div class="grid grid-cols-3 gap-4">
  <div class="col-span-2">Wide</div>
  <div>Normal</div>
</div>

Responsive Design

Tailwind uses mobile-first responsive prefixes. Unprefixed utilities apply to all screen sizes. Add sm: (640px+), md: (768px+), lg: (1024px+), or xl: (1280px+) prefixes to apply styles at specific breakpoints and above.

Build responsive layouts by starting with mobile styles, then adding larger-screen overrides. For example, flex-col md:flex-row stacks items vertically on mobile and horizontally on medium screens. Use hidden md:block to show/hide elements at different sizes.

Tailwind Responsive Design Coming Soon
<div class="flex flex-col md:flex-row
            gap-4 p-4">
  <aside class="w-full md:w-64
               bg-gray-100 p-4">
    Sidebar (top on mobile)
  </aside>
  <main class="flex-1">
    Main content
  </main>
</div>

<p class="text-sm md:text-base lg:text-lg">
  Responsive typography
</p>
` }; // ================= REFERENCE CHEATSHEET CONTENT ================= const referenceContent = { css: `

CSS Properties Reference

Quick reference for commonly used CSS properties. Click any property to see syntax and examples.

Colors & Backgrounds

PropertyValuesExample
colornamed, hex, rgb(), hsl()color: steelblue;
backgroundcolor, image, gradientbackground: #f0f0f0;
background-colorcolor valuebackground-color: white;
background-imageurl(), gradientbackground-image: url(bg.png);
opacity0 to 1opacity: 0.8;

Typography

PropertyValuesExample
font-familyfont name, genericfont-family: system-ui, sans-serif;
font-sizepx, rem, em, %font-size: 1rem;
font-weightnormal, bold, 100-900font-weight: 600;
line-heightnumber, length, %line-height: 1.5;
text-alignleft, center, right, justifytext-align: center;
text-decorationnone, underline, line-throughtext-decoration: none;
text-transformnone, uppercase, lowercase, capitalizetext-transform: uppercase;
letter-spacinglengthletter-spacing: 0.05em;

Box Model

PropertyValuesExample
widthlength, %, auto, fit-contentwidth: 100%;
heightlength, %, autoheight: 200px;
min-widthlength, %, automin-width: 200px;
min-heightlength, %, automin-height: 100vh;
max-widthlength, %, nonemax-width: 600px;
paddinglength (1-4 values)padding: 1rem 2rem;
marginlength, auto (1-4 values)margin: 0 auto;
borderwidth style colorborder: 1px solid gray;
border-radiuslength (1-4 values)border-radius: 8px;
border-collapseseparate, collapseborder-collapse: collapse;
box-sizingcontent-box, border-boxbox-sizing: border-box;

Layout

PropertyValuesExample
displayblock, inline, flex, grid, nonedisplay: flex;
positionstatic, relative, absolute, fixed, stickyposition: relative;
top/right/bottom/leftlength, %, autotop: 0; left: 50%;
z-indexinteger, autoz-index: 10;
overflowvisible, hidden, scroll, autooverflow: auto;
floatnone, left, rightfloat: left;

Visual Effects

PropertyValuesExample
box-shadowx y blur spread colorbox-shadow: 0 4px 8px rgba(0,0,0,0.1);
text-shadowx y blur colortext-shadow: 1px 1px 2px gray;
transformtranslate, rotate, scaletransform: translateY(-2px);
cursorpointer, default, text, grabcursor: pointer;

Transitions & Animations

PropertyValuesExample
transitionproperty duration easingtransition: all 0.3s ease;
transition-propertyall, none, specifictransition-property: opacity;
transition-durationtime (s, ms)transition-duration: 0.3s;
transition-timing-functionease, linear, ease-in-outtransition-timing-function: ease-out;
animationname duration timing delayanimation: fade 1s ease-in;
animation-name@keyframes nameanimation-name: slide;
animation-durationtime (s, ms)animation-duration: 2s;
animation-iteration-countnumber, infiniteanimation-iteration-count: infinite;
animation-fill-modenone, forwards, backwards, bothanimation-fill-mode: forwards;

CSS Variables

SyntaxDescriptionExample
--nameDefine custom property--primary: steelblue;
var(--name)Use custom propertycolor: var(--primary);
var(--name, fallback)With fallback valuecolor: var(--accent, blue);
:root { }Global scope:root { --gap: 1rem; }

See also: Flexbox Reference | Grid Reference | Selectors Reference

`, selectors: `

CSS Selectors Reference

Selectors determine which HTML elements your styles apply to. More specific selectors override less specific ones.

Basic Selectors

SelectorDescriptionExample
elementAll elements of typep { ... }
.classElements with class.btn { ... }
#idElement with ID#header { ... }
*All elements* { box-sizing: border-box; }
[attr]Elements with attribute[disabled] { ... }
[attr="value"]Attribute equals value[type="text"] { ... }

Combinators

SelectorDescriptionExample
A BB inside A (descendant)nav a { ... }
A > BB direct child of Aul > li { ... }
A + BB immediately after Ah2 + p { ... }
A ~ BB after A (sibling)h2 ~ p { ... }
A, BA or B (grouping)h1, h2, h3 { ... }

Pseudo-classes

SelectorDescriptionExample
:hoverMouse over elementa:hover { color: blue; }
:focusElement has focusinput:focus { outline: 2px solid; }
:activeBeing clickedbutton:active { transform: scale(0.98); }
:first-childFirst child elementli:first-child { ... }
:last-childLast child elementli:last-child { ... }
:nth-child(n)nth child (1-based)tr:nth-child(odd) { ... }
:not(sel)Elements not matching:not(.hidden) { ... }
:checkedChecked inputsinput:checked { ... }
:disabledDisabled form elementsbutton:disabled { opacity: 0.5; }

Pseudo-elements

SelectorDescriptionExample
::beforeInsert before content.icon::before { content: "→"; }
::afterInsert after content.link::after { content: "↗"; }
::first-letterFirst letter of textp::first-letter { font-size: 2em; }
::first-lineFirst line of textp::first-line { font-weight: bold; }
::placeholderInput placeholder textinput::placeholder { color: gray; }
::selectionSelected/highlighted text::selection { background: yellow; }

Specificity (Highest to Lowest)

  1. Inline styles - style="..." attribute (1000 points)
  2. ID selectors - #id (100 points)
  3. Class/pseudo-class/attribute - .class, :hover, [attr] (10 points)
  4. Element/pseudo-element - div, ::before (1 point)
  5. Universal selector - * (0 points)

Practice: Basic Selectors Lessons | Advanced Selectors

`, flexbox: `

Flexbox Reference

Flexbox is a one-dimensional layout system. Apply display: flex to a container, and its children become flex items.

Container Properties

PropertyValuesDescription
displayflex, inline-flexEnable flex container
flex-directionrow, row-reverse, column, column-reverseMain axis direction
flex-wrapnowrap, wrap, wrap-reverseAllow items to wrap
justify-contentflex-start, flex-end, center, space-between, space-around, space-evenlyMain axis alignment
align-itemsstretch, flex-start, flex-end, center, baselineCross axis alignment
align-contentstretch, flex-start, flex-end, center, space-between, space-aroundMulti-line alignment
gaplengthSpace between items

Item Properties

PropertyValuesDescription
flex-grownumber (default: 0)How much item grows
flex-shrinknumber (default: 1)How much item shrinks
flex-basislength, autoInitial size before grow/shrink
flexgrow shrink basisShorthand (e.g., flex: 1)
align-selfauto, flex-start, flex-end, center, stretchOverride align-items
orderinteger (default: 0)Visual order of item

Common Patterns

/* Center horizontally and vertically */
.center {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* Space items evenly */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* Equal-width columns */
.columns {
  display: flex;
  gap: 1rem;
}
.columns > * {
  flex: 1;
}

/* Sticky footer */
.page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
.main { flex: 1; }

Practice: Flexbox Lessons | Compare: CSS Grid

`, grid: `

CSS Grid Reference

CSS Grid is a two-dimensional layout system for complex row and column layouts. Apply display: grid to a container.

Container Properties

PropertyValuesDescription
displaygrid, inline-gridEnable grid container
grid-template-columnstrack sizesDefine column widths
grid-template-rowstrack sizesDefine row heights
gaplengthSpace between cells
justify-itemsstart, end, center, stretchHorizontal alignment in cells
align-itemsstart, end, center, stretchVertical alignment in cells
justify-contentstart, end, center, space-between, space-aroundHorizontal grid alignment
align-contentstart, end, center, space-between, space-aroundVertical grid alignment

Track Sizing

ValueDescriptionExample
px, rem, %Fixed or relative sizes200px 1rem 50%
frFraction of available space1fr 2fr 1fr
autoSize to contentauto 1fr auto
minmax(min, max)Size rangeminmax(100px, 1fr)
repeat(n, size)Repeat n timesrepeat(3, 1fr)
repeat(auto-fill, ...)Fill available spacerepeat(auto-fill, minmax(200px, 1fr))
repeat(auto-fit, ...)Fill and stretchrepeat(auto-fit, minmax(200px, 1fr))

Item Properties

PropertyValuesDescription
grid-columnstart / endColumn span
grid-rowstart / endRow span
grid-arearow-start / col-start / row-end / col-endShorthand for both
justify-selfstart, end, center, stretchHorizontal alignment
align-selfstart, end, center, stretchVertical alignment

Common Patterns

/* 3 equal columns */
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

/* Responsive auto-fill */
.responsive {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 1rem;
}

/* Holy grail layout */
.layout {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;
}

/* Span multiple cells */
.wide {
  grid-column: 1 / -1; /* Full width */
}
.tall {
  grid-row: span 2; /* 2 rows */
}

Practice: Grid Lessons | Compare: Flexbox

`, html: `

HTML Elements Reference

Semantic HTML elements that describe their meaning to browsers and assistive technologies.

Document Structure

ElementPurposeExample
<html>Root element<html lang="en">
<head>Document metadataContains title, meta, links
<body>Document contentAll visible content
<header>Introductory contentLogo, nav, title
<nav>Navigation linksMain site navigation
<main>Main content (one per page)Primary page content
<footer>Footer contentCopyright, links
<aside>Sidebar/tangential contentRelated links, ads

Content Sectioning

ElementPurposeExample
<article>Self-contained contentBlog post, comment
<section>Thematic groupingChapter, tab panel
<h1>-<h6>Headings (hierarchy)One h1 per page, nest others
<p>ParagraphBlock of text
<div>Generic containerWhen no semantic element fits

Text Content

ElementPurposeExample
<a>Hyperlink<a href="url">Link</a>
<strong>Important text (bold)<strong>Warning</strong>
<em>Emphasized text (italic)<em>really</em>
<code>Inline code<code>const x</code>
<pre>Preformatted textCode blocks
<blockquote>Block quotation<blockquote cite="src">
<span>Generic inline containerStyling inline text
<br>Line breakAddress, poem lines
<hr>Thematic breakScene change, topic shift

Semantic Inline

ElementPurposeExample
<time>Date/time<time datetime="2024-01-15">
<mark>Highlighted text<mark>important</mark>
<small>Fine printLegal text, copyright
<abbr>Abbreviation<abbr title="HyperText">HTML</abbr>
<kbd>Keyboard input<kbd>Ctrl+C</kbd>
<sub>SubscriptH2O
<sup>Superscriptx2
<ins>Inserted textShows additions
<del>Deleted textShows removals

Lists

ElementPurposeExample
<ul>Unordered listBulleted items
<ol>Ordered listNumbered steps
<li>List itemInside ul or ol
<dl>Description listTerm/definition pairs
<dt>Description termThe term being defined
<dd>Description detailsDefinition of the term

Forms

ElementPurposeKey Attributes
<form>Form containeraction, method
<input>Input fieldtype, name, required, placeholder
<textarea>Multi-line textrows, cols, name
<select>Dropdown menuname, multiple
<option>Select optionvalue, selected
<button>Clickable buttontype (submit/button/reset)
<label>Input labelfor (matches input id)
<fieldset>Group form controlsWith legend for title

Media

ElementPurposeKey Attributes
<img>Imagesrc, alt (required), width, height
<figure>Self-contained mediaContains img + figcaption
<figcaption>Figure captionDescription of figure
<video>Video playersrc, controls, autoplay
<audio>Audio playersrc, controls
<picture>Responsive imagesContains source + img

Tables

ElementPurposeNotes
<table>Table containerFor tabular data only
<thead>Table header groupContains header rows
<tbody>Table body groupContains data rows
<tfoot>Table footer groupSummary row
<tr>Table rowContains cells
<th>Header cellscope="col" or "row"
<td>Data cellcolspan, rowspan for spanning

Interactive Elements

ElementPurposeKey Attributes
<details>Expandable contentopen (default expanded)
<summary>Details toggle labelFirst child of details
<dialog>Modal/popup dialogopen, use showModal()
<progress>Progress barvalue, max
<meter>Scalar measurementvalue, min, max, low, high
<datalist>Input suggestionsid (link via input list attr)

Learn: HTML Section | Style with: CSS Properties

` }; // ================= URL ROUTING & PAGE SWITCHING ================= function initRouter() { // Handle browser back/forward window.addEventListener("popstate", handlePopState); } function handlePopState() { handleRoute(false); } /** * Strip HTML tags from a string for meta descriptions */ function stripHtml(html) { const tmp = document.createElement("div"); tmp.innerHTML = html || ""; return tmp.textContent || tmp.innerText || ""; } /** * Update page meta tags based on current route for SEO */ function updatePageMeta(route) { const defaultTitle = "Code Crispies - Learn HTML & CSS Interactively | Free Coding Practice"; const defaultDesc = "Master HTML, CSS, and Tailwind through hands-on coding exercises. Free, open-source learning platform with instant feedback. No account required."; let title = defaultTitle; let description = defaultDesc; if (!route) { document.title = title; return; } switch (route.type) { case RouteType.HOME: // Use defaults break; case RouteType.SECTION: { const sectionNames = { css: "CSS", html: "HTML", tailwind: "Tailwind CSS" }; const sectionName = sectionNames[route.sectionId] || route.sectionId; title = `${sectionName} Lessons - Code Crispies | Learn ${sectionName}`; description = `Learn ${sectionName} through interactive coding exercises. Hands-on practice with instant feedback.`; break; } case RouteType.LESSON: { const module = lessonEngine.modules.find((m) => m.id === route.moduleId); const lesson = module?.lessons[route.lessonIndex]; if (module && lesson) { title = `${lesson.title} - ${module.title} | Code Crispies`; const lessonDesc = stripHtml(lesson.description || lesson.task); description = lessonDesc.length > 155 ? lessonDesc.slice(0, 152) + "..." : lessonDesc || defaultDesc; } break; } case RouteType.REFERENCE: { const refNames = { css: "CSS Properties", selectors: "CSS Selectors", flexbox: "Flexbox", grid: "CSS Grid", html: "HTML Elements" }; const refName = refNames[route.refId] || "Reference"; title = `${refName} Reference - Code Crispies`; description = `Quick reference guide for ${refName}. Syntax, examples, and common patterns for web development.`; break; } } // Update document title document.title = title; // Update meta description const metaDesc = document.querySelector('meta[name="description"]'); if (metaDesc) metaDesc.setAttribute("content", description); // Update Open Graph tags const ogTitle = document.querySelector('meta[property="og:title"]'); const ogDesc = document.querySelector('meta[property="og:description"]'); if (ogTitle) ogTitle.setAttribute("content", title.replace(" | Code Crispies", "").replace(" - Code Crispies", "")); if (ogDesc) ogDesc.setAttribute("content", description); // Update Twitter tags const twitterTitle = document.querySelector('meta[name="twitter:title"]'); const twitterDesc = document.querySelector('meta[name="twitter:description"]'); if (twitterTitle) twitterTitle.setAttribute("content", title.replace(" | Code Crispies", "").replace(" - Code Crispies", "")); if (twitterDesc) twitterDesc.setAttribute("content", description); } /** * 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: showReferencePage(route.refId); break; case RouteType.LESSON: navigateToLesson(route.moduleId, route.lessonIndex, shouldUpdateUrl); break; case RouteType.LANGUAGE: // Switch language and redirect to home track("language_url", { language: route.lang }); setLanguage(route.lang); applyTranslations(); // Sync language dropdown if (elements.langSelect) { elements.langSelect.value = route.lang; } // Reload modules in new language and re-render sidebar const langModules = loadModules(route.lang); lessonEngine.setModules(langModules); renderModuleList(elements.moduleList, langModules, selectModule, selectLesson); updateProgressDisplay(); history.replaceState(null, "", window.location.pathname); showLandingPage(); return; // Skip updateNavHighlight/updatePageMeta since we're redirecting default: showLandingPage(); } updateNavHighlight(route); updatePageMeta(route); } /** * Hide all page containers */ function hideAllPages() { elements.landingPage?.classList.add("hidden"); elements.sectionPage?.classList.add("hidden"); elements.referencePage?.classList.add("hidden"); elements.gameLayout?.classList.add("hidden"); } /** * Update section color coding on body * @param {string|null} sectionId - Section ID (css, html, tailwind) or null to reset */ function updateSectionColor(sectionId) { if (sectionId) { document.body.setAttribute("data-section", sectionId); } else { document.body.removeAttribute("data-section"); } } /** * Show home landing page */ function showLandingPage() { hideAllPages(); elements.landingPage?.classList.remove("hidden"); window.scrollTo(0, 0); // Reset section color on landing page updateSectionColor(null); // Update section progress on landing page updateLandingProgress(); // Render footer lesson links renderFooterLessonLinks(); } /** * Render module links in the landing page footer, grouped by section */ function renderFooterLessonLinks() { if (!elements.footerLessonLinks) return; const modules = lessonEngine.modules || []; const sectionGroups = { css: [], html: [] }; modules.forEach((module) => { if (module.excludeFromProgress) return; const sectionId = getModuleSection(module); if (sectionId && sectionGroups[sectionId]) { sectionGroups[sectionId].push(module); } }); let html = ""; Object.entries(sectionGroups).forEach(([sectionId, sectionModules]) => { if (sectionModules.length === 0) return; const sectionName = sectionId.toUpperCase(); html += `"; }); elements.footerLessonLinks.innerHTML = html; } /** * 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 = `${total} lessons to explore`; } else { progressEl.textContent = ""; } } }); } /** * Show section landing page */ function showSectionPage(sectionId) { hideAllPages(); elements.sectionPage?.classList.remove("hidden"); window.scrollTo(0, 0); // Update section color updateSectionColor(sectionId); // Track section page view track("section_view", { section: sectionId }); 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; // Inject educational content (includes integrated module links) if (elements.sectionIntro && sectionContent[sectionId]) { elements.sectionIntro.innerHTML = sectionContent[sectionId]; // Highlight code blocks with CodeMirror highlightSectionCodeBlocks(); } // Get modules for this section to calculate progress 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`; } /** * Show reference/cheatsheet page */ function showReferencePage(refId) { hideAllPages(); elements.referencePage?.classList.remove("hidden"); window.scrollTo(0, 0); // Default to CSS if no refId const activeRef = refId || "css"; // Map reference to section for color coding const refToSection = { css: "css", selectors: "css", flexbox: "css", grid: "css", html: "html" }; updateSectionColor(refToSection[activeRef] || "css"); // Track reference page view track("reference_view", { ref: activeRef }); // Update nav highlighting const navLinks = elements.referenceNav?.querySelectorAll(".ref-nav-link"); navLinks?.forEach((link) => { link.classList.toggle("active", link.dataset.ref === activeRef); }); // Load reference content if (elements.referenceBody && referenceContent[activeRef]) { elements.referenceBody.innerHTML = referenceContent[activeRef]; // Make large tables collapsible makeTablesCollapsible(); // Highlight code blocks highlightReferenceCodeBlocks(); } else if (elements.referenceBody) { elements.referenceBody.innerHTML = `

Reference for "${activeRef}" coming soon...

`; } } /** * Make reference tables with more than 5 rows collapsible */ function makeTablesCollapsible() { const tables = elements.referenceBody?.querySelectorAll(".ref-table"); const ROW_LIMIT = 5; tables?.forEach((table) => { const tbody = table.querySelector("tbody"); if (!tbody) return; const rows = Array.from(tbody.querySelectorAll("tr")); if (rows.length <= ROW_LIMIT) return; // Split rows: visible (first 5) and hidden (rest) const visibleRows = rows.slice(0, ROW_LIMIT); const hiddenRows = rows.slice(ROW_LIMIT); // Create new tbody with only visible rows tbody.innerHTML = ""; visibleRows.forEach((row) => tbody.appendChild(row)); // Create details/summary for hidden rows const details = document.createElement("details"); details.className = "ref-table-more"; const summary = document.createElement("summary"); summary.textContent = `Show ${hiddenRows.length} more...`; details.appendChild(summary); // Create a wrapper table for hidden rows (same structure) const hiddenTable = document.createElement("table"); hiddenTable.className = "ref-table ref-table-continuation"; const hiddenTbody = document.createElement("tbody"); hiddenRows.forEach((row) => hiddenTbody.appendChild(row)); hiddenTable.appendChild(hiddenTbody); details.appendChild(hiddenTable); // Insert after the original table table.parentNode.insertBefore(details, table.nextSibling); }); } /** * Highlight code blocks in reference pages with CodeMirror */ function highlightReferenceCodeBlocks() { // Clean up previous views sectionCodeViews.forEach((view) => view.destroy()); sectionCodeViews = []; const codeBlocks = elements.referenceBody?.querySelectorAll("pre code"); codeBlocks?.forEach((block) => { const code = block.textContent; const isHtml = block.classList.contains("language-html"); const parent = block.parentElement; parent.innerHTML = ""; parent.classList.add("cm-code-block"); const view = new EditorView({ state: EditorState.create({ doc: code, extensions: [isHtml ? html() : css(), crispyEditorTheme, readOnlyTheme, EditorState.readOnly.of(true), EditorView.editable.of(false)] }), parent }); sectionCodeViews.push(view); }); } /** * 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.REFERENCE && link.dataset.section === "reference") { link.classList.add("active"); } else if (route?.type === RouteType.LESSON) { // Highlight section based on module's inferred section // Skip highlighting for modules excluded from progress (welcome, playground, goodbye) const module = lessonEngine.modules.find((m) => m.id === route.moduleId); if (module && !module.excludeFromProgress) { 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) { // Invalid module - fallback to first module const fallbackModule = lessonEngine.modules[0]; if (fallbackModule) { replaceHash(fallbackModule.id, 0); lessonEngine.setModuleById(fallbackModule.id); lessonEngine.setLessonByIndex(0); loadCurrentLesson(); updateModuleHighlight(fallbackModule.id); updateSectionColor(getModuleSection(fallbackModule)); } return; } // Update section color based on module updateSectionColor(getModuleSection(module)); // Validate lessonIndex is in bounds if (lessonIndex < 0 || lessonIndex >= module.lessons.length) { // Invalid lesson - go to first lesson of module replaceHash(moduleId, 0); lessonEngine.setModuleById(moduleId); lessonEngine.setLessonByIndex(0); loadCurrentLesson(); updateModuleHighlight(moduleId); return; } // Valid navigation lessonEngine.setModuleById(moduleId); lessonEngine.setLessonByIndex(lessonIndex); if (shouldUpdateUrl) { updateHash(moduleId, lessonIndex); } loadCurrentLesson(); updateModuleHighlight(moduleId); } // ================= INITIALIZATION ================= function initCodeEditor() { const container = elements.editorContent; if (!container) return; // Remove the textarea - CodeMirror will replace it const textarea = container.querySelector("textarea"); if (textarea) { textarea.remove(); } // Initialize CodeMirror codeEditor = new CodeEditor(container, { mode: currentMode, placeholder: "Type your code here...", onChange: handleEditorChange }); codeEditor.init(""); } function init() { // Initialize i18n before anything else initI18n(); loadUserSettings(); // Restore cached lesson content immediately to avoid "Loading..." flash restoreLessonCache(); // Initialize CodeMirror editor initCodeEditor(); // Set timeout to show fallback if loading takes too long loadingTimeout = setTimeout(showLoadingFallback, 3000); // Load modules after editor is ready initializeModules(); // Initialize URL router for shareable links initRouter(); // Sidebar controls elements.menuBtn.addEventListener("click", openSidebar); elements.closeSidebar.addEventListener("click", closeSidebar); elements.sidebarBackdrop.addEventListener("click", closeSidebar); // Logo click - navigate to home landing elements.logoLink.addEventListener("click", (e) => { e.preventDefault(); navigateTo(""); showLandingPage(); }); // Language select elements.langSelect.value = getLanguage(); elements.langSelect.addEventListener("change", (e) => changeLanguage(e.target.value)); // Expected result toggle elements.showExpectedBtn.addEventListener("click", toggleExpectedResult); // Navigation elements.prevBtn.addEventListener("click", prevLesson); elements.nextBtn.addEventListener("click", nextLesson); elements.runBtn.addEventListener("click", runCode); // Editor tools elements.undoBtn.addEventListener("click", () => { if (codeEditor) codeEditor.undo(); }); elements.redoBtn.addEventListener("click", () => { if (codeEditor) codeEditor.redo(); }); elements.resetCodeBtn.addEventListener("click", handleResetCodeClick); elements.randomTemplateBtn.addEventListener("click", loadRandomTemplate); elements.shareBtn.addEventListener("click", showShareDialog); // Dialogs elements.helpBtn.addEventListener("click", showHelp); elements.helpDialogClose.addEventListener("click", closeHelpDialog); elements.helpDialog.addEventListener("click", (e) => { if (e.target === elements.helpDialog) closeHelpDialog(); }); elements.resetBtn.addEventListener("click", showResetConfirmation); elements.resetDialogClose.addEventListener("click", closeResetDialog); elements.resetDialog.addEventListener("click", (e) => { if (e.target === elements.resetDialog) closeResetDialog(); }); elements.cancelReset.addEventListener("click", closeResetDialog); elements.confirmReset.addEventListener("click", handleResetConfirm); elements.resetCodeDialogClose.addEventListener("click", closeResetCodeDialog); elements.resetCodeDialog.addEventListener("click", (e) => { if (e.target === elements.resetCodeDialog) closeResetCodeDialog(); }); elements.cancelResetCode.addEventListener("click", closeResetCodeDialog); elements.confirmResetCode.addEventListener("click", handleResetCodeConfirm); // Share dialog elements.shareDialogClose.addEventListener("click", closeShareDialog); elements.shareDialog.addEventListener("click", (e) => { if (e.target === elements.shareDialog) closeShareDialog(); }); elements.copyUrlBtn.addEventListener("click", copyShareUrl); // Settings elements.disableFeedbackToggle.addEventListener("change", (e) => { state.userSettings.disableFeedbackErrors = !e.target.checked; saveUserSettings(); }); // Click on editor content to focus CodeMirror elements.editorContent?.addEventListener("click", () => { if (codeEditor) codeEditor.focus(); }); // Keyboard shortcuts document.addEventListener("keydown", (e) => { // Ctrl+Enter to run code if (e.ctrlKey && e.key === "Enter") { runCode(); e.preventDefault(); } // Escape to close sidebar (dialogs handle Escape natively) if (e.key === "Escape") { closeSidebar(); } }); // Landing page tracking (event delegation) elements.landingPage?.addEventListener("click", (e) => { const target = e.target.closest("a"); if (!target) return; if (target.classList.contains("cta-button")) { track("landing_cta", { href: target.getAttribute("href") }); } else if (target.classList.contains("section-card")) { track("landing_section", { section: target.dataset.section }); } else if (target.closest(".footer-support")) { track("support_click", { location: "landing" }); } }); } // Start the application init();