init project
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.idea
|
||||||
|
.code
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
1450
package-lock.json
generated
Normal file
1450
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "code-crispies",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "An interactive platform for learning CSS through practical challenges",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"css",
|
||||||
|
"html",
|
||||||
|
"learning",
|
||||||
|
"interactive",
|
||||||
|
"education"
|
||||||
|
],
|
||||||
|
"author": "Michael Czechowski <mail@dailysh.it>",
|
||||||
|
"license": "Copyright Michael Czechowski 2025",
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.3.5",
|
||||||
|
"vitest": "^3.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/android-chrome-192x192.png
Normal file
BIN
public/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
public/android-chrome-512x512.png
Normal file
BIN
public/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
public/apple-touch-icon.png
Normal file
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
public/favicon-16x16.png
Normal file
BIN
public/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 555 B |
BIN
public/favicon-32x32.png
Normal file
BIN
public/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
public/site.webmanifest
Normal file
1
public/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
49
scaffold.sh
Executable file
49
scaffold.sh
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Define base directory
|
||||||
|
BASE_DIR="."
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
mkdir -p "$BASE_DIR/src/styles"
|
||||||
|
mkdir -p "$BASE_DIR/src/js"
|
||||||
|
mkdir -p "$BASE_DIR/src/lessons/configs"
|
||||||
|
|
||||||
|
# Create files with comments
|
||||||
|
touch "$BASE_DIR/src/index.html"
|
||||||
|
echo "<!-- Main entry HTML file -->" > "$BASE_DIR/src/index.html"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/src/styles/main.css"
|
||||||
|
echo "/* Global styles */" > "$BASE_DIR/src/styles/main.css"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/src/js/app.js"
|
||||||
|
echo "// Main application logic" > "$BASE_DIR/src/js/app.js"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/src/js/LessonEngine.js"
|
||||||
|
echo "// Core lesson processing engine" > "$BASE_DIR/src/js/LessonEngine.js"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/src/js/renderer.js"
|
||||||
|
echo "// Handles UI rendering" > "$BASE_DIR/src/js/renderer.js"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/src/js/validator.js"
|
||||||
|
echo "// Validates user code submissions" > "$BASE_DIR/src/js/validator.js"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/src/lessons/lesson-config.js"
|
||||||
|
echo "// Loads and parses lesson configs" > "$BASE_DIR/src/lessons/lesson-config.js"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/src/lessons/configs/flexbox.json"
|
||||||
|
echo "{ /* Flexbox lesson config */ }" > "$BASE_DIR/src/lessons/configs/flexbox.json"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/src/lessons/configs/grid.json"
|
||||||
|
echo "{ /* Grid lesson config */ }" > "$BASE_DIR/src/lessons/configs/grid.json"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/src/lessons/configs/basics.json"
|
||||||
|
echo "{ /* Basics lesson config */ }" > "$BASE_DIR/src/lessons/configs/basics.json"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/package.json"
|
||||||
|
echo "{\n \"name\": \"css-learning-platform\",\n \"version\": \"1.0.0\"\n}" > "$BASE_DIR/package.json"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/vite.config.js"
|
||||||
|
echo "// Vite config file" > "$BASE_DIR/vite.config.js"
|
||||||
|
|
||||||
|
echo "Project scaffolded at: $BASE_DIR"
|
||||||
|
|
||||||
89
src/index.html
Normal file
89
src/index.html
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Code Crispies - Learn CSS Interactively</title>
|
||||||
|
<link rel="stylesheet" href="./styles/main.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<header class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>Code Crispies</h1>
|
||||||
|
</div>
|
||||||
|
<nav class="main-nav">
|
||||||
|
<ul>
|
||||||
|
<li><button id="module-selector-btn" class="btn">Modules</button></li>
|
||||||
|
<li><button id="reset-btn" class="btn">Reset Progress</button></li>
|
||||||
|
<li><button id="help-btn" class="btn">Help</button></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="module-list">
|
||||||
|
<!-- Module list will be populated here -->
|
||||||
|
</div>
|
||||||
|
<div class="lesson-progress">
|
||||||
|
<!-- Lesson progress will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-area">
|
||||||
|
<div class="lesson-container">
|
||||||
|
<h2 id="lesson-title">Loading...</h2>
|
||||||
|
<div class="lesson-description" id="lesson-description">
|
||||||
|
Please select a lesson to begin.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="challenge-container">
|
||||||
|
<div class="preview-area" id="preview-area">
|
||||||
|
<!-- Preview of the challenge will be shown here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-container">
|
||||||
|
<div class="task-instruction" id="task-instruction">
|
||||||
|
<!-- Task instructions will be shown here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-editor">
|
||||||
|
<div class="editor-header">
|
||||||
|
<span>CSS Editor</span>
|
||||||
|
<button id="run-btn" class="btn btn-primary">Run</button>
|
||||||
|
</div>
|
||||||
|
<div class="editor-content">
|
||||||
|
<pre><code id="editor-prefix"></code></pre>
|
||||||
|
<textarea id="code-input" class="code-input" spellcheck="false"></textarea>
|
||||||
|
<pre><code id="editor-suffix"></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button id="prev-btn" class="btn">Previous</button>
|
||||||
|
<div class="level-indicator" id="level-indicator">Level 0/0</div>
|
||||||
|
<button id="next-btn" class="btn btn-primary">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div id="modal-container" class="modal-container hidden">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="modal-title">Modal Title</h3>
|
||||||
|
<button id="modal-close" class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-content" id="modal-content">
|
||||||
|
<!-- Modal content will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="./js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
230
src/js/LessonEngine.js
Normal file
230
src/js/LessonEngine.js
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
/**
|
||||||
|
* LessonEngine - Core class for managing lessons and applying/testing user code
|
||||||
|
* This file is the implementation of the LessonEngine class declaration from app.js
|
||||||
|
*/
|
||||||
|
import { validateUserCode } from './validator.js';
|
||||||
|
import { showFeedback } from './renderer.js';
|
||||||
|
|
||||||
|
export class LessonEngine {
|
||||||
|
constructor() {
|
||||||
|
this.currentLesson = null;
|
||||||
|
this.userCode = '';
|
||||||
|
this.currentModule = null;
|
||||||
|
this.currentLessonIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current module
|
||||||
|
* @param {Object} module - The module object from the config
|
||||||
|
*/
|
||||||
|
setModule(module) {
|
||||||
|
this.currentModule = module;
|
||||||
|
this.currentLessonIndex = 0;
|
||||||
|
if (module && module.lessons && module.lessons.length > 0) {
|
||||||
|
this.setLesson(module.lessons[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current lesson
|
||||||
|
* @param {Object} lesson - The lesson object from the config
|
||||||
|
*/
|
||||||
|
setLesson(lesson) {
|
||||||
|
this.currentLesson = lesson;
|
||||||
|
this.userCode = lesson.initialCode || '';
|
||||||
|
this.renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set lesson by index within the current module
|
||||||
|
* @param {number} index - The lesson index
|
||||||
|
* @returns {boolean} Whether the operation was successful
|
||||||
|
*/
|
||||||
|
setLessonByIndex(index) {
|
||||||
|
if (!this.currentModule || !this.currentModule.lessons) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= this.currentModule.lessons.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentLessonIndex = index;
|
||||||
|
this.setLesson(this.currentModule.lessons[index]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move to the next lesson
|
||||||
|
* @returns {boolean} Whether the operation was successful
|
||||||
|
*/
|
||||||
|
nextLesson() {
|
||||||
|
return this.setLessonByIndex(this.currentLessonIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move to the previous lesson
|
||||||
|
* @returns {boolean} Whether the operation was successful
|
||||||
|
*/
|
||||||
|
previousLesson() {
|
||||||
|
return this.setLessonByIndex(this.currentLessonIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply user-written CSS to the preview area
|
||||||
|
* @param {string} code - User CSS code
|
||||||
|
*/
|
||||||
|
applyUserCode(code) {
|
||||||
|
if (!this.currentLesson) return;
|
||||||
|
|
||||||
|
this.userCode = code;
|
||||||
|
this.renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the preview for the current lesson
|
||||||
|
*/
|
||||||
|
renderPreview() {
|
||||||
|
if (!this.currentLesson) return;
|
||||||
|
|
||||||
|
const { previewHTML, previewBaseCSS, previewContainer, sandboxCSS } = this.currentLesson;
|
||||||
|
|
||||||
|
// Create an iframe for isolated preview rendering
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.width = '100%';
|
||||||
|
iframe.style.height = '100%';
|
||||||
|
iframe.style.border = 'none';
|
||||||
|
iframe.title = 'Preview';
|
||||||
|
|
||||||
|
// Get the preview container
|
||||||
|
const container = document.getElementById(previewContainer || 'preview-area');
|
||||||
|
|
||||||
|
// Clear the container and add the iframe
|
||||||
|
container.innerHTML = '';
|
||||||
|
container.appendChild(iframe);
|
||||||
|
|
||||||
|
// Create the complete CSS by combining base CSS with user code and sandbox CSS
|
||||||
|
const combinedCSS = `
|
||||||
|
/* Base CSS */
|
||||||
|
${previewBaseCSS || ''}
|
||||||
|
|
||||||
|
/* User Code */
|
||||||
|
${this.userCode || ''}
|
||||||
|
|
||||||
|
/* Sandbox CSS (for visualizing the exercise) */
|
||||||
|
${sandboxCSS || ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Write the content to the iframe
|
||||||
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
iframeDoc.open();
|
||||||
|
iframeDoc.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>${combinedCSS}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${previewHTML || '<div>No preview available</div>'}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
iframeDoc.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate user code against the current lesson's requirements
|
||||||
|
* @returns {Object} Validation result
|
||||||
|
*/
|
||||||
|
validateCode() {
|
||||||
|
if (!this.currentLesson) {
|
||||||
|
return { isValid: false, message: 'No active lesson to validate against.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = validateUserCode(this.userCode, this.currentLesson);
|
||||||
|
|
||||||
|
// Display feedback to the user
|
||||||
|
showFeedback(result.isValid, result.message);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current state of the lesson
|
||||||
|
* @returns {Object} The current lesson state
|
||||||
|
*/
|
||||||
|
getCurrentState() {
|
||||||
|
return {
|
||||||
|
module: this.currentModule,
|
||||||
|
lesson: this.currentLesson,
|
||||||
|
lessonIndex: this.currentLessonIndex,
|
||||||
|
userCode: this.userCode,
|
||||||
|
totalLessons: this.currentModule ? this.currentModule.lessons.length : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save progress to localStorage
|
||||||
|
*/
|
||||||
|
saveProgress() {
|
||||||
|
if (!this.currentModule || !this.currentLesson) return;
|
||||||
|
|
||||||
|
const progressData = {
|
||||||
|
moduleId: this.currentModule.id,
|
||||||
|
lessonIndex: this.currentLessonIndex,
|
||||||
|
userCode: this.userCode,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem('cssQuest_progress', JSON.stringify(progressData));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load progress from localStorage
|
||||||
|
* @param {Array} modules - Available modules
|
||||||
|
* @returns {Object|null} Loaded progress data or null if not found
|
||||||
|
*/
|
||||||
|
loadProgress(modules) {
|
||||||
|
const savedProgress = localStorage.getItem('cssQuest_progress');
|
||||||
|
if (!savedProgress) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const progressData = JSON.parse(savedProgress);
|
||||||
|
|
||||||
|
// Find the module
|
||||||
|
const module = modules.find(m => m.id === progressData.moduleId);
|
||||||
|
if (!module) return null;
|
||||||
|
|
||||||
|
this.setModule(module);
|
||||||
|
this.setLessonByIndex(progressData.lessonIndex);
|
||||||
|
|
||||||
|
// Restore user code if available
|
||||||
|
if (progressData.userCode) {
|
||||||
|
this.userCode = progressData.userCode;
|
||||||
|
this.renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
return progressData;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading progress:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the current state
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
if (this.currentLesson) {
|
||||||
|
this.userCode = this.currentLesson.initialCode || '';
|
||||||
|
this.renderPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all saved progress
|
||||||
|
*/
|
||||||
|
clearProgress() {
|
||||||
|
localStorage.removeItem('cssQuest_progress');
|
||||||
|
}
|
||||||
|
}
|
||||||
366
src/js/app.js
Normal file
366
src/js/app.js
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import { LessonEngine } from './LessonEngine';
|
||||||
|
import { renderLesson, renderModuleList, renderLevelIndicator, showFeedback } from './renderer';
|
||||||
|
import { validateUserCode } from './validator';
|
||||||
|
import { loadModules } from '../lessons/lesson-config';
|
||||||
|
|
||||||
|
// Main Application state
|
||||||
|
const state = {
|
||||||
|
currentModule: null,
|
||||||
|
currentLessonIndex: 0,
|
||||||
|
modules: [],
|
||||||
|
userProgress: {}, // Format: { moduleId: { completed: [0, 2, 3], current: 4 } }
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
const elements = {
|
||||||
|
moduleList: document.querySelector('.module-list'),
|
||||||
|
lessonTitle: document.getElementById('lesson-title'),
|
||||||
|
lessonDescription: document.getElementById('lesson-description'),
|
||||||
|
taskInstruction: document.getElementById('task-instruction'),
|
||||||
|
previewArea: document.getElementById('preview-area'),
|
||||||
|
editorPrefix: document.getElementById('editor-prefix'),
|
||||||
|
codeInput: document.getElementById('code-input'),
|
||||||
|
editorSuffix: document.getElementById('editor-suffix'),
|
||||||
|
prevBtn: document.getElementById('prev-btn'),
|
||||||
|
nextBtn: document.getElementById('next-btn'),
|
||||||
|
runBtn: document.getElementById('run-btn'),
|
||||||
|
levelIndicator: document.getElementById('level-indicator'),
|
||||||
|
modalContainer: document.getElementById('modal-container'),
|
||||||
|
modalTitle: document.getElementById('modal-title'),
|
||||||
|
modalContent: document.getElementById('modal-content'),
|
||||||
|
modalClose: document.getElementById('modal-close'),
|
||||||
|
moduleSelectorBtn: document.getElementById('module-selector-btn'),
|
||||||
|
resetBtn: document.getElementById('reset-btn'),
|
||||||
|
helpBtn: document.getElementById('help-btn'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the lesson engine
|
||||||
|
const lessonEngine = new LessonEngine();
|
||||||
|
|
||||||
|
// Load user progress from localStorage
|
||||||
|
function loadUserProgress() {
|
||||||
|
const savedProgress = localStorage.getItem('codeCrispiesProgress');
|
||||||
|
if (savedProgress) {
|
||||||
|
state.userProgress = JSON.parse(savedProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save user progress to localStorage
|
||||||
|
function saveUserProgress() {
|
||||||
|
localStorage.setItem('codeCrispiesProgress', JSON.stringify(state.userProgress));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the module list
|
||||||
|
async function initializeModules() {
|
||||||
|
try {
|
||||||
|
state.modules = await loadModules();
|
||||||
|
renderModuleList(elements.moduleList, state.modules, selectModule);
|
||||||
|
|
||||||
|
// Select the first module or the last one user was on
|
||||||
|
const lastModuleId = localStorage.getItem('lastModuleId');
|
||||||
|
if (lastModuleId && state.modules.find(m => m.id === lastModuleId)) {
|
||||||
|
selectModule(lastModuleId);
|
||||||
|
} else if (state.modules.length > 0) {
|
||||||
|
selectModule(state.modules[0].id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load modules:', error);
|
||||||
|
elements.lessonDescription.textContent = 'Failed to load modules. Please refresh the page.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select a module
|
||||||
|
function selectModule(moduleId) {
|
||||||
|
const selectedModule = state.modules.find(module => module.id === moduleId);
|
||||||
|
if (!selectedModule) return;
|
||||||
|
|
||||||
|
state.currentModule = selectedModule;
|
||||||
|
|
||||||
|
// Update module list UI
|
||||||
|
const moduleItems = elements.moduleList.querySelectorAll('.module-list-item');
|
||||||
|
moduleItems.forEach(item => {
|
||||||
|
item.classList.remove('active');
|
||||||
|
if (item.dataset.moduleId === moduleId) {
|
||||||
|
item.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load user progress for this module
|
||||||
|
if (!state.userProgress[moduleId]) {
|
||||||
|
state.userProgress[moduleId] = { completed: [], current: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
state.currentLessonIndex = state.userProgress[moduleId].current || 0;
|
||||||
|
loadCurrentLesson();
|
||||||
|
|
||||||
|
// Save the last selected module
|
||||||
|
localStorage.setItem('lastModuleId', moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the current lesson
|
||||||
|
function loadCurrentLesson() {
|
||||||
|
if (!state.currentModule || !state.currentModule.lessons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure lesson index is in bounds
|
||||||
|
if (state.currentLessonIndex >= state.currentModule.lessons.length) {
|
||||||
|
state.currentLessonIndex = state.currentModule.lessons.length - 1;
|
||||||
|
} else if (state.currentLessonIndex < 0) {
|
||||||
|
state.currentLessonIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lesson = state.currentModule.lessons[state.currentLessonIndex];
|
||||||
|
lessonEngine.setLesson(lesson);
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
renderLesson(
|
||||||
|
elements.lessonTitle,
|
||||||
|
elements.lessonDescription,
|
||||||
|
elements.taskInstruction,
|
||||||
|
elements.previewArea,
|
||||||
|
elements.editorPrefix,
|
||||||
|
elements.codeInput,
|
||||||
|
elements.editorSuffix,
|
||||||
|
lesson
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update level indicator
|
||||||
|
renderLevelIndicator(
|
||||||
|
elements.levelIndicator,
|
||||||
|
state.currentLessonIndex + 1,
|
||||||
|
state.currentModule.lessons.length
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update navigation buttons
|
||||||
|
updateNavigationButtons();
|
||||||
|
|
||||||
|
// Save current progress
|
||||||
|
state.userProgress[state.currentModule.id].current = state.currentLessonIndex;
|
||||||
|
saveUserProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update navigation buttons state
|
||||||
|
function updateNavigationButtons() {
|
||||||
|
elements.prevBtn.disabled = state.currentLessonIndex === 0;
|
||||||
|
elements.nextBtn.disabled = !state.currentModule ||
|
||||||
|
state.currentLessonIndex === state.currentModule.lessons.length - 1;
|
||||||
|
|
||||||
|
// Style changes for disabled buttons
|
||||||
|
if (elements.prevBtn.disabled) {
|
||||||
|
elements.prevBtn.classList.add('btn-disabled');
|
||||||
|
} else {
|
||||||
|
elements.prevBtn.classList.remove('btn-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.nextBtn.disabled) {
|
||||||
|
elements.nextBtn.classList.add('btn-disabled');
|
||||||
|
} else {
|
||||||
|
elements.nextBtn.classList.remove('btn-disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to the next lesson
|
||||||
|
function nextLesson() {
|
||||||
|
if (!state.currentModule) return;
|
||||||
|
|
||||||
|
if (state.currentLessonIndex < state.currentModule.lessons.length - 1) {
|
||||||
|
state.currentLessonIndex++;
|
||||||
|
loadCurrentLesson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to the previous lesson
|
||||||
|
function prevLesson() {
|
||||||
|
if (state.currentLessonIndex > 0) {
|
||||||
|
state.currentLessonIndex--;
|
||||||
|
loadCurrentLesson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the user code
|
||||||
|
function runCode() {
|
||||||
|
const userCode = elements.codeInput.value;
|
||||||
|
const lesson = state.currentModule.lessons[state.currentLessonIndex];
|
||||||
|
|
||||||
|
const validationResult = validateUserCode(userCode, lesson);
|
||||||
|
|
||||||
|
if (validationResult.isValid) {
|
||||||
|
// Mark lesson as completed
|
||||||
|
const moduleProgress = state.userProgress[state.currentModule.id];
|
||||||
|
if (!moduleProgress.completed.includes(state.currentLessonIndex)) {
|
||||||
|
moduleProgress.completed.push(state.currentLessonIndex);
|
||||||
|
saveUserProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success feedback
|
||||||
|
showFeedback(true, validationResult.message || 'Great job! Your code works correctly.');
|
||||||
|
|
||||||
|
// Apply the code to see the result
|
||||||
|
lessonEngine.applyUserCode(userCode);
|
||||||
|
|
||||||
|
// Enable the next button if not already on the last lesson
|
||||||
|
if (state.currentLessonIndex < state.currentModule.lessons.length - 1) {
|
||||||
|
elements.nextBtn.disabled = false;
|
||||||
|
elements.nextBtn.classList.remove('btn-disabled');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show error feedback
|
||||||
|
showFeedback(false, validationResult.message || 'Your code doesn\'t seem to be correct. Try again!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the module selector modal
|
||||||
|
function showModuleSelector() {
|
||||||
|
debugger;
|
||||||
|
elements.modalTitle.textContent = 'Select a Module';
|
||||||
|
|
||||||
|
// Create module buttons
|
||||||
|
const moduleButtons = state.modules.map(module => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.classList.add('btn', 'module-button');
|
||||||
|
button.style.display = 'block';
|
||||||
|
button.style.width = '100%';
|
||||||
|
button.style.marginBottom = '10px';
|
||||||
|
button.style.padding = '15px';
|
||||||
|
button.style.textAlign = 'left';
|
||||||
|
|
||||||
|
// Add completion status
|
||||||
|
const progress = state.userProgress[module.id];
|
||||||
|
const completedCount = progress ? progress.completed.length : 0;
|
||||||
|
debugger;
|
||||||
|
const totalLessons = module.lessons.length;
|
||||||
|
const percentComplete = Math.round((completedCount / totalLessons) * 100);
|
||||||
|
|
||||||
|
button.innerHTML = `
|
||||||
|
<strong>${module.title}</strong>
|
||||||
|
<div style="margin-top: 5px; font-size: 0.8rem; color: var(--light-text);">
|
||||||
|
${module.description}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 8px; height: 6px; background-color: #f0f0f0; border-radius: 3px;">
|
||||||
|
<div style="height: 100%; width: ${percentComplete}%; background-color: var(--primary-color); border-radius: 3px;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 5px; font-size: 0.8rem; text-align: right;">
|
||||||
|
${completedCount}/${totalLessons} lessons completed
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
selectModule(module.id);
|
||||||
|
closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear and update modal content
|
||||||
|
elements.modalContent.innerHTML = '';
|
||||||
|
moduleButtons.forEach(button => {
|
||||||
|
elements.modalContent.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
elements.modalContainer.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show help modal
|
||||||
|
function showHelp() {
|
||||||
|
elements.modalTitle.textContent = 'Help';
|
||||||
|
|
||||||
|
elements.modalContent.innerHTML = `
|
||||||
|
<h3>How to Use Code Crispies</h3>
|
||||||
|
<p>Code Crispies is an interactive platform for learning CSS through practical exercises.</p>
|
||||||
|
|
||||||
|
<h4>Getting Started</h4>
|
||||||
|
<p>Select a module from the sidebar to start learning. Each module contains a series of lessons focused on specific CSS concepts.</p>
|
||||||
|
|
||||||
|
<h4>Completing Lessons</h4>
|
||||||
|
<p>For each lesson:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Read the instructions and objective</li>
|
||||||
|
<li>Write your CSS code in the editor</li>
|
||||||
|
<li>Click "Run" to test your solution</li>
|
||||||
|
<li>If correct, you can proceed to the next lesson</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h4>Controls</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Run</strong> - Test your CSS code</li>
|
||||||
|
<li><strong>Previous/Next</strong> - Navigate between lessons</li>
|
||||||
|
<li><strong>Modules</strong> - Select a different learning module</li>
|
||||||
|
<li><strong>Reset Progress</strong> - Clear all your saved progress</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Tips</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Use the preview area to see how your CSS affects the elements</li>
|
||||||
|
<li>Your progress is automatically saved in your browser storage</li>
|
||||||
|
<li>You can revisit completed lessons at any time</li>
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
elements.modalContainer.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset user progress
|
||||||
|
function resetProgress() {
|
||||||
|
elements.modalTitle.textContent = 'Reset Progress';
|
||||||
|
|
||||||
|
elements.modalContent.innerHTML = `
|
||||||
|
<p>Are you sure you want to reset all your progress? This cannot be undone.</p>
|
||||||
|
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;">
|
||||||
|
<button id="cancel-reset" class="btn">Cancel</button>
|
||||||
|
<button id="confirm-reset" class="btn btn-primary">Reset Progress</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('cancel-reset').addEventListener('click', closeModal);
|
||||||
|
document.getElementById('confirm-reset').addEventListener('click', () => {
|
||||||
|
localStorage.removeItem('codeCrispiesProgress');
|
||||||
|
localStorage.removeItem('lastModuleId');
|
||||||
|
state.userProgress = {};
|
||||||
|
closeModal();
|
||||||
|
|
||||||
|
// Reload the current module
|
||||||
|
if (state.currentModule) {
|
||||||
|
const currentModuleId = state.currentModule.id;
|
||||||
|
selectModule(currentModuleId);
|
||||||
|
} else if (state.modules.length > 0) {
|
||||||
|
selectModule(state.modules[0].id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.modalContainer.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
function closeModal() {
|
||||||
|
elements.modalContainer.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the application
|
||||||
|
function init() {
|
||||||
|
loadUserProgress();
|
||||||
|
initializeModules();
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
elements.prevBtn.addEventListener('click', prevLesson);
|
||||||
|
elements.nextBtn.addEventListener('click', nextLesson);
|
||||||
|
elements.runBtn.addEventListener('click', runCode);
|
||||||
|
elements.modalClose.addEventListener('click', closeModal);
|
||||||
|
elements.moduleSelectorBtn.addEventListener('click', showModuleSelector);
|
||||||
|
elements.resetBtn.addEventListener('click', resetProgress);
|
||||||
|
elements.helpBtn.addEventListener('click', showHelp);
|
||||||
|
|
||||||
|
// Handle keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// Ctrl+Enter to run code
|
||||||
|
if (e.ctrlKey && e.key === 'Enter') {
|
||||||
|
runCode();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the application
|
||||||
|
init();
|
||||||
119
src/js/renderer.js
Normal file
119
src/js/renderer.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* Renderer - Handles UI updates for the CSS learning platform
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Feedback elements cache
|
||||||
|
let feedbackElement = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the module list in the sidebar
|
||||||
|
* @param {HTMLElement} container - The container element for the module list
|
||||||
|
* @param {Array} modules - The list of modules
|
||||||
|
* @param {Function} onSelectModule - Callback when a module is selected
|
||||||
|
*/
|
||||||
|
export function renderModuleList(container, modules, onSelectModule) {
|
||||||
|
// Clear the container
|
||||||
|
container.innerHTML = '<h3>Modules</h3>';
|
||||||
|
|
||||||
|
// Create list items for each module
|
||||||
|
modules.forEach(module => {
|
||||||
|
const moduleItem = document.createElement('div');
|
||||||
|
moduleItem.classList.add('module-list-item');
|
||||||
|
moduleItem.dataset.moduleId = module.id;
|
||||||
|
moduleItem.textContent = module.title;
|
||||||
|
|
||||||
|
moduleItem.addEventListener('click', () => {
|
||||||
|
onSelectModule(module.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(moduleItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a lesson in the UI
|
||||||
|
* @param {HTMLElement} titleEl - The lesson title element
|
||||||
|
* @param {HTMLElement} descriptionEl - The lesson description element
|
||||||
|
* @param {HTMLElement} taskEl - The task instruction element
|
||||||
|
* @param {HTMLElement} previewEl - The preview area element
|
||||||
|
* @param {HTMLElement} prefixEl - The code editor prefix element
|
||||||
|
* @param {HTMLElement} inputEl - The code input element
|
||||||
|
* @param {HTMLElement} suffixEl - The code editor suffix element
|
||||||
|
* @param {Object} lesson - The lesson object
|
||||||
|
*/
|
||||||
|
export function renderLesson(
|
||||||
|
titleEl,
|
||||||
|
descriptionEl,
|
||||||
|
taskEl,
|
||||||
|
previewEl,
|
||||||
|
prefixEl,
|
||||||
|
inputEl,
|
||||||
|
suffixEl,
|
||||||
|
lesson
|
||||||
|
) {
|
||||||
|
// Set lesson title and description
|
||||||
|
titleEl.textContent = lesson.title || 'Untitled Lesson';
|
||||||
|
descriptionEl.innerHTML = lesson.description || '';
|
||||||
|
|
||||||
|
// Set task instructions
|
||||||
|
taskEl.innerHTML = lesson.task || '';
|
||||||
|
|
||||||
|
// Set code editor contents
|
||||||
|
prefixEl.textContent = lesson.codePrefix || '';
|
||||||
|
inputEl.value = lesson.initialCode || '';
|
||||||
|
suffixEl.textContent = lesson.codeSuffix || '';
|
||||||
|
|
||||||
|
// Clear any existing feedback
|
||||||
|
clearFeedback();
|
||||||
|
|
||||||
|
// Initial preview render with empty user code
|
||||||
|
// The LessonEngine will handle this when it's first set
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the level indicator
|
||||||
|
* @param {HTMLElement} element - The level indicator element
|
||||||
|
* @param {number} current - The current level number
|
||||||
|
* @param {number} total - The total number of levels
|
||||||
|
*/
|
||||||
|
export function renderLevelIndicator(element, current, total) {
|
||||||
|
element.textContent = `Lesson ${current} of ${total}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show feedback for user submissions
|
||||||
|
* @param {boolean} isSuccess - Whether the submission was successful
|
||||||
|
* @param {string} message - The feedback message
|
||||||
|
*/
|
||||||
|
export function showFeedback(isSuccess, message) {
|
||||||
|
// Clear any existing feedback
|
||||||
|
clearFeedback();
|
||||||
|
|
||||||
|
// Create feedback element
|
||||||
|
feedbackElement = document.createElement('div');
|
||||||
|
feedbackElement.classList.add(isSuccess ? 'feedback-success' : 'feedback-error');
|
||||||
|
feedbackElement.textContent = message;
|
||||||
|
|
||||||
|
// Find where to insert the feedback
|
||||||
|
const insertAfter = document.querySelector('.code-editor');
|
||||||
|
if (insertAfter && insertAfter.parentNode) {
|
||||||
|
insertAfter.parentNode.insertBefore(feedbackElement, insertAfter.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-remove feedback after some time if successful
|
||||||
|
if (isSuccess) {
|
||||||
|
setTimeout(() => {
|
||||||
|
clearFeedback();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear any existing feedback
|
||||||
|
*/
|
||||||
|
export function clearFeedback() {
|
||||||
|
if (feedbackElement && feedbackElement.parentNode) {
|
||||||
|
feedbackElement.parentNode.removeChild(feedbackElement);
|
||||||
|
}
|
||||||
|
feedbackElement = null;
|
||||||
|
}
|
||||||
183
src/js/validator.js
Normal file
183
src/js/validator.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* Validator - Functions to validate user CSS code
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate user CSS code against the lesson requirements
|
||||||
|
* @param {string} userCode - User submitted CSS code
|
||||||
|
* @param {Object} lesson - The current lesson object
|
||||||
|
* @returns {Object} Validation result with isValid and message properties
|
||||||
|
*/
|
||||||
|
export function validateUserCode(userCode, lesson) {
|
||||||
|
if (!lesson || !lesson.validations) {
|
||||||
|
return { isValid: true, message: 'No validations specified for this lesson.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the validations array from the lesson
|
||||||
|
const validations = lesson.validations;
|
||||||
|
|
||||||
|
// Default validation result
|
||||||
|
let result = {
|
||||||
|
isValid: true,
|
||||||
|
message: 'Your code looks good!'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process each validation rule
|
||||||
|
for (const validation of validations) {
|
||||||
|
const { type, value, message, options } = validation;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'contains':
|
||||||
|
if (!containsValidation(userCode, value, options)) {
|
||||||
|
return { isValid: false, message: message || `Your code should include "${value}".` };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'not_contains':
|
||||||
|
if (containsValidation(userCode, value, options)) {
|
||||||
|
return { isValid: false, message: message || `Your code should not include "${value}".` };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'regex':
|
||||||
|
if (!regexValidation(userCode, value, options)) {
|
||||||
|
return { isValid: false, message: message || 'Your code does not match the expected pattern.' };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'property_value':
|
||||||
|
if (!propertyValueValidation(userCode, value, options)) {
|
||||||
|
return { isValid: false, message: message || `The "${value.property}" property should be set to "${value.expected}".` };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'syntax':
|
||||||
|
const syntaxResult = syntaxValidation(userCode);
|
||||||
|
if (!syntaxResult.isValid) {
|
||||||
|
return { isValid: false, message: message || `CSS syntax error: ${syntaxResult.error}` };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'custom':
|
||||||
|
if (validation.validator && typeof validation.validator === 'function') {
|
||||||
|
const customResult = validation.validator(userCode);
|
||||||
|
if (!customResult.isValid) {
|
||||||
|
return { isValid: false, message: customResult.message || message || 'Your code does not meet the requirements.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Add more validation types as needed
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown validation type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've passed all validations, return success
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if code contains a specific string or pattern
|
||||||
|
* @param {string} code - User CSS code
|
||||||
|
* @param {string} value - String to check for
|
||||||
|
* @param {Object} options - Validation options
|
||||||
|
* @returns {boolean} Whether the validation passes
|
||||||
|
*/
|
||||||
|
function containsValidation(code, value, options = {}) {
|
||||||
|
const { caseSensitive = true, wholeWord = false } = options;
|
||||||
|
|
||||||
|
if (!caseSensitive) {
|
||||||
|
code = code.toLowerCase();
|
||||||
|
value = value.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wholeWord) {
|
||||||
|
const regex = new RegExp(`\\b${escapeRegExp(value)}\\b`, caseSensitive ? '' : 'i');
|
||||||
|
return regex.test(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
return code.includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if code matches a regex pattern
|
||||||
|
* @param {string} code - User CSS code
|
||||||
|
* @param {string} pattern - Regex pattern to check
|
||||||
|
* @param {Object} options - Validation options
|
||||||
|
* @returns {boolean} Whether the validation passes
|
||||||
|
*/
|
||||||
|
function regexValidation(code, pattern, options = {}) {
|
||||||
|
const { caseSensitive = true, multiline = true } = options;
|
||||||
|
|
||||||
|
let flags = '';
|
||||||
|
if (!caseSensitive) flags += 'i';
|
||||||
|
if (multiline) flags += 'm';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(pattern, flags);
|
||||||
|
return regex.test(code);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid regex in validation:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a CSS property has the expected value
|
||||||
|
* @param {string} code - User CSS code
|
||||||
|
* @param {Object} value - Object with property and expected value
|
||||||
|
* @param {Object} options - Validation options
|
||||||
|
* @returns {boolean} Whether the validation passes
|
||||||
|
*/
|
||||||
|
function propertyValueValidation(code, value, options = {}) {
|
||||||
|
const { property, expected } = value;
|
||||||
|
const { exact = false } = options;
|
||||||
|
|
||||||
|
// Create a regex to extract the property value
|
||||||
|
// This is a simplified version and might not handle all CSS syntax nuances
|
||||||
|
const propertyRegex = new RegExp(`${escapeRegExp(property)}\\s*:\\s*([^;\\}]+)`, 'i');
|
||||||
|
const match = code.match(propertyRegex);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
// Property not found
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualValue = match[1].trim();
|
||||||
|
|
||||||
|
if (exact) {
|
||||||
|
return actualValue === expected;
|
||||||
|
} else {
|
||||||
|
// Allow for flexible matching
|
||||||
|
return actualValue.toLowerCase().includes(expected.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate CSS syntax
|
||||||
|
* @param {string} code - User CSS code
|
||||||
|
* @returns {Object} Validation result
|
||||||
|
*/
|
||||||
|
function syntaxValidation(code) {
|
||||||
|
try {
|
||||||
|
// Create a hidden style element to test the CSS
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = code;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
document.head.removeChild(style);
|
||||||
|
return { isValid: true };
|
||||||
|
} catch (e) {
|
||||||
|
return { isValid: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape string for safe use in regex
|
||||||
|
* @param {string} string - String to escape
|
||||||
|
* @returns {string} Escaped string
|
||||||
|
*/
|
||||||
|
function escapeRegExp(string) {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
296
src/lessons/configs/basics.json
Normal file
296
src/lessons/configs/basics.json
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
{
|
||||||
|
"id": "basics",
|
||||||
|
"title": "CSS Basics",
|
||||||
|
"description": "Learn the fundamental concepts of CSS styling",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "basics-1",
|
||||||
|
"title": "Introduction to Selectors",
|
||||||
|
"description": "Selectors are patterns used to select HTML elements you want to style. In this lesson, you'll learn how to target elements with CSS.",
|
||||||
|
"task": "Style the paragraph element by making its text color blue. Use the 'color' property with the value 'blue'.",
|
||||||
|
"previewHTML": "<p class='target'>This paragraph needs to be blue!</p><p>This paragraph should remain unchanged.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 20px; }",
|
||||||
|
"sandboxCSS": ".target { outline: 2px dashed #ccc; padding: 10px; }",
|
||||||
|
"codePrefix": "/* Style the paragraph with class 'target' */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".target",
|
||||||
|
"message": "Make sure to use the '.target' class selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color",
|
||||||
|
"message": "Use the 'color' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "blue"
|
||||||
|
},
|
||||||
|
"message": "Set the color property to 'blue'",
|
||||||
|
"options": {
|
||||||
|
"exact": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "basics-2",
|
||||||
|
"title": "Box Model Basics",
|
||||||
|
"description": "The CSS box model is a fundamental concept that describes how elements are sized and spaced on a page.",
|
||||||
|
"task": "Add padding of 20px and a 2px solid border with color #333 to the box element.",
|
||||||
|
"previewHTML": "<div class='box'>This box needs padding and a border!</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 20px; } .box { background-color: #f0f0f0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add padding and border to the box */\n.box {\n /* Add your code below */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "padding",
|
||||||
|
"message": "Use the 'padding' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border",
|
||||||
|
"message": "Use the 'border' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "padding",
|
||||||
|
"expected": "20px"
|
||||||
|
},
|
||||||
|
"message": "Set the padding to '20px'",
|
||||||
|
"options": {
|
||||||
|
"exact": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "border:\\s*2px\\s+solid\\s+#333",
|
||||||
|
"message": "Set the border to '2px solid #333'",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "basics-3",
|
||||||
|
"title": "Colors and Backgrounds",
|
||||||
|
"description": "Learn how to apply background colors and control text coloring in different ways.",
|
||||||
|
"task": "Style the element with a background color of #e0f7fa and text color of #01579b.",
|
||||||
|
"previewHTML": "<div class='colorbox'>This element needs colors!</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 20px; } .colorbox { padding: 25px; text-align: center; font-weight: bold; font-size: 18px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Style the colorbox with background and text colors */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".colorbox",
|
||||||
|
"message": "Use the '.colorbox' class selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color",
|
||||||
|
"message": "Use the 'background-color' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color",
|
||||||
|
"message": "Use the 'color' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "#e0f7fa"
|
||||||
|
},
|
||||||
|
"message": "Set the background-color to '#e0f7fa'",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "#01579b"
|
||||||
|
},
|
||||||
|
"message": "Set the text color to '#01579b'",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "basics-4",
|
||||||
|
"title": "Typography Basics",
|
||||||
|
"description": "Learn how to control text appearance with CSS typography properties.",
|
||||||
|
"task": "Style the heading with font-family 'Georgia, serif', font-size of 24px, and add text-shadow of 1px 1px 2px #aaa.",
|
||||||
|
"previewHTML": "<h2 class='heading'>Style This Heading Text</h2>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 20px; }",
|
||||||
|
"sandboxCSS": ".heading { border-bottom: 1px solid #ddd; padding-bottom: 10px; }",
|
||||||
|
"codePrefix": "/* Style the heading text */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".heading",
|
||||||
|
"message": "Use the '.heading' class selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-family",
|
||||||
|
"message": "Use the 'font-family' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-size",
|
||||||
|
"message": "Use the 'font-size' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "text-shadow",
|
||||||
|
"message": "Use the 'text-shadow' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "text-shadow:\\s*1px\\s+1px\\s+2px\\s+#aaa",
|
||||||
|
"message": "Set the text-shadow to '1px 1px 2px #aaa'",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "basics-5",
|
||||||
|
"title": "CSS Units",
|
||||||
|
"description": "Learn about different CSS units like pixels, percentages, em, rem, and when to use each.",
|
||||||
|
"task": "Set the width of the box to 80%, the max-width to 600px, and the font-size to 1.2rem.",
|
||||||
|
"previewHTML": "<div class='units-box'>This box needs sizing with different units.</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 20px; } .units-box { background-color: #f5f5f5; padding: 15px; border: 1px solid #ddd; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Style the box using different units */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".units-box",
|
||||||
|
"message": "Use the '.units-box' class selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "width",
|
||||||
|
"message": "Use the 'width' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "max-width",
|
||||||
|
"message": "Use the 'max-width' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-size",
|
||||||
|
"message": "Use the 'font-size' property",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "width",
|
||||||
|
"expected": "80%"
|
||||||
|
},
|
||||||
|
"message": "Set the width to '80%'",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "max-width",
|
||||||
|
"expected": "600px"
|
||||||
|
},
|
||||||
|
"message": "Set the max-width to '600px'",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-size",
|
||||||
|
"expected": "1.2rem"
|
||||||
|
},
|
||||||
|
"message": "Set the font-size to '1.2rem'",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
src/lessons/configs/flexbox.json
Normal file
4
src/lessons/configs/flexbox.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"id": "flexbox",
|
||||||
|
"title": "Flexbox Layout"
|
||||||
|
}
|
||||||
1
src/lessons/configs/grid.json
Normal file
1
src/lessons/configs/grid.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
99
src/lessons/lesson-config.js
Normal file
99
src/lessons/lesson-config.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* Lesson Config - Functions for loading lesson configurations
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Import lesson configs
|
||||||
|
import flexboxConfig from './configs/flexbox.json';
|
||||||
|
import gridConfig from './configs/grid.json';
|
||||||
|
import basicsConfig from './configs/basics.json';
|
||||||
|
|
||||||
|
// Module store
|
||||||
|
const moduleStore = [
|
||||||
|
// flexboxConfig,
|
||||||
|
// gridConfig,
|
||||||
|
basicsConfig
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all available modules
|
||||||
|
* @returns {Promise<Array>} Promise resolving to array of modules
|
||||||
|
*/
|
||||||
|
export async function loadModules() {
|
||||||
|
// In a real app, we might load these from a server
|
||||||
|
return moduleStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a module by its ID
|
||||||
|
* @param {string} moduleId - The module ID to find
|
||||||
|
* @returns {Object|null} The module object or null if not found
|
||||||
|
*/
|
||||||
|
export function getModuleById(moduleId) {
|
||||||
|
return moduleStore.find(module => module.id === moduleId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load module configs from a URL
|
||||||
|
* @param {string} url - URL to load the config from
|
||||||
|
* @returns {Promise<Object>} Promise resolving to the module config
|
||||||
|
*/
|
||||||
|
export async function loadModuleFromUrl(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load module: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleConfig = await response.json();
|
||||||
|
validateModuleConfig(moduleConfig);
|
||||||
|
|
||||||
|
return moduleConfig;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading module from URL:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a module configuration
|
||||||
|
* @param {Object} config - The module configuration to validate
|
||||||
|
* @throws {Error} If the configuration is invalid
|
||||||
|
*/
|
||||||
|
function validateModuleConfig(config) {
|
||||||
|
// Required fields
|
||||||
|
if (!config.id) throw new Error('Module config missing "id"');
|
||||||
|
if (!config.title) throw new Error('Module config missing "title"');
|
||||||
|
if (!Array.isArray(config.lessons)) throw new Error('Module config missing "lessons" array');
|
||||||
|
|
||||||
|
// Check each lesson
|
||||||
|
config.lessons.forEach((lesson, index) => {
|
||||||
|
if (!lesson.title) throw new Error(`Lesson ${index} missing "title"`);
|
||||||
|
if (!lesson.previewHTML) throw new Error(`Lesson ${index} missing "previewHTML"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a custom module to the store
|
||||||
|
* @param {Object} moduleConfig - The module configuration to add
|
||||||
|
* @returns {boolean} Success status
|
||||||
|
*/
|
||||||
|
export function addCustomModule(moduleConfig) {
|
||||||
|
try {
|
||||||
|
validateModuleConfig(moduleConfig);
|
||||||
|
|
||||||
|
// Check if module with same ID already exists
|
||||||
|
const existingIndex = moduleStore.findIndex(m => m.id === moduleConfig.id);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
// Replace existing module
|
||||||
|
moduleStore[existingIndex] = moduleConfig;
|
||||||
|
} else {
|
||||||
|
// Add new module
|
||||||
|
moduleStore.push(moduleConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding custom module:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
324
src/styles/main.css
Normal file
324
src/styles/main.css
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
:root {
|
||||||
|
--primary-color: #4a6bfd;
|
||||||
|
--primary-light: #7a93fe;
|
||||||
|
--primary-dark: #244ae8;
|
||||||
|
--secondary-color: #ff7e5f;
|
||||||
|
--text-color: #2c3e50;
|
||||||
|
--light-text: #777;
|
||||||
|
--bg-color: #f9f9f9;
|
||||||
|
--panel-bg: #ffffff;
|
||||||
|
--border-color: #e0e0e0;
|
||||||
|
--success-color: #2ecc71;
|
||||||
|
--error-color: #e74c3c;
|
||||||
|
--font-main: 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
--shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-main), serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Styles */
|
||||||
|
.header {
|
||||||
|
background-color: var(--panel-bg);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav ul {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Layout */
|
||||||
|
.main-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background-color: var(--panel-bg);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
position: sticky;
|
||||||
|
top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: calc(100% - 240px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Module List Styles */
|
||||||
|
.module-list {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-list-item {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-list-item:hover {
|
||||||
|
background-color: rgba(74, 107, 253, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-list-item.active {
|
||||||
|
background-color: rgba(74, 107, 253, 0.1);
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lesson Container */
|
||||||
|
.lesson-container {
|
||||||
|
background-color: var(--panel-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#lesson-title {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-description {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Challenge Container */
|
||||||
|
.challenge-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.challenge-container {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-area,
|
||||||
|
.editor-container {
|
||||||
|
width: 48%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-area {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 300px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-instruction {
|
||||||
|
background-color: rgba(74, 107, 253, 0.05);
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--light-text);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
outline: none;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Controls */
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-indicator {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--light-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: #fff;
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-main);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background-color: var(--panel-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--light-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feedback */
|
||||||
|
.feedback-success {
|
||||||
|
color: var(--success-color);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(46, 204, 113, 0.1);
|
||||||
|
border-left: 3px solid var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-error {
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(231, 76, 60, 0.1);
|
||||||
|
border-left: 3px solid var(--error-color);
|
||||||
|
}
|
||||||
15
vite.config.js
Normal file
15
vite.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: './src',
|
||||||
|
publicDir: './public',
|
||||||
|
build: {
|
||||||
|
outDir: './dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: true
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user