feat: implement milestone-based progress system and activate new lessons

Progress System:
- Replace percentage-based progress with milestone markers (1, 5, 10, 20, 30, 50, 75, 100)
- Add visual milestone indicators with reached/current/next states
- Add celebration animation when milestones are reached
- Update progress bar to show progress toward next milestone
- Add progressTextMilestone i18n key for all 6 languages

New Lessons Activated:
- HTML Dialog (native modal dialogs)
- HTML Progress & Meter (indicator elements)
- HTML Fieldset (form grouping)
- HTML Datalist (autocomplete inputs)

This adds 10 new lessons across all 6 languages, bringing total from ~66 to ~76.
This commit is contained in:
2026-01-16 13:56:29 +01:00
parent b051974957
commit d2fbe0e085
7 changed files with 209 additions and 13 deletions

View File

@@ -165,6 +165,7 @@ const elements = {
sectionFooterLessonLinks: document.getElementById("section-footer-lesson-links"),
progressFill: document.getElementById("progress-fill"),
progressText: document.getElementById("progress-text"),
milestonesContainer: document.getElementById("milestones"),
resetBtn: document.getElementById("reset-btn"),
disableFeedbackToggle: document.getElementById("disable-feedback-toggle"),
@@ -310,14 +311,48 @@ function showSuccessHint(message) {
// ================= PROGRESS DISPLAY =================
// Track last milestone to detect new achievements
let lastMilestoneReached = 0;
function updateProgressDisplay() {
const stats = lessonEngine.getProgressStats();
elements.progressFill.style.width = `${stats.percentComplete}%`;
elements.progressText.textContent = t("progressText", {
percent: stats.percentComplete,
// Update progress bar (now shows progress to next milestone)
elements.progressFill.style.width = `${stats.progressToNext}%`;
// Update progress text
elements.progressText.textContent = t("progressTextMilestone", {
completed: stats.totalCompleted,
total: stats.totalLessons
next: stats.nextMilestone
});
// Update milestone indicators
if (elements.milestonesContainer) {
const milestoneEls = elements.milestonesContainer.querySelectorAll(".milestone");
milestoneEls.forEach((el) => {
const value = parseInt(el.dataset.value, 10);
el.classList.remove("reached", "current", "next", "just-reached");
if (stats.milestonesReached.includes(value)) {
el.classList.add("reached");
// Check if this milestone was just reached
if (value > lastMilestoneReached && value === stats.currentMilestone) {
el.classList.add("just-reached");
}
} else if (value === stats.nextMilestone) {
el.classList.add("next");
}
if (value === stats.currentMilestone) {
el.classList.add("current");
}
});
}
// Update last milestone for celebration detection
if (stats.currentMilestone > lastMilestoneReached) {
lastMilestoneReached = stats.currentMilestone;
}
}
// ================= USER SETTINGS =================

View File

@@ -261,12 +261,18 @@ function setupAuthForms() {
});
// OAuth buttons
document.getElementById("google-login")?.addEventListener("click", () => {
authModule?.signInWithGoogle();
document.getElementById("google-login")?.addEventListener("click", async () => {
const { error } = await authModule?.signInWithGoogle() ?? { error: null };
if (error) {
showOAuthError(error.message);
}
});
document.getElementById("github-login")?.addEventListener("click", () => {
authModule?.signInWithGitHub();
document.getElementById("github-login")?.addEventListener("click", async () => {
const { error } = await authModule?.signInWithGitHub() ?? { error: null };
if (error) {
showOAuthError(error.message);
}
});
// Close dialog on backdrop click
@@ -364,6 +370,22 @@ async function handleResetSubmit(e) {
}
}
function showOAuthError(message) {
// Show error in the currently visible form's error element
const loginError = document.getElementById("login-error");
const signupError = document.getElementById("signup-error");
// Use whichever form is visible
const errorEl = !document.getElementById("login-form")?.classList.contains("hidden")
? loginError
: signupError;
if (errorEl) {
errorEl.textContent = message;
errorEl.classList.remove("hidden");
}
}
function switchForm(formName) {
const loginForm = document.getElementById("login-form");
const signupForm = document.getElementById("signup-form");

View File

@@ -16,6 +16,10 @@ import htmlElementsEN from "../../lessons/20-html-elements.json";
import htmlFormsBasicEN from "../../lessons/21-html-forms-basic.json";
import htmlFormsValidationEN from "../../lessons/22-html-forms-validation.json";
import htmlDetailsSummaryEN from "../../lessons/23-html-details-summary.json";
import htmlProgressMeterEN from "../../lessons/24-html-progress-meter.json";
import htmlDatalistEN from "../../lessons/25-html-datalist.json";
import htmlDialogEN from "../../lessons/27-html-dialog.json";
import htmlFieldsetEN from "../../lessons/28-html-forms-fieldset.json";
import htmlFigureEN from "../../lessons/29-html-figure.json";
import htmlTablesEN from "../../lessons/30-html-tables.json";
import htmlSvgEN from "../../lessons/32-html-svg.json";
@@ -35,6 +39,10 @@ import htmlElementsDE from "../../lessons/de/20-html-elements.json";
import htmlFormsBasicDE from "../../lessons/de/21-html-forms-basic.json";
import htmlFormsValidationDE from "../../lessons/de/22-html-forms-validation.json";
import htmlDetailsSummaryDE from "../../lessons/de/23-html-details-summary.json";
import htmlProgressMeterDE from "../../lessons/de/24-html-progress-meter.json";
import htmlDatalistDE from "../../lessons/de/25-html-datalist.json";
import htmlDialogDE from "../../lessons/de/27-html-dialog.json";
import htmlFieldsetDE from "../../lessons/de/28-html-forms-fieldset.json";
import htmlTablesDE from "../../lessons/de/30-html-tables.json";
import htmlSvgDE from "../../lessons/de/32-html-svg.json";
import flexboxDE from "../../lessons/de/flexbox.json";
@@ -50,6 +58,10 @@ import htmlElementsPL from "../../lessons/pl/20-html-elements.json";
import htmlFormsBasicPL from "../../lessons/pl/21-html-forms-basic.json";
import htmlFormsValidationPL from "../../lessons/pl/22-html-forms-validation.json";
import htmlDetailsSummaryPL from "../../lessons/pl/23-html-details-summary.json";
import htmlProgressMeterPL from "../../lessons/pl/24-html-progress-meter.json";
import htmlDatalistPL from "../../lessons/pl/25-html-datalist.json";
import htmlDialogPL from "../../lessons/pl/27-html-dialog.json";
import htmlFieldsetPL from "../../lessons/pl/28-html-forms-fieldset.json";
import htmlTablesPL from "../../lessons/pl/30-html-tables.json";
import htmlSvgPL from "../../lessons/pl/32-html-svg.json";
import flexboxPL from "../../lessons/pl/flexbox.json";
@@ -65,6 +77,10 @@ import htmlElementsES from "../../lessons/es/20-html-elements.json";
import htmlFormsBasicES from "../../lessons/es/21-html-forms-basic.json";
import htmlFormsValidationES from "../../lessons/es/22-html-forms-validation.json";
import htmlDetailsSummaryES from "../../lessons/es/23-html-details-summary.json";
import htmlProgressMeterES from "../../lessons/es/24-html-progress-meter.json";
import htmlDatalistES from "../../lessons/es/25-html-datalist.json";
import htmlDialogES from "../../lessons/es/27-html-dialog.json";
import htmlFieldsetES from "../../lessons/es/28-html-forms-fieldset.json";
import htmlTablesES from "../../lessons/es/30-html-tables.json";
import htmlSvgES from "../../lessons/es/32-html-svg.json";
import flexboxES from "../../lessons/es/flexbox.json";
@@ -80,6 +96,10 @@ import htmlElementsAR from "../../lessons/ar/20-html-elements.json";
import htmlFormsBasicAR from "../../lessons/ar/21-html-forms-basic.json";
import htmlFormsValidationAR from "../../lessons/ar/22-html-forms-validation.json";
import htmlDetailsSummaryAR from "../../lessons/ar/23-html-details-summary.json";
import htmlProgressMeterAR from "../../lessons/ar/24-html-progress-meter.json";
import htmlDatalistAR from "../../lessons/ar/25-html-datalist.json";
import htmlDialogAR from "../../lessons/ar/27-html-dialog.json";
import htmlFieldsetAR from "../../lessons/ar/28-html-forms-fieldset.json";
import htmlTablesAR from "../../lessons/ar/30-html-tables.json";
import htmlSvgAR from "../../lessons/ar/32-html-svg.json";
import flexboxAR from "../../lessons/ar/flexbox.json";
@@ -95,6 +115,10 @@ import htmlElementsUK from "../../lessons/uk/20-html-elements.json";
import htmlFormsBasicUK from "../../lessons/uk/21-html-forms-basic.json";
import htmlFormsValidationUK from "../../lessons/uk/22-html-forms-validation.json";
import htmlDetailsSummaryUK from "../../lessons/uk/23-html-details-summary.json";
import htmlProgressMeterUK from "../../lessons/uk/24-html-progress-meter.json";
import htmlDatalistUK from "../../lessons/uk/25-html-datalist.json";
import htmlDialogUK from "../../lessons/uk/27-html-dialog.json";
import htmlFieldsetUK from "../../lessons/uk/28-html-forms-fieldset.json";
import htmlTablesUK from "../../lessons/uk/30-html-tables.json";
import htmlSvgUK from "../../lessons/uk/32-html-svg.json";
import flexboxUK from "../../lessons/uk/flexbox.json";
@@ -121,8 +145,12 @@ const moduleStoreEN = [
htmlSvgEN,
// HTML Interactive
htmlDetailsSummaryEN,
htmlDialogEN,
htmlProgressMeterEN,
htmlFormsBasicEN,
htmlFormsValidationEN,
htmlFieldsetEN,
htmlDatalistEN,
htmlTablesEN,
// Outro
goodbyeEN,
@@ -151,8 +179,12 @@ const moduleStoreDE = [
htmlSvgDE,
// HTML Interactive
htmlDetailsSummaryDE,
htmlDialogDE,
htmlProgressMeterDE,
htmlFormsBasicDE,
htmlFormsValidationDE,
htmlFieldsetDE,
htmlDatalistDE,
htmlTablesDE,
// Outro
goodbyeEN,
@@ -181,8 +213,12 @@ const moduleStorePL = [
htmlSvgPL,
// HTML Interactive
htmlDetailsSummaryPL,
htmlDialogPL,
htmlProgressMeterPL,
htmlFormsBasicPL,
htmlFormsValidationPL,
htmlFieldsetPL,
htmlDatalistPL,
htmlTablesPL,
// Outro
goodbyeEN,
@@ -211,8 +247,12 @@ const moduleStoreES = [
htmlSvgES,
// HTML Interactive
htmlDetailsSummaryES,
htmlDialogES,
htmlProgressMeterES,
htmlFormsBasicES,
htmlFormsValidationES,
htmlFieldsetES,
htmlDatalistES,
htmlTablesES,
// Outro
goodbyeEN,
@@ -241,8 +281,12 @@ const moduleStoreAR = [
htmlSvgAR,
// HTML Interactive
htmlDetailsSummaryAR,
htmlDialogAR,
htmlProgressMeterAR,
htmlFormsBasicAR,
htmlFormsValidationAR,
htmlFieldsetAR,
htmlDatalistAR,
htmlTablesAR,
// Outro
goodbyeEN,
@@ -271,8 +315,12 @@ const moduleStoreUK = [
htmlSvgUK,
// HTML Interactive
htmlDetailsSummaryUK,
htmlDialogUK,
htmlProgressMeterUK,
htmlFormsBasicUK,
htmlFormsValidationUK,
htmlFieldsetUK,
htmlDatalistUK,
htmlTablesUK,
// Outro
goodbyeEN,

View File

@@ -39,6 +39,7 @@ const translations = {
language: "Language",
progress: "Progress",
progressText: "{percent}% Complete ({completed}/{total})",
progressTextMilestone: "{completed} of {next}",
lessons: "Lessons",
settings: "Settings",
showHints: "Show Hints",
@@ -260,6 +261,7 @@ const translations = {
language: "Sprache",
progress: "Fortschritt",
progressText: "{percent}% abgeschlossen ({completed}/{total})",
progressTextMilestone: "{completed} von {next}",
lessons: "Lektionen",
settings: "Einstellungen",
showHints: "Hinweise anzeigen",
@@ -481,6 +483,7 @@ const translations = {
language: "Język",
progress: "Postęp",
progressText: "{percent}% ukończone ({completed}/{total})",
progressTextMilestone: "{completed} z {next}",
lessons: "Lekcje",
settings: "Ustawienia",
showHints: "Pokaż podpowiedzi",
@@ -701,6 +704,7 @@ const translations = {
language: "Idioma",
progress: "Progreso",
progressText: "{percent}% completado ({completed}/{total})",
progressTextMilestone: "{completed} de {next}",
lessons: "Lecciones",
settings: "Configuración",
showHints: "Mostrar pistas",
@@ -923,6 +927,7 @@ const translations = {
language: "اللغة",
progress: "التقدم",
progressText: "{percent}% مكتمل ({completed}/{total})",
progressTextMilestone: "{completed} من {next}",
lessons: "الدروس",
settings: "الإعدادات",
showHints: "إظهار التلميحات",
@@ -1140,6 +1145,7 @@ const translations = {
language: "Мова",
progress: "Прогрес",
progressText: "{percent}% завершено ({completed}/{total})",
progressTextMilestone: "{completed} з {next}",
lessons: "Уроки",
settings: "Налаштування",
showHints: "Показувати підказки",

View File

@@ -472,10 +472,11 @@ export class LessonEngine {
}
/**
* Get overall progress statistics
* @returns {Object} Progress statistics
* Get overall progress statistics with milestone data
* @returns {Object} Progress statistics including milestone progress
*/
getProgressStats() {
const MILESTONES = [1, 5, 10, 20, 30, 50, 75, 100];
let totalLessons = 0;
let totalCompleted = 0;
@@ -490,10 +491,25 @@ export class LessonEngine {
}
});
// Calculate milestone progress
const milestonesReached = MILESTONES.filter((m) => totalCompleted >= m);
const currentMilestone = milestonesReached[milestonesReached.length - 1] || 0;
const nextMilestone = MILESTONES.find((m) => m > totalCompleted) || 100;
const progressToNext =
nextMilestone > currentMilestone
? Math.round(((totalCompleted - currentMilestone) / (nextMilestone - currentMilestone)) * 100)
: 100;
return {
totalLessons,
totalCompleted,
percentComplete: totalLessons > 0 ? Math.round((totalCompleted / totalLessons) * 100) : 0
percentComplete: totalLessons > 0 ? Math.round((totalCompleted / totalLessons) * 100) : 0,
// Milestone data
milestones: MILESTONES,
milestonesReached,
currentMilestone,
nextMilestone,
progressToNext
};
}

View File

@@ -468,11 +468,21 @@
<div class="sidebar-section">
<h4 data-i18n="progress">Progress</h4>
<div class="progress-display" id="progress-display">
<div class="progress-display milestone-progress" id="progress-display">
<div class="milestones" id="milestones">
<span class="milestone" data-value="1">1</span>
<span class="milestone" data-value="5">5</span>
<span class="milestone" data-value="10">10</span>
<span class="milestone" data-value="20">20</span>
<span class="milestone" data-value="30">30</span>
<span class="milestone" data-value="50">50</span>
<span class="milestone" data-value="75">75</span>
<span class="milestone" data-value="100">100</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<span class="progress-text" id="progress-text">0% Complete</span>
<span class="progress-text" id="progress-text">0 of 100</span>
</div>
</div>

View File

@@ -1019,6 +1019,65 @@ nav.sidebar-section {
color: var(--light-text);
}
/* Milestone Progress */
.milestone-progress {
gap: var(--spacing-sm);
}
.milestones {
display: flex;
justify-content: space-between;
padding: 0 2px;
}
.milestone {
display: flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
font-size: 0.7rem;
font-weight: 600;
color: var(--light-text);
background: var(--border-color);
border-radius: 50%;
transition: all 0.3s ease;
}
.milestone.reached {
background: linear-gradient(135deg, #9163b8, #7c4dff);
color: white;
}
.milestone.current {
background: linear-gradient(135deg, #d45aa0, #1aafb8);
color: white;
transform: scale(1.15);
box-shadow: 0 2px 8px rgba(212, 90, 160, 0.4);
}
.milestone.next {
border: 2px dashed var(--light-text);
background: transparent;
}
/* Milestone celebration animation */
@keyframes milestone-pop {
0% {
transform: scale(1);
}
50% {
transform: scale(1.4);
}
100% {
transform: scale(1.15);
}
}
.milestone.just-reached {
animation: milestone-pop 0.5s ease-out;
}
/* Module List in Sidebar */
.module-list {
/* No max-height - parent nav.sidebar-section handles overflow */