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:
43
src/app.js
43
src/app.js
@@ -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 =================
|
||||
|
||||
30
src/auth.js
30
src/auth.js
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: "Показувати підказки",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
59
src/main.css
59
src/main.css
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user