diff --git a/lessons/10-tailwind-basics.json b/lessons/10-tailwind-basics.json new file mode 100644 index 0000000..1f42b92 --- /dev/null +++ b/lessons/10-tailwind-basics.json @@ -0,0 +1,28 @@ +{ + "$schema": "../schemas/code-crispies-module-schema.json", + "id": "tailwind-basics", + "title": "Tailwind CSS Basics", + "mode": "tailwind", + "lessons": [ + { + "id": "bg-colors", + "title": "Background Colors", + "description": "Learn to apply background colors using Tailwind utilities.", + "task": "Add a blue background to the div using Tailwind classes.", + "previewHTML": "
Hello Tailwind!
", + "previewBaseCSS": "body { padding: 20px; font-family: sans-serif; }", + "initialCode": "", + "validations": [ + { + "type": "contains_class", + "value": "bg-blue-500", + "message": "Add the 'bg-blue-500' class for a blue background." + } + ], + "sandboxCSS": "", + "previewContainer": "" + } + ], + "description": "", + "difficulty": "advanced" +} diff --git a/schemas/code-crispies-module-schema.json b/schemas/code-crispies-module-schema.json index 61ac620..f74cbcc 100644 --- a/schemas/code-crispies-module-schema.json +++ b/schemas/code-crispies-module-schema.json @@ -17,6 +17,11 @@ "type": "string", "description": "Detailed description of the module content and purpose" }, + "mode": { + "type": "string", + "enum": ["css", "tailwind"], + "description": "Whether this module teaches CSS or Tailwind" + }, "difficulty": { "type": "string", "enum": ["beginner", "intermediate", "advanced"], @@ -53,6 +58,15 @@ "type": "string", "description": "Detailed description of the lesson content and concepts" }, + "mode": { + "type": "string", + "enum": ["css", "tailwind"], + "description": "Override module mode for individual lessons" + }, + "tailwindConfig": { + "type": "object", + "description": "Custom Tailwind configuration if needed" + }, "task": { "type": "string", "description": "The specific task instructions for the student to complete" @@ -91,7 +105,7 @@ "properties": { "type": { "type": "string", - "enum": ["contains", "not_contains", "regex", "property_value", "syntax", "custom"], + "enum": ["contains", "contains_class", "not_contains", "regex", "property_value", "syntax", "custom"], "description": "Type of validation to perform" }, "value": { diff --git a/src/app.js b/src/app.js index e1ff4e4..3d4c255 100644 --- a/src/app.js +++ b/src/app.js @@ -210,6 +210,19 @@ function resetSuccessIndicators() { elements.runBtn.classList.remove("re-run"); } +function updateEditorForMode(mode) { + const codeInput = elements.codeInput; + const editorLabel = document.querySelector(".editor-label"); + + if (mode === "tailwind") { + codeInput.placeholder = "Enter Tailwind classes (e.g., bg-blue-500 text-white p-4)"; + if (editorLabel) editorLabel.textContent = "Tailwind Classes:"; + } else { + codeInput.placeholder = "Enter your CSS code here..."; + if (editorLabel) editorLabel.textContent = "CSS Code:"; + } +} + // Configure editor layout based on display type function resetEditorLayout(lesson) { elements.validationIndicators.innerHTML = ""; @@ -229,8 +242,12 @@ function loadCurrentLesson() { } const lesson = state.currentModule.lessons[state.currentLessonIndex]; + const mode = lesson.mode || state.currentModule?.mode || "css"; lessonEngine.setLesson(lesson); + // Update UI based on mode + updateEditorForMode(mode); + // Reset any success indicators resetSuccessIndicators(); diff --git a/src/config/lessons.js b/src/config/lessons.js index c15d64b..c9f5382 100644 --- a/src/config/lessons.js +++ b/src/config/lessons.js @@ -11,22 +11,24 @@ import carouselConfig from "../../lessons/02-css-only-carousel.json"; // import selectorsConfig from "../../lessons/02-selectors.json"; // import colorsConfig from "../../lessons/03-colors.json"; // import typographyConfig from "../../lessons/04-typography.json"; -// import unitVariablesConfig from "../../lessons/05-units-variables.json"; +import unitVariablesConfig from "../../lessons/05-units-variables.json"; // import transitionsAnimationsConfig from "../../lessons/06-transitions-animations.json"; // import layoutConfig from "../../lessons/07-layouts.json"; // import responsiveConfig from "../../lessons/08-responsive.json"; +import tailwindConfig from "../../lessons/10-tailwind-basics.json"; // Module store const moduleStore = [ // basicsConfig, basicSelectorsConfig, - // advancedSelectorsConfig, + advancedSelectorsConfig, + tailwindConfig // carouselConfig // boxModelConfig, // selectorsConfig, // colorsConfig // typographyConfig, - // unitVariablesConfig, + // unitVariablesConfig // transitionsAnimationsConfig, // layoutConfig, // responsiveConfig @@ -37,8 +39,13 @@ const moduleStore = [ * @returns {Promise} Promise resolving to array of modules */ export async function loadModules() { - // In a real app, we might load these from a server - return moduleStore; + return moduleStore.map((module) => ({ + ...module, + lessons: module.lessons.map((lesson) => ({ + ...lesson, + mode: module.mode || "css" + })) + })); } /** diff --git a/src/helpers/validator.js b/src/helpers/validator.js index 63b8140..f191c06 100644 --- a/src/helpers/validator.js +++ b/src/helpers/validator.js @@ -2,13 +2,79 @@ * Validator - Functions to validate user CSS code */ +export function validateUserCode(userCode, lesson) { + const mode = lesson.mode || "css"; + + if (mode === "tailwind") { + return validateTailwindClasses(userCode, lesson); + } else { + return validateCssCode(userCode, lesson); + } +} + +function validateTailwindClasses(userClasses, lesson) { + if (!lesson || !lesson.validations) { + return { isValid: true, message: "No validations specified for this lesson." }; + } + + let result = { + isValid: true, + validCases: 0, + totalCases: lesson.validations.length, + message: "Your Tailwind classes look CRISPY!" + }; + + for (const validation of lesson.validations) { + const { type, value, message } = validation; + let validationPassed = false; + + switch (type) { + case "contains_class": + validationPassed = userClasses.split(/\s+/).includes(value); + if (!validationPassed) { + result = { + ...result, + isValid: false, + message: message || `Your classes should include "${value}".` + }; + } + break; + + case "contains_pattern": + const regex = new RegExp(value); + validationPassed = regex.test(userClasses); + if (!validationPassed) { + result = { + ...result, + isValid: false, + message: message || "Your classes don't match the expected pattern." + }; + } + break; + + default: + // Fall back to original CSS validation for other types + validationPassed = containsValidation(userClasses, value); + } + + if (validationPassed) { + result.validCases++; + } else { + return result; + } + } + + result.validCases = lesson.validations.length; + return result; +} + /** * 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) { +export function validateCssCode(userCode, lesson) { if (!lesson || !lesson.validations) { return { isValid: true, message: "No validations specified for this lesson." }; } diff --git a/src/impl/LessonEngine.js b/src/impl/LessonEngine.js index 93a242d..abfaf8c 100644 --- a/src/impl/LessonEngine.js +++ b/src/impl/LessonEngine.js @@ -111,44 +111,64 @@ export class LessonEngine { renderPreview() { if (!this.currentLesson) return; + const mode = this.currentLesson.mode || this.currentModule?.mode || "css"; 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); - // Get the complete CSS by combining all parts - const userCssWithWrapper = this.getCompleteCss(); - - // Write the content to the iframe const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; iframeDoc.open(); - iframeDoc.write(` - - - - - - - - - ${previewHTML || "
No preview available
"} - - - `); + + if (mode === "tailwind") { + // For Tailwind mode, user code goes directly in HTML classes + const htmlWithClasses = this.injectTailwindClasses(previewHTML, this.userCode); + iframeDoc.write(` + + + + + + + + + ${htmlWithClasses} + + + `); + } else { + // Original CSS mode + const userCssWithWrapper = this.getCompleteCss(); + iframeDoc.write(` + + + + + + + + + ${previewHTML} + + + `); + } + iframeDoc.close(); } + injectTailwindClasses(html, userClasses) { + // Replace placeholder in HTML with user's Tailwind classes + return html.replace(/{{USER_CLASSES}}/g, userClasses); + } + /** * Validate user code against the current lesson's requirements * @returns {Object} Validation result