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:
@@ -5,6 +5,7 @@
|
||||
"description": "Welcome to Code Crispies - your interactive web development learning platform",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"excludeFromProgress": true,
|
||||
"lessons": [
|
||||
{
|
||||
"id": "get-started",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"description": "Congratulations on completing your learning journey!",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"excludeFromProgress": true,
|
||||
"lessons": [
|
||||
{
|
||||
"id": "congratulations",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"description": "مرحباً بك في Code Crispies - منصتك التفاعلية لتعلم تطوير الويب",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"excludeFromProgress": true,
|
||||
"lessons": [
|
||||
{
|
||||
"id": "get-started",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"description": "Willkommen bei Code Crispies - deine interaktive Lernplattform für Webentwicklung",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"excludeFromProgress": true,
|
||||
"lessons": [
|
||||
{
|
||||
"id": "get-started",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"description": "Bienvenido a Code Crispies - tu plataforma interactiva de aprendizaje de desarrollo web",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"excludeFromProgress": true,
|
||||
"lessons": [
|
||||
{
|
||||
"id": "get-started",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"description": "Witaj w Code Crispies - twojej interaktywnej platformie do nauki tworzenia stron",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"excludeFromProgress": true,
|
||||
"lessons": [
|
||||
{
|
||||
"id": "get-started",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"description": "Ласкаво просимо до Code Crispies - вашої інтерактивної платформи для вивчення веб-розробки",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"excludeFromProgress": true,
|
||||
"lessons": [
|
||||
{
|
||||
"id": "get-started",
|
||||
|
||||
240
src/app.js
240
src/app.js
@@ -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
|
||||
|
||||
75
src/config/sections.js
Normal file
75
src/config/sections.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Section definitions for Code Crispies
|
||||
* Sections group related modules (CSS, HTML, Tailwind)
|
||||
*/
|
||||
|
||||
export const sections = {
|
||||
css: {
|
||||
id: "css",
|
||||
title: "CSS",
|
||||
description: "Styling, layout, and animations",
|
||||
color: "#264de4",
|
||||
order: 1
|
||||
},
|
||||
html: {
|
||||
id: "html",
|
||||
title: "HTML",
|
||||
description: "Semantic markup and native elements",
|
||||
color: "#e34c26",
|
||||
order: 2
|
||||
},
|
||||
tailwind: {
|
||||
id: "tailwind",
|
||||
title: "Tailwind CSS",
|
||||
description: "Utility-first CSS framework",
|
||||
color: "#06b6d4",
|
||||
order: 3
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get section by ID
|
||||
* @param {string} sectionId
|
||||
* @returns {object|null}
|
||||
*/
|
||||
export function getSection(sectionId) {
|
||||
return sections[sectionId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sections sorted by order
|
||||
* @returns {object[]}
|
||||
*/
|
||||
export function getSectionList() {
|
||||
return Object.values(sections).sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer section from module mode
|
||||
* @param {object} module
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getModuleSection(module) {
|
||||
// Explicit section takes precedence
|
||||
if (module.section) return module.section;
|
||||
|
||||
// Infer from mode
|
||||
const mode = module.mode || "css";
|
||||
if (mode === "html") return "html";
|
||||
if (mode === "tailwind") return "tailwind";
|
||||
return "css";
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter modules by section
|
||||
* @param {object[]} modules
|
||||
* @param {string} sectionId
|
||||
* @returns {object[]}
|
||||
*/
|
||||
export function getModulesBySection(modules, sectionId) {
|
||||
return modules.filter((m) => {
|
||||
// Skip excluded modules (welcome, goodbye)
|
||||
if (m.excludeFromProgress) return false;
|
||||
return getModuleSection(m) === sectionId;
|
||||
});
|
||||
}
|
||||
@@ -1,26 +1,81 @@
|
||||
/**
|
||||
* URL Router for Code Crispies
|
||||
* Handles hash-based routing for shareable lesson links
|
||||
* Format: #module-id/lesson-index (e.g., #flexbox/2)
|
||||
* Handles hash-based routing for pages, sections, and lessons
|
||||
*
|
||||
* Route formats:
|
||||
* - # -> Home landing page
|
||||
* - #css -> CSS section landing
|
||||
* - #html -> HTML section landing
|
||||
* - #tailwind -> Tailwind section landing
|
||||
* - #reference/css -> CSS cheatsheet
|
||||
* - #module/index -> Lesson (e.g., #flexbox/2)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse current URL hash into module and lesson info
|
||||
* @returns {{ moduleId: string, lessonIndex: number } | null}
|
||||
* Route types
|
||||
*/
|
||||
export const RouteType = {
|
||||
HOME: "home",
|
||||
SECTION: "section",
|
||||
REFERENCE: "reference",
|
||||
LESSON: "lesson"
|
||||
};
|
||||
|
||||
/**
|
||||
* Valid section IDs
|
||||
*/
|
||||
const SECTIONS = ["css", "html", "tailwind"];
|
||||
|
||||
/**
|
||||
* Parse current URL hash into route info
|
||||
* @returns {{ type: string, moduleId?: string, lessonIndex?: number, sectionId?: string, refId?: string } | null}
|
||||
*/
|
||||
export function parseHash() {
|
||||
const hash = window.location.hash.slice(1); // Remove '#'
|
||||
if (!hash) return null;
|
||||
|
||||
// Empty hash = home
|
||||
if (!hash) {
|
||||
return { type: RouteType.HOME };
|
||||
}
|
||||
|
||||
const parts = hash.split("/");
|
||||
if (parts.length !== 2) return null;
|
||||
|
||||
// Single segment routes
|
||||
if (parts.length === 1) {
|
||||
const segment = parts[0];
|
||||
|
||||
// Section landing pages
|
||||
if (SECTIONS.includes(segment)) {
|
||||
return { type: RouteType.SECTION, sectionId: segment };
|
||||
}
|
||||
|
||||
// Reference index (no specific ref)
|
||||
if (segment === "reference") {
|
||||
return { type: RouteType.REFERENCE, refId: null };
|
||||
}
|
||||
|
||||
// Single segment could be module with implicit lesson 0
|
||||
return { type: RouteType.LESSON, moduleId: segment, lessonIndex: 0 };
|
||||
}
|
||||
|
||||
// Two segment routes
|
||||
if (parts.length === 2) {
|
||||
// Reference subpages
|
||||
if (parts[0] === "reference") {
|
||||
return { type: RouteType.REFERENCE, refId: parts[1] };
|
||||
}
|
||||
|
||||
// Lesson route (existing behavior)
|
||||
const moduleId = parts[0];
|
||||
const lessonIndex = parseInt(parts[1], 10);
|
||||
|
||||
if (!moduleId || isNaN(lessonIndex) || lessonIndex < 0) return null;
|
||||
if (moduleId && !isNaN(lessonIndex) && lessonIndex >= 0) {
|
||||
return { type: RouteType.LESSON, moduleId, lessonIndex };
|
||||
}
|
||||
}
|
||||
|
||||
return { moduleId, lessonIndex };
|
||||
// Invalid route
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,6 +90,17 @@ export function updateHash(moduleId, lessonIndex) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update URL to a specific route
|
||||
* @param {string} route - The route string (e.g., "css", "reference/flexbox", "")
|
||||
*/
|
||||
export function navigateTo(route) {
|
||||
const newHash = route ? `#${route}` : "#";
|
||||
if (window.location.hash !== newHash) {
|
||||
history.pushState(null, "", newHash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace URL hash without history entry (for invalid URL fallbacks)
|
||||
* @param {string} moduleId
|
||||
@@ -45,6 +111,15 @@ export function replaceHash(moduleId, lessonIndex) {
|
||||
history.replaceState(null, "", newHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace URL to a specific route without history entry
|
||||
* @param {string} route - The route string
|
||||
*/
|
||||
export function replaceTo(route) {
|
||||
const newHash = route ? `#${route}` : "#";
|
||||
history.replaceState(null, "", newHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full shareable URL for current lesson
|
||||
* @param {string} moduleId
|
||||
@@ -55,3 +130,11 @@ export function getShareableUrl(moduleId, lessonIndex) {
|
||||
const base = window.location.origin + window.location.pathname;
|
||||
return `${base}#${moduleId}/${lessonIndex}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid section IDs
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getSectionIds() {
|
||||
return [...SECTIONS];
|
||||
}
|
||||
|
||||
@@ -463,9 +463,12 @@ export class LessonEngine {
|
||||
let totalCompleted = 0;
|
||||
|
||||
this.modules.forEach((module) => {
|
||||
// Skip modules excluded from progress (e.g., welcome, goodbye)
|
||||
if (module.excludeFromProgress) return;
|
||||
|
||||
totalLessons += module.lessons.length;
|
||||
const progress = this.userProgress[module.id];
|
||||
if (progress && progress.completed) {
|
||||
if (progress?.completed) {
|
||||
totalCompleted += progress.completed.length;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
<button id="menu-btn" class="menu-toggle" data-i18n-aria-label="menuOpen" aria-label="Open menu">
|
||||
<span class="hamburger-icon"></span>
|
||||
</button>
|
||||
<nav class="main-nav" id="main-nav" aria-label="Main sections">
|
||||
<a href="#css" class="nav-link" data-section="css">CSS</a>
|
||||
<a href="#html" class="nav-link" data-section="html">HTML</a>
|
||||
<a href="#tailwind" class="nav-link" data-section="tailwind">Tailwind</a>
|
||||
</nav>
|
||||
<span class="header-level-pill" id="header-level-pill"></span>
|
||||
</div>
|
||||
<a href="#" id="logo-link" class="logo">
|
||||
@@ -30,6 +35,55 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Landing Page (Home) -->
|
||||
<div class="landing-page hidden" id="landing-page">
|
||||
<div class="landing-content">
|
||||
<section class="hero">
|
||||
<img src="./bowl.png" width="120" alt="" class="hero-logo" />
|
||||
<h1>Learn Web Development<br /><span class="hero-highlight">Interactively</span></h1>
|
||||
<p class="hero-subtitle">Master HTML, CSS, and Tailwind through hands-on exercises. Free and open source.</p>
|
||||
</section>
|
||||
|
||||
<section class="section-cards" id="section-cards">
|
||||
<a href="#css" class="section-card" data-section="css">
|
||||
<div class="section-card-icon" style="background: #264de4">CSS</div>
|
||||
<h2>CSS</h2>
|
||||
<p>Styling, layout, and animations</p>
|
||||
<span class="section-card-progress" id="css-progress"></span>
|
||||
</a>
|
||||
<a href="#html" class="section-card" data-section="html">
|
||||
<div class="section-card-icon" style="background: #e34c26">HTML</div>
|
||||
<h2>HTML</h2>
|
||||
<p>Semantic markup and native elements</p>
|
||||
<span class="section-card-progress" id="html-progress"></span>
|
||||
</a>
|
||||
<a href="#tailwind" class="section-card" data-section="tailwind">
|
||||
<div class="section-card-icon" style="background: #06b6d4">TW</div>
|
||||
<h2>Tailwind CSS</h2>
|
||||
<p>Utility-first CSS framework</p>
|
||||
<span class="section-card-progress" id="tailwind-progress"></span>
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Landing Page -->
|
||||
<div class="section-page hidden" id="section-page">
|
||||
<header class="section-hero">
|
||||
<h1 id="section-title">CSS</h1>
|
||||
<p id="section-description">Styling, layout, and animations</p>
|
||||
<div class="section-progress-bar">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="section-progress-fill"></div>
|
||||
</div>
|
||||
<span class="progress-text" id="section-progress-text">0 of 0 lessons complete</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="module-grid" id="module-grid">
|
||||
<!-- Module cards populated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="game-layout" id="main-content">
|
||||
<!-- Left Panel: Instructions + Editor -->
|
||||
<div class="left-panel">
|
||||
@@ -37,7 +91,17 @@
|
||||
<div class="lesson-title-row">
|
||||
<h2 id="lesson-title"></h2>
|
||||
<button id="share-btn" class="share-btn" data-i18n-title="shareTitle" title="Share lesson" aria-label="Share lesson">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
|
||||
221
src/main.css
221
src/main.css
@@ -384,7 +384,9 @@ kbd {
|
||||
cursor: pointer;
|
||||
color: var(--light-text);
|
||||
border-radius: var(--border-radius-sm);
|
||||
transition: color 0.2s, background 0.2s;
|
||||
transition:
|
||||
color 0.2s,
|
||||
background 0.2s;
|
||||
}
|
||||
|
||||
.share-btn:hover {
|
||||
@@ -1481,6 +1483,223 @@ input:checked + .toggle-slider::before {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ================= MAIN NAV ================= */
|
||||
.main-nav {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--light-text);
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: var(--primary-bg-light);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: var(--primary-bg-medium);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.main-nav {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= LANDING PAGE ================= */
|
||||
.landing-page {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.landing-content {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-color);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero-highlight {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: var(--light-text);
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Section Cards */
|
||||
.section-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--panel-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.section-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-card-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 800;
|
||||
font-size: 1.2rem;
|
||||
color: white;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.section-card h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.section-card p {
|
||||
font-size: 0.9rem;
|
||||
color: var(--light-text);
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.section-card-progress {
|
||||
font-size: 0.8rem;
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ================= SECTION PAGE ================= */
|
||||
.section-page {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-color);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.section-hero {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section-hero h1 {
|
||||
font-size: 2rem;
|
||||
color: var(--primary-dark);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.section-hero p {
|
||||
color: var(--light-text);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.section-progress-bar {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section-progress-bar .progress-bar {
|
||||
height: 8px;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* Module Grid */
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.module-card {
|
||||
display: block;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--panel-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
box-shadow: var(--shadow);
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.module-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.module-card h3 {
|
||||
font-size: 1rem;
|
||||
color: var(--primary-dark);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.module-card-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8rem;
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.module-card-progress {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
|
||||
Reference in New Issue
Block a user