feat: add lesson difficulty indicators and improve mobile sidebar

- Add computeLessonDifficulty function to determine lesson difficulty
  based on selector complexity (easy/medium/hard)
- Display difficulty badge with bar indicator in lesson title row
- Add mobile navigation links (CSS, HTML, Tailwind) to sidebar
- Add mobile auth trigger button in sidebar
- Redesign settings section with card layout and native toggles
- Add difficulty translations for all 6 languages
- Fix module pill overflow on narrow screens
This commit is contained in:
2026-01-16 21:47:47 +01:00
parent cd43482048
commit 2f85df98cb
7 changed files with 397 additions and 30 deletions

View File

@@ -3,6 +3,49 @@
*/
import { t } from "../i18n.js";
/**
* Compute lesson difficulty based on lesson structure
* - Easy: selector is provided in codePrefix (student only writes properties)
* - Medium: student writes a simple selector (single element/class)
* - Hard: student writes compound selectors (descendant, chained classes, type+class)
* @param {Object} lesson - The lesson object
* @returns {"easy"|"medium"|"hard"} The computed difficulty
*/
export function computeLessonDifficulty(lesson) {
const codePrefix = lesson.codePrefix || "";
const solution = lesson.solution || "";
// If codePrefix contains an opening brace, selector is provided → Easy
if (codePrefix.includes("{")) {
return "easy";
}
// No codePrefix with selector - check the solution complexity
// Hard: descendant selectors (space before {), chained classes (.a.b), type+class (a.class)
const selectorMatch = solution.match(/^([^{]+)\{/);
if (selectorMatch) {
const selector = selectorMatch[1].trim();
// Descendant selector: has space (e.g., ".nav a", ".card p")
if (/\S\s+\S/.test(selector)) {
return "hard";
}
// Chained classes: multiple dots without space (e.g., ".btn.primary")
if ((selector.match(/\./g) || []).length > 1) {
return "hard";
}
// Type + class: element followed by dot (e.g., "a.btn", "div.card")
if (/^[a-z]+\.[a-z]/i.test(selector)) {
return "hard";
}
}
// Simple selector → Medium
return "medium";
}
// Feedback elements cache
let feedbackElement = null;
let feedbackTimeout = null;
@@ -138,6 +181,42 @@ export function renderLesson(titleEl, descriptionEl, taskEl, previewEl, prefixEl
// The LessonEngine will handle this when it's first set
}
/**
* Render the difficulty badge (right-aligned in title row)
* @param {HTMLElement} container - The container element (lesson-title-row)
* @param {Object} lesson - The lesson object
*/
export function renderDifficultyBadge(container, lesson) {
// Remove existing difficulty wrapper if any
const existingWrapper = container.querySelector(".difficulty-wrapper");
if (existingWrapper) {
existingWrapper.remove();
}
// Compute difficulty
const difficulty = computeLessonDifficulty(lesson);
// Create wrapper for right-alignment
const wrapper = document.createElement("span");
wrapper.className = "difficulty-wrapper";
// Create badge element with three bars
const badge = document.createElement("span");
badge.className = `difficulty-badge difficulty-${difficulty}`;
badge.setAttribute("aria-label", t(`difficulty_${difficulty}_label`));
badge.setAttribute("title", t(`difficulty_${difficulty}`));
// Add three bars
for (let i = 0; i < 3; i++) {
const bar = document.createElement("span");
bar.className = "bar";
badge.appendChild(bar);
}
wrapper.appendChild(badge);
container.appendChild(wrapper);
}
/**
* Update the level indicator
* @param {HTMLElement} element - The level indicator element