diff --git a/CLAUDE.md b/CLAUDE.md index 6252da6..038786a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ Code Crispies is an interactive CSS/Tailwind learning platform built with pure J ## Commands ```bash -npm start # Start dev server at http://localhost:1312 +npm start # Start dev server at http://localhost:1234 npm run build # Production build to dist/ npm run test # Run tests once npm run test.watch # Run tests in watch mode diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c207a79 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# Code Crispies - Interactive CSS Learning Platform + +.PHONY: help dev build test test-watch test-coverage format clean install deploy + +# Default port +PORT = 1234 + +help: + @echo "Code Crispies - Development Commands" + @echo "" + @echo "Development:" + @echo " make dev - Start dev server (port $(PORT))" + @echo " make build - Production build to dist/" + @echo "" + @echo "Testing:" + @echo " make test - Run tests once" + @echo " make test-watch - Run tests in watch mode" + @echo " make test-coverage - Run tests with coverage" + @echo "" + @echo "Other:" + @echo " make format - Format all source files" + @echo " make clean - Remove build artifacts" + @echo " make install - Install dependencies" + +# Development +dev: + npm start + +# Build +build: + npm run build + +# Testing +test: + npm run test + +test-watch: + npm run test.watch + +test-coverage: + npm run test.coverage + +# Formatting +format: + npm run format + npm run format.lessons + +# Clean +clean: + rm -rf dist/ node_modules/.vite/ + +# Install +install: + npm install diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..bb86bdc --- /dev/null +++ b/flake.nix @@ -0,0 +1,34 @@ +{ + description = "Code Crispies - Interactive CSS/HTML/Tailwind learning platform"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + nodejs_20 + nodePackages.npm + gnumake + ]; + + shellHook = '' + echo "Code Crispies Development Environment" + echo "" + echo "Commands:" + echo " make dev - Start dev server (port 1234)" + echo " make build - Production build" + echo " make test - Run tests" + echo " make format - Format code" + echo "" + ''; + }; + } + ); +} diff --git a/src/app.de.js b/src/app.de.js deleted file mode 100644 index c893645..0000000 --- a/src/app.de.js +++ /dev/null @@ -1,594 +0,0 @@ -import { LessonEngine } from "./impl/LessonEngine.js"; -import { CodeEditor } from "./impl/CodeEditor.js"; -import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js"; -import { loadModules } from "./config/lessons.de.js"; - -// Simplified state - LessonEngine now manages lesson state and progress -const state = { - userSettings: { - disableFeedbackErrors: false - }, - showExpected: false -}; - -// DOM elements - updated for new layout -const elements = { - // Header - menuBtn: document.getElementById("menu-btn"), - helpBtn: document.getElementById("help-btn"), - - // Left panel - lessonTitle: document.getElementById("lesson-title"), - 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"), - 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"), - prevBtn: document.getElementById("prev-btn"), - nextBtn: document.getElementById("next-btn"), - levelIndicator: document.getElementById("level-indicator"), - - // Sidebar - sidebarDrawer: document.getElementById("sidebar-drawer"), - sidebarBackdrop: document.getElementById("sidebar-backdrop"), - closeSidebar: document.getElementById("close-sidebar"), - moduleList: document.getElementById("module-list"), - 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") -}; - -// 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() { - // 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 = "Lösung ausblenden"; - elements.showExpectedBtn.classList.add("btn-primary"); - } else { - elements.expectedOverlay.classList.remove("visible"); - elements.showExpectedBtn.textContent = "Lösung zeigen"; - elements.showExpectedBtn.classList.remove("btn-primary"); - } -} - -// ================= 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 = `${stats.percentComplete}% abgeschlossen (${stats.totalCompleted}/${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("Fehler beim Laden der Einstellungen:", e); - } - } -} - -function saveUserSettings() { - localStorage.setItem("codeCrispies.settings", JSON.stringify(state.userSettings)); -} - -// ================= MODULE INITIALIZATION ================= - -async function initializeModules() { - try { - const modules = await loadModules(); - lessonEngine.setModules(modules); - - // Use the new renderModuleList function with both callbacks - renderModuleList(elements.moduleList, modules, selectModule, selectLesson); - - // Load saved progress and select appropriate module - 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); - } - - updateProgressDisplay(); - } catch (error) { - console.error("Module konnten nicht geladen werden:", error); - elements.lessonDescription.textContent = "Module konnten nicht geladen werden. Bitte Seite neu laden."; - } -} - -// ================= MODULE/LESSON SELECTION ================= - -function selectModule(moduleId) { - const success = lessonEngine.setModuleById(moduleId); - if (!success) return; - - // 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); - loadCurrentLesson(); - - // Close sidebar after selection on mobile - if (window.innerWidth <= 768) { - closeSidebar(); - } -} - -// ================= LESSON LOADING ================= - -function resetSuccessIndicators() { - 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"); -} - -function updateEditorForMode(mode) { - const editorLabel = document.querySelector(".editor-label"); - - const modeConfig = { - html: { - placeholder: "HTML hier eingeben... Probiere: nav>ul>li*3 dann Tab drücken", - label: "HTML-Editor", - cmMode: "html" - }, - tailwind: { - placeholder: "Tailwind-Klassen eingeben (z.B. bg-blue-500 text-white p-4)", - label: "Tailwind-Klassen", - cmMode: "css" - }, - css: { - placeholder: "CSS-Code hier eingeben...", - label: "CSS-Editor", - cmMode: "css" - } - }; - - 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"; - - // Update UI based on mode - updateEditorForMode(mode); - - // Reset any success indicators - resetSuccessIndicators(); - - // Clear hints - clearHint(); - - // Hide expected overlay - state.showExpected = false; - elements.expectedOverlay.classList.remove("visible"); - elements.showExpectedBtn.textContent = "Lösung zeigen"; - 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 - if (codeEditor) { - codeEditor.setValue(engineState.userCode); - } - - // Update Run button text based on completion status - if (engineState.isCompleted) { - elements.runBtn.innerHTML = 'Erneut anwenden'; - - // Add completion badge if not present - if (!document.querySelector(".completion-badge")) { - const badge = document.createElement("span"); - badge.className = "completion-badge"; - badge.textContent = "Erledigt"; - elements.lessonTitle.appendChild(badge); - } - } else { - elements.runBtn.innerHTML = 'Ausführen'; - - // Remove completion badge if exists - const badge = document.querySelector(".completion-badge"); - if (badge) badge.remove(); - } - - // Update level indicator - renderLevelIndicator(elements.levelIndicator, engineState.lessonIndex + 1, engineState.totalLessons); - - // 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(); -} - -// ================= 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 success = lessonEngine.nextLesson(); - if (success) { - loadCurrentLesson(); - } -} - -function prevLesson() { - const success = lessonEngine.previousLesson(); - if (success) { - loadCurrentLesson(); - } -} - -// ================= CODE EXECUTION ================= - -function resetCode() { - // Reset editor to initial code for current lesson - lessonEngine.reset(); - const engineState = lessonEngine.getCurrentState(); - if (codeEditor && engineState.lesson) { - codeEditor.setValue(engineState.lesson.initialCode || ""); - } - // Clear hints and success indicators - clearHint(); - resetSuccessIndicators(); -} - -function runCode() { - const userCode = codeEditor ? codeEditor.getValue() : ""; - - // 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); - - // Validate code using LessonEngine - const validationResult = lessonEngine.validateCode(); - - if (validationResult.isValid) { - // Show success hint - showSuccessHint(validationResult.message || "CRISPY! ٩(◕‿◕)۶ Dein Code funktioniert."); - - // Update Run button - elements.runBtn.innerHTML = 'Erneut anwenden'; - elements.runBtn.classList.add("success"); - - // Add completion badge - if (!document.querySelector(".completion-badge")) { - const badge = document.createElement("span"); - badge.className = "completion-badge"; - badge.textContent = "Erledigt"; - elements.lessonTitle.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 - elements.previewWrapper?.classList.add("matched"); - setTimeout(() => { - elements.previewWrapper?.classList.remove("matched"); - }, 2500); - - 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 || "Weiter versuchen!", step, total); - } - } -} - -// ================= DIALOGS ================= - -function showHelp() { - elements.helpDialog.showModal(); -} - -function closeHelpDialog() { - elements.helpDialog.close(); -} - -function showResetConfirmation() { - elements.resetDialog.showModal(); -} - -function closeResetDialog() { - elements.resetDialog.close(); -} - -function handleResetConfirm() { - lessonEngine.clearProgress(); - closeResetDialog(); - closeSidebar(); - - // Reload first module - const modules = lessonEngine.modules; - if (modules.length > 0) { - selectModule(modules[0].id); - } - - updateProgressDisplay(); -} - -// ================= 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: "Code hier eingeben...", - onChange: handleEditorChange - }); - - codeEditor.init(""); -} - -function init() { - loadUserSettings(); - - // Initialize CodeMirror editor - initCodeEditor(); - - // Load modules after editor is ready - initializeModules().catch(console.error); - - // Sidebar controls - elements.menuBtn.addEventListener("click", openSidebar); - elements.closeSidebar.addEventListener("click", closeSidebar); - elements.sidebarBackdrop.addEventListener("click", closeSidebar); - - // 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", resetCode); - - // 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); - - // 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(); - } - }); -} - -// Start the application -init(); diff --git a/src/app.js b/src/app.js index 776d58c..27fa5f9 100644 --- a/src/app.js +++ b/src/app.js @@ -2,6 +2,7 @@ import { LessonEngine } from "./impl/LessonEngine.js"; 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"; // Simplified state - LessonEngine now manages lesson state and progress const state = { @@ -15,6 +16,7 @@ const state = { const elements = { // Header menuBtn: document.getElementById("menu-btn"), + langBtn: document.getElementById("lang-btn"), helpBtn: document.getElementById("help-btn"), // Left panel @@ -100,15 +102,42 @@ function toggleExpectedResult() { if (state.showExpected) { elements.expectedOverlay.classList.add("visible"); - elements.showExpectedBtn.textContent = "Hide Expected"; + elements.showExpectedBtn.textContent = t("hideExpected"); elements.showExpectedBtn.classList.add("btn-primary"); } else { elements.expectedOverlay.classList.remove("visible"); - elements.showExpectedBtn.textContent = "Show Expected"; + elements.showExpectedBtn.textContent = t("showExpected"); elements.showExpectedBtn.classList.remove("btn-primary"); } } +// ================= LANGUAGE TOGGLE ================= + +async function toggleLanguage() { + const currentLang = getLanguage(); + const newLang = currentLang === "en" ? "de" : "en"; + setLanguage(newLang); + applyTranslations(); + + // Reload lessons in new language + const engineState = lessonEngine.getCurrentState(); + const currentModuleId = engineState.module?.id; + const currentLessonIndex = engineState.lessonIndex; + + const modules = await 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(); +} + // ================= HINT SYSTEM ================= function showHint(message, step, total, isSuccess = false) { @@ -139,7 +168,11 @@ function showSuccessHint(message) { function updateProgressDisplay() { const stats = lessonEngine.getProgressStats(); elements.progressFill.style.width = `${stats.percentComplete}%`; - elements.progressText.textContent = `${stats.percentComplete}% Complete (${stats.totalCompleted}/${stats.totalLessons})`; + elements.progressText.textContent = t("progressText", { + percent: stats.percentComplete, + completed: stats.totalCompleted, + total: stats.totalLessons + }); } // ================= USER SETTINGS ================= @@ -165,7 +198,7 @@ function saveUserSettings() { async function initializeModules() { try { - const modules = await loadModules(); + const modules = await loadModules(getLanguage()); lessonEngine.setModules(modules); // Use the new renderModuleList function with both callbacks @@ -184,7 +217,7 @@ async function initializeModules() { updateProgressDisplay(); } catch (error) { console.error("Failed to load modules:", error); - elements.lessonDescription.textContent = "Failed to load modules. Please refresh the page."; + elements.lessonDescription.textContent = t("failedToLoad"); } } @@ -248,7 +281,7 @@ function updateEditorForMode(mode) { cmMode: "html" }, tailwind: { - placeholder: "Enter Tailwind classes (e.g., bg-blue-500 text-white p-4)", + placeholder: t("tailwindPlaceholder"), label: "Tailwind Classes", cmMode: "css" }, @@ -296,7 +329,7 @@ function loadCurrentLesson() { // Hide expected overlay state.showExpected = false; elements.expectedOverlay.classList.remove("visible"); - elements.showExpectedBtn.textContent = "Show Expected"; + elements.showExpectedBtn.textContent = t("showExpected"); elements.showExpectedBtn.classList.remove("btn-primary"); // Update UI @@ -318,17 +351,17 @@ function loadCurrentLesson() { // Update Run button text based on completion status if (engineState.isCompleted) { - elements.runBtn.innerHTML = 'Re-run'; + 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 = "Completed"; + badge.textContent = t("completed"); elements.lessonTitle.appendChild(badge); } } else { - elements.runBtn.innerHTML = 'Run'; + elements.runBtn.querySelector("span").textContent = t("run"); // Remove completion badge if exists const badge = document.querySelector(".completion-badge"); @@ -448,17 +481,17 @@ function runCode() { if (validationResult.isValid) { // Show success hint - showSuccessHint(validationResult.message || "CRISPY! ٩(◕‿◕)۶ Your code works correctly."); + showSuccessHint(validationResult.message || t("successMessage")); // Update Run button - elements.runBtn.innerHTML = 'Re-run'; + 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 = "Completed"; + badge.textContent = t("completed"); elements.lessonTitle.appendChild(badge); } @@ -486,7 +519,7 @@ function runCode() { // Only show hints if enabled if (!state.userSettings.disableFeedbackErrors) { - showHint(validationResult.message || "Keep trying!", step, total); + showHint(validationResult.message || t("keepTrying"), step, total); } } } @@ -546,6 +579,9 @@ function initCodeEditor() { } function init() { + // Initialize i18n before anything else + initI18n(); + loadUserSettings(); // Initialize CodeMirror editor @@ -559,6 +595,9 @@ function init() { elements.closeSidebar.addEventListener("click", closeSidebar); elements.sidebarBackdrop.addEventListener("click", closeSidebar); + // Language toggle + elements.langBtn.addEventListener("click", toggleLanguage); + // Expected result toggle elements.showExpectedBtn.addEventListener("click", toggleExpectedResult); diff --git a/src/config/lessons.js b/src/config/lessons.js index 397bd96..1fc8b8c 100644 --- a/src/config/lessons.js +++ b/src/config/lessons.js @@ -1,63 +1,112 @@ /** * Lesson Config - Functions for loading lesson configurations + * Supports English and German lesson content */ -// Import lesson configs -import basicSelectorsConfig from "../../lessons/00-basic-selectors.json"; -import advancedSelectorsConfig from "../../lessons/01-advanced-selectors.json"; -import tailwindConfig from "../../lessons/10-tailwind-basics.json"; -// CSS lessons -import boxModelConfig from "../../lessons/01-box-model.json"; -import flexboxConfig from "../../lessons/flexbox.json"; -import responsiveConfig from "../../lessons/08-responsive.json"; -import unitsVariablesConfig from "../../lessons/05-units-variables.json"; -import transitionsAnimationsConfig from "../../lessons/06-transitions-animations.json"; -// HTML lessons -import htmlElementsConfig from "../../lessons/20-html-elements.json"; -import htmlFormsBasicConfig from "../../lessons/21-html-forms-basic.json"; -import htmlFormsValidationConfig from "../../lessons/22-html-forms-validation.json"; -import htmlDetailsSummaryConfig from "../../lessons/23-html-details-summary.json"; -import htmlProgressMeterConfig from "../../lessons/24-html-progress-meter.json"; -import htmlDatalistConfig from "../../lessons/25-html-datalist.json"; -import htmlDataAttributesConfig from "../../lessons/26-html-data-attributes.json"; -import htmlDialogConfig from "../../lessons/27-html-dialog.json"; -import htmlFormsFieldsetConfig from "../../lessons/28-html-forms-fieldset.json"; -import htmlFigureConfig from "../../lessons/29-html-figure.json"; -import htmlTablesConfig from "../../lessons/30-html-tables.json"; -import htmlMarqueeConfig from "../../lessons/31-html-marquee.json"; -import htmlSvgConfig from "../../lessons/32-html-svg.json"; +// English lesson imports +import basicSelectorsEN from "../../lessons/00-basic-selectors.json"; +import advancedSelectorsEN from "../../lessons/01-advanced-selectors.json"; +import boxModelEN from "../../lessons/01-box-model.json"; +import unitsVariablesEN from "../../lessons/05-units-variables.json"; +import transitionsAnimationsEN from "../../lessons/06-transitions-animations.json"; +import responsiveEN from "../../lessons/08-responsive.json"; +import tailwindEN from "../../lessons/10-tailwind-basics.json"; +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 htmlDataAttributesEN from "../../lessons/26-html-data-attributes.json"; +import htmlDialogEN from "../../lessons/27-html-dialog.json"; +import htmlFormsFieldsetEN from "../../lessons/28-html-forms-fieldset.json"; +import htmlFigureEN from "../../lessons/29-html-figure.json"; +import htmlTablesEN from "../../lessons/30-html-tables.json"; +import htmlMarqueeEN from "../../lessons/31-html-marquee.json"; +import htmlSvgEN from "../../lessons/32-html-svg.json"; +import flexboxEN from "../../lessons/flexbox.json"; -// Module store -const moduleStore = [ - htmlElementsConfig, - htmlFormsBasicConfig, - htmlFormsValidationConfig, - htmlDetailsSummaryConfig, - htmlProgressMeterConfig, - htmlDatalistConfig, - htmlDataAttributesConfig, - htmlDialogConfig, - htmlFormsFieldsetConfig, - htmlFigureConfig, - htmlTablesConfig, - htmlMarqueeConfig, - htmlSvgConfig, - boxModelConfig, - flexboxConfig, - responsiveConfig, - unitsVariablesConfig, - transitionsAnimationsConfig, - basicSelectorsConfig, - advancedSelectorsConfig, - tailwindConfig +// German lesson imports +import basicSelectorsDE from "../../lessons/de/00-basic-selectors.json"; +import advancedSelectorsDE from "../../lessons/de/01-advanced-selectors.json"; +import boxModelDE from "../../lessons/de/01-box-model.json"; +import unitsVariablesDE from "../../lessons/de/05-units-variables.json"; +import transitionsAnimationsDE from "../../lessons/de/06-transitions-animations.json"; +import responsiveDE from "../../lessons/de/08-responsive.json"; +import tailwindDE from "../../lessons/de/10-tailwind-basics.json"; +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 htmlDataAttributesDE from "../../lessons/de/26-html-data-attributes.json"; +import htmlDialogDE from "../../lessons/de/27-html-dialog.json"; +import htmlFormsFieldsetDE from "../../lessons/de/28-html-forms-fieldset.json"; +import htmlFigureDE from "../../lessons/de/29-html-figure.json"; +import htmlTablesDE from "../../lessons/de/30-html-tables.json"; +import htmlMarqueeDE from "../../lessons/de/31-html-marquee.json"; +import htmlSvgDE from "../../lessons/de/32-html-svg.json"; +import flexboxDE from "../../lessons/de/flexbox.json"; + +// English module store +const moduleStoreEN = [ + htmlElementsEN, + htmlFormsBasicEN, + htmlFormsValidationEN, + htmlDetailsSummaryEN, + htmlProgressMeterEN, + htmlDatalistEN, + htmlDataAttributesEN, + htmlDialogEN, + htmlFormsFieldsetEN, + htmlFigureEN, + htmlTablesEN, + htmlMarqueeEN, + htmlSvgEN, + boxModelEN, + flexboxEN, + responsiveEN, + unitsVariablesEN, + transitionsAnimationsEN, + basicSelectorsEN, + advancedSelectorsEN, + tailwindEN +]; + +// German module store +const moduleStoreDE = [ + htmlElementsDE, + htmlFormsBasicDE, + htmlFormsValidationDE, + htmlDetailsSummaryDE, + htmlProgressMeterDE, + htmlDatalistDE, + htmlDataAttributesDE, + htmlDialogDE, + htmlFormsFieldsetDE, + htmlFigureDE, + htmlTablesDE, + htmlMarqueeDE, + htmlSvgDE, + boxModelDE, + flexboxDE, + responsiveDE, + unitsVariablesDE, + transitionsAnimationsDE, + basicSelectorsDE, + advancedSelectorsDE, + tailwindDE ]; /** - * Load all available modules + * Load all available modules for a given language + * @param {string} language - Language code ('en' or 'de') * @returns {Promise} Promise resolving to array of modules */ -export async function loadModules() { - return moduleStore.map((module) => ({ +export async function loadModules(language = "en") { + const store = language === "de" ? moduleStoreDE : moduleStoreEN; + return store.map((module) => ({ ...module, lessons: module.lessons.map((lesson) => ({ ...lesson, @@ -69,10 +118,12 @@ export async function loadModules() { /** * Get a module by its ID * @param {string} moduleId - The module ID to find + * @param {string} language - Language code ('en' or 'de') * @returns {Object|null} The module object or null if not found */ -export function getModuleById(moduleId) { - return moduleStore.find((module) => module.id === moduleId) || null; +export function getModuleById(moduleId, language = "en") { + const store = language === "de" ? moduleStoreDE : moduleStoreEN; + return store.find((module) => module.id === moduleId) || null; } /** @@ -118,20 +169,23 @@ function validateModuleConfig(config) { /** * Add a custom module to the store * @param {Object} moduleConfig - The module configuration to add + * @param {string} language - Language code ('en' or 'de') * @returns {boolean} Success status */ -export function addCustomModule(moduleConfig) { +export function addCustomModule(moduleConfig, language = "en") { try { validateModuleConfig(moduleConfig); + const store = language === "de" ? moduleStoreDE : moduleStoreEN; + // Check if module with same ID already exists - const existingIndex = moduleStore.findIndex((m) => m.id === moduleConfig.id); + const existingIndex = store.findIndex((m) => m.id === moduleConfig.id); if (existingIndex >= 0) { // Replace existing module - moduleStore[existingIndex] = moduleConfig; + store[existingIndex] = moduleConfig; } else { // Add new module - moduleStore.push(moduleConfig); + store.push(moduleConfig); } return true; diff --git a/src/helpers/renderer.js b/src/helpers/renderer.js index edfbb33..abcc779 100644 --- a/src/helpers/renderer.js +++ b/src/helpers/renderer.js @@ -1,6 +1,7 @@ /** * Renderer - Handles UI updates for the CSS learning platform */ +import { t } from "../i18n.js"; // Feedback elements cache let feedbackElement = null; @@ -77,7 +78,7 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes lessonItem.classList.add("lesson-list-item"); lessonItem.dataset.moduleId = module.id; lessonItem.dataset.lessonIndex = index; - lessonItem.textContent = lesson.title || `Lesson ${index + 1}`; + lessonItem.textContent = lesson.title || t("lessonFallback", { index: index + 1 }); // Mark lesson as completed if in progress data if (progress[module.id] && progress[module.id].completed.includes(index)) { @@ -137,7 +138,7 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes */ export function renderLesson(titleEl, descriptionEl, taskEl, previewEl, prefixEl, inputEl, suffixEl, lesson) { // Set lesson title and description - titleEl.textContent = lesson.title || "Untitled Lesson"; + titleEl.textContent = lesson.title || t("untitledLesson"); descriptionEl.innerHTML = lesson.description || ""; // Set task instructions @@ -162,7 +163,7 @@ export function renderLesson(titleEl, descriptionEl, taskEl, previewEl, prefixEl * @param {number} total - The total number of levels */ export function renderLevelIndicator(element, current, total) { - element.textContent = `Lesson ${current} of ${total}`; + element.textContent = t("levelIndicator", { current, total }); } /** diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..fad4cae --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,287 @@ +/** + * Internationalization module for Code Crispies + */ + +const translations = { + en: { + // Page + pageTitle: "CODE CRISPIES - Learn CSS Interactively", + skipLink: "Skip to main content", + + // Header + menuOpen: "Open menu", + langSwitch: "DE", + langSwitchLabel: "Sprache wechseln: Deutsch", + help: "Help", + + // Instructions + loading: "Loading...", + selectLesson: "Please select a lesson to begin.", + editorLabel: "CSS Editor", + undoTitle: "Undo (Ctrl+Z)", + redoTitle: "Redo (Ctrl+Shift+Z)", + resetCodeTitle: "Reset to initial code", + run: "Run", + rerun: "Re-run", + + // Preview + yourOutput: "Your Output", + showExpected: "Show Expected", + hideExpected: "Hide Expected", + previous: "Previous", + next: "Next", + levelIndicator: "Lesson {current} of {total}", + + // Sidebar + menu: "Menu", + closeMenu: "Close menu", + progress: "Progress", + progressText: "{percent}% Complete ({completed}/{total})", + lessons: "Lessons", + settings: "Settings", + showHints: "Show Hints", + resetAllProgress: "Reset All Progress", + openSource: "Open Source:", + by: "by", + + // Help dialog + helpTitle: "Help", + aboutTitle: "About Code Crispies", + aboutText: "Code Crispies is a free, open-source platform for learning web development through hands-on exercises. No account required - just start coding!", + learningModesTitle: "Learning Modes", + modeCss: "CSS - Write CSS rules to style elements", + modeTailwind: "Tailwind - Apply utility classes directly in HTML", + modeHtml: "HTML - Practice semantic markup and native elements", + gettingStartedTitle: "Getting Started", + gettingStartedText: "Open the menu (☰) to browse lesson modules. Each module covers a specific topic with progressive exercises.", + completingLessonsTitle: "Completing Lessons", + completingStep1: "Read the task instructions on the left", + completingStep2: "Write your code in the editor", + completingStep3: "Watch the live preview update as you type", + completingStep4: "Follow hints to fix any issues", + completingStep5: "Click Next when complete", + editorToolsTitle: "Editor Tools", + editorToolUndo: "↶ Undo / ↷ Redo - Navigate edit history", + editorToolReset: "⟲ Reset - Restore initial code for current lesson", + editorToolExpected: "Show Expected - Toggle the target result overlay", + keyboardShortcutsTitle: "Keyboard Shortcuts", + shortcutRun: "Ctrl+Enter - Validate immediately", + shortcutUndo: "Ctrl+Z - Undo", + shortcutRedo: "Ctrl+Shift+Z - Redo", + emmetTitle: "Emmet Shortcuts (HTML mode)", + emmetText: "Type abbreviations and press Tab to expand:", + emmetClass: "div.box → div with class", + emmetChildren: "ul>li*3 → ul with 3 li children", + emmetNested: "form>input+button → nested structure", + emmetContent: "p{Hello} → p with text content", + + // Reset dialog + resetDialogTitle: "Reset Progress", + resetDialogText: "Are you sure you want to reset all your progress? This cannot be undone.", + cancel: "Cancel", + resetAll: "Reset All", + + // Dynamic content + completed: "Completed", + successMessage: "CRISPY! ٩(◕‿◕)۶ Your code works correctly.", + keepTrying: "Keep trying!", + failedToLoad: "Failed to load modules. Please refresh the page.", + tailwindPlaceholder: "Enter Tailwind classes (e.g., bg-blue-500 text-white p-4)", + lessonFallback: "Lesson {index}", + untitledLesson: "Untitled Lesson" + }, + + de: { + // Page + pageTitle: "CODE CRISPIES - CSS interaktiv lernen", + skipLink: "Zum Hauptinhalt springen", + + // Header + menuOpen: "Menü öffnen", + langSwitch: "EN", + langSwitchLabel: "Switch language: English", + help: "Hilfe", + + // Instructions + loading: "Laden...", + selectLesson: "Bitte wähle eine Lektion aus, um zu beginnen.", + editorLabel: "CSS-Editor", + undoTitle: "Rückgängig (Strg+Z)", + redoTitle: "Wiederholen (Strg+Umschalt+Z)", + resetCodeTitle: "Auf Anfangscode zurücksetzen", + run: "Ausführen", + rerun: "Erneut anwenden", + + // Preview + yourOutput: "Deine Ausgabe", + showExpected: "Lösung zeigen", + hideExpected: "Lösung ausblenden", + previous: "Zurück", + next: "Weiter", + levelIndicator: "Lektion {current} von {total}", + + // Sidebar + menu: "Menü", + closeMenu: "Menü schließen", + progress: "Fortschritt", + progressText: "{percent}% abgeschlossen ({completed}/{total})", + lessons: "Lektionen", + settings: "Einstellungen", + showHints: "Hinweise anzeigen", + resetAllProgress: "Fortschritt zurücksetzen", + openSource: "Open Source:", + by: "von", + + // Help dialog + helpTitle: "Hilfe", + aboutTitle: "Über Code Crispies", + aboutText: "Code Crispies ist eine kostenlose Open-Source-Plattform zum Erlernen von Webentwicklung durch praktische Übungen. Kein Konto erforderlich - einfach loslegen!", + learningModesTitle: "Lernmodi", + modeCss: "CSS - Schreibe CSS-Regeln zum Stylen von Elementen", + modeTailwind: "Tailwind - Wende Utility-Klassen direkt im HTML an", + modeHtml: "HTML - Übe semantisches Markup und native Elemente", + gettingStartedTitle: "Erste Schritte", + gettingStartedText: "Öffne das Menü (☰), um Lektionsmodule zu durchsuchen. Jedes Modul behandelt ein Thema mit aufeinander aufbauenden Übungen.", + completingLessonsTitle: "Lektionen abschließen", + completingStep1: "Lies die Aufgabenstellung auf der linken Seite", + completingStep2: "Schreibe deinen Code im Editor", + completingStep3: "Beobachte die Live-Vorschau während du tippst", + completingStep4: "Folge den Hinweisen, um Fehler zu beheben", + completingStep5: "Klicke auf Weiter, wenn du fertig bist", + editorToolsTitle: "Editor-Werkzeuge", + editorToolUndo: "↶ Rückgängig / ↷ Wiederholen - Bearbeitungsverlauf navigieren", + editorToolReset: "⟲ Zurücksetzen - Ursprünglichen Code wiederherstellen", + editorToolExpected: "Lösung zeigen - Zielergebnis ein-/ausblenden", + keyboardShortcutsTitle: "Tastenkürzel", + shortcutRun: "Strg+Enter - Sofort validieren", + shortcutUndo: "Strg+Z - Rückgängig", + shortcutRedo: "Strg+Umschalt+Z - Wiederholen", + emmetTitle: "Emmet-Kürzel (HTML-Modus)", + emmetText: "Tippe Abkürzungen und drücke Tab zum Erweitern:", + emmetClass: "div.box → div mit Klasse", + emmetChildren: "ul>li*3 → ul mit 3 li-Kindern", + emmetNested: "form>input+button → verschachtelte Struktur", + emmetContent: "p{Hallo} → p mit Textinhalt", + + // Reset dialog + resetDialogTitle: "Fortschritt zurücksetzen", + resetDialogText: "Bist du sicher, dass du deinen gesamten Fortschritt zurücksetzen möchtest? Dies kann nicht rückgängig gemacht werden.", + cancel: "Abbrechen", + resetAll: "Alles zurücksetzen", + + // Dynamic content + completed: "Erledigt", + successMessage: "CRISPY! ٩(◕‿◕)۶ Dein Code funktioniert.", + keepTrying: "Weiter versuchen!", + failedToLoad: "Module konnten nicht geladen werden. Bitte Seite neu laden.", + tailwindPlaceholder: "Tailwind-Klassen eingeben (z.B. bg-blue-500 text-white p-4)", + lessonFallback: "Lektion {index}", + untitledLesson: "Unbenannte Lektion" + } +}; + +let currentLang = "en"; + +/** + * Detect initial language from localStorage or browser + */ +export function detectLanguage() { + // Check localStorage first + const stored = localStorage.getItem("codeCrispies.language"); + if (stored && translations[stored]) { + return stored; + } + + // Check browser language + const browserLang = navigator.language.split("-")[0]; + if (translations[browserLang]) { + return browserLang; + } + + return "en"; +} + +/** + * Get current language + */ +export function getLanguage() { + return currentLang; +} + +/** + * Set language and persist to localStorage + */ +export function setLanguage(lang) { + if (!translations[lang]) { + console.warn(`Language "${lang}" not supported, falling back to English`); + lang = "en"; + } + currentLang = lang; + localStorage.setItem("codeCrispies.language", lang); + document.documentElement.lang = lang; +} + +/** + * Get a translation by key with optional interpolation + * @param {string} key - Translation key + * @param {Object} params - Optional parameters for interpolation + */ +export function t(key, params = {}) { + const text = translations[currentLang]?.[key] || translations.en[key] || key; + + if (Object.keys(params).length === 0) { + return text; + } + + // Replace {param} placeholders + return text.replace(/\{(\w+)\}/g, (match, param) => { + return params[param] !== undefined ? params[param] : match; + }); +} + +/** + * Apply translations to all elements with data-i18n attribute + */ +export function applyTranslations() { + // Update page title + document.title = t("pageTitle"); + + // Update elements with data-i18n attribute (text content) + document.querySelectorAll("[data-i18n]").forEach((el) => { + const key = el.dataset.i18n; + el.textContent = t(key); + }); + + // Update elements with data-i18n-html attribute (innerHTML) + document.querySelectorAll("[data-i18n-html]").forEach((el) => { + const key = el.dataset.i18nHtml; + el.innerHTML = t(key); + }); + + // Update elements with data-i18n-title attribute + document.querySelectorAll("[data-i18n-title]").forEach((el) => { + const key = el.dataset.i18nTitle; + el.title = t(key); + }); + + // Update elements with data-i18n-aria-label attribute + document.querySelectorAll("[data-i18n-aria-label]").forEach((el) => { + const key = el.dataset.i18nAriaLabel; + el.setAttribute("aria-label", t(key)); + }); + + // Update elements with data-i18n-placeholder attribute + document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => { + const key = el.dataset.i18nPlaceholder; + el.placeholder = t(key); + }); +} + +/** + * Initialize i18n system + */ +export function initI18n() { + const lang = detectLanguage(); + setLanguage(lang); + applyTranslations(); +} diff --git a/src/index.de.html b/src/index.de.html deleted file mode 100644 index 2304aa4..0000000 --- a/src/index.de.html +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - - CODE CRISPIES - CSS interaktiv lernen - - - - -
- -
- - -
- EN - -
-
- - -
- -
-
- Laden... -

Laden...

-
- Bitte wähle eine Lektion aus, um zu beginnen. -
-
- -
-
- -
-
-
- -
-
- - - -
- -
-
-
- - -
-
-
- -
-
-
- - -
-
-
- Deine Ausgabe - -
-
-
- -
-
-
- -
-
-
-
-
- -
Lektion 0/0
- -
-
-
- - - - - - - - - -
-

Hilfe

- -
-
-

Über Code Crispies

-

Code Crispies ist eine kostenlose Open-Source-Plattform zum Erlernen von Webentwicklung durch praktische Übungen. Kein Konto erforderlich - einfach loslegen!

- -

Lernmodi

-
    -
  • CSS - Schreibe CSS-Regeln zum Stylen von Elementen
  • -
  • Tailwind - Wende Utility-Klassen direkt im HTML an
  • -
  • HTML - Übe semantisches Markup und native Elemente
  • -
- -

Erste Schritte

-

Öffne das Menü (☰), um Lektionsmodule zu durchsuchen. Jedes Modul behandelt ein Thema mit aufeinander aufbauenden Übungen.

- -

Lektionen abschließen

-
    -
  1. Lies die Aufgabenstellung auf der linken Seite
  2. -
  3. Schreibe deinen Code im Editor
  4. -
  5. Klicke auf Ausführen oder drücke Strg+Enter
  6. -
  7. Folge den Hinweisen, um Fehler zu beheben
  8. -
  9. Klicke auf Weiter, wenn du fertig bist
  10. -
- -

Editor-Werkzeuge

-
    -
  • ↶ Rückgängig / ↷ Wiederholen - Bearbeitungsverlauf navigieren
  • -
  • ⟲ Zurücksetzen - Ursprünglichen Code wiederherstellen
  • -
  • Lösung zeigen - Zielergebnis ein-/ausblenden
  • -
- -

Tastenkürzel

-
    -
  • Strg+Enter - Code ausführen
  • -
  • Strg+Z - Rückgängig
  • -
  • Strg+Umschalt+Z - Wiederholen
  • -
- -

Emmet-Kürzel (HTML-Modus)

-

Tippe Abkürzungen und drücke Tab zum Erweitern:

-
    -
  • div.box → div mit Klasse
  • -
  • ul>li*3 → ul mit 3 li-Kindern
  • -
  • form>input+button → verschachtelte Struktur
  • -
  • p{Hallo} → p mit Textinhalt
  • -
-
-
- - - -
-

Fortschritt zurücksetzen

- -
-
-

Bist du sicher, dass du deinen gesamten Fortschritt zurücksetzen möchtest? Dies kann nicht rückgängig gemacht werden.

-
- - -
-
-
-
- - - - diff --git a/src/index.html b/src/index.html index 8f98d39..801e724 100644 --- a/src/index.html +++ b/src/index.html @@ -8,11 +8,10 @@ - +
-
-
- DE - + +
-
- Loading... -

Loading...

-
+ Loading... +

Loading...

+
Please select a lesson to begin.
-
- -
+
- +
- - - + + +
-
-
- -
+
@@ -72,24 +65,20 @@
- Your Output - + Your Output +
-
- -
+
-
- -
+
- +
Level 0/0
- +
@@ -98,14 +87,14 @@ -