feat: add HTML lessons mode and side-by-side comparison UI
- Add HTML mode support with new validation types (element_exists, element_count, attribute_value, element_text, parent_child, sibling) - Create 3 HTML lesson modules: Elements, Forms Basic, Forms Validation - Implement side-by-side preview comparison (Your Output vs Expected) - Add merge animation with "Perfect Match!" overlay on validation success - Render expected output from solutionCode field in lesson JSON - Update schema to support HTML mode and solutionCode - Reorder modules: HTML first, then CSS, then Tailwind - Update tests for new functionality
This commit is contained in:
@@ -15,7 +15,7 @@ let feedbackTimeout = null;
|
||||
*/
|
||||
export function renderModuleList(container, modules, onSelectModule, onSelectLesson) {
|
||||
// Clear the container
|
||||
container.innerHTML = "<h3>CSS Lessons</h3>";
|
||||
container.innerHTML = "<h3>Lessons</h3>";
|
||||
|
||||
// Get user progress from localStorage
|
||||
const progressData = localStorage.getItem("codeCrispies.progress");
|
||||
|
||||
@@ -5,13 +5,205 @@
|
||||
export function validateUserCode(userCode, lesson) {
|
||||
const mode = lesson.mode || "css";
|
||||
|
||||
if (mode === "tailwind") {
|
||||
return validateTailwindClasses(userCode, lesson);
|
||||
} else {
|
||||
return validateCssCode(userCode, lesson);
|
||||
switch (mode) {
|
||||
case "html":
|
||||
return validateHtmlCode(userCode, lesson);
|
||||
case "tailwind":
|
||||
return validateTailwindClasses(userCode, lesson);
|
||||
case "css":
|
||||
default:
|
||||
return validateCssCode(userCode, lesson);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user HTML code against the lesson requirements
|
||||
* @param {string} userHtml - User submitted HTML code
|
||||
* @param {Object} lesson - The current lesson object
|
||||
* @returns {Object} Validation result with isValid and message properties
|
||||
*/
|
||||
function validateHtmlCode(userHtml, lesson) {
|
||||
if (!lesson || !lesson.validations) {
|
||||
return { isValid: true, message: "No validations specified for this lesson." };
|
||||
}
|
||||
|
||||
// Parse the HTML using DOMParser
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(userHtml, "text/html");
|
||||
|
||||
// Check for parse errors (DOMParser doesn't throw, but inserts error elements for XML)
|
||||
// For HTML mode, it's more lenient, so we mainly check validations
|
||||
|
||||
const validations = lesson.validations;
|
||||
|
||||
let result = {
|
||||
isValid: true,
|
||||
validCases: 0,
|
||||
totalCases: validations.length,
|
||||
message: "Your HTML looks great!"
|
||||
};
|
||||
|
||||
for (const validation of validations) {
|
||||
const { type, value, message } = validation;
|
||||
let validationPassed = false;
|
||||
|
||||
switch (type) {
|
||||
case "element_exists":
|
||||
// value is a CSS selector string
|
||||
validationPassed = doc.querySelector(value) !== null;
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || `Missing element: ${value}`
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "element_count":
|
||||
// value is { selector: string, count?: number, min?: number }
|
||||
const elements = doc.querySelectorAll(value.selector);
|
||||
if (value.count !== undefined) {
|
||||
validationPassed = elements.length === value.count;
|
||||
} else if (value.min !== undefined) {
|
||||
validationPassed = elements.length >= value.min;
|
||||
} else {
|
||||
validationPassed = elements.length > 0;
|
||||
}
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || `Expected ${value.count || value.min + "+"} ${value.selector} element(s)`
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "attribute_value":
|
||||
// value is { selector: string, attr: string, value: any }
|
||||
const el = doc.querySelector(value.selector);
|
||||
if (!el) {
|
||||
validationPassed = false;
|
||||
} else if (value.value === true) {
|
||||
// Check attribute exists (boolean attribute like "required")
|
||||
validationPassed = el.hasAttribute(value.attr);
|
||||
} else if (value.value === null) {
|
||||
// Check attribute exists with any value
|
||||
validationPassed = el.hasAttribute(value.attr);
|
||||
} else {
|
||||
// Check attribute has specific value
|
||||
validationPassed = el.getAttribute(value.attr) === value.value;
|
||||
}
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || `Element ${value.selector} should have ${value.attr} attribute`
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "element_text":
|
||||
// value is { selector: string, text: string }
|
||||
const textEl = doc.querySelector(value.selector);
|
||||
validationPassed = textEl && textEl.textContent.includes(value.text);
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || `Element ${value.selector} should contain "${value.text}"`
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "parent_child":
|
||||
// value is { parent: string, child: string }
|
||||
const parentEl = doc.querySelector(value.parent);
|
||||
validationPassed = parentEl && parentEl.querySelector(value.child) !== null;
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || `${value.child} should be inside ${value.parent}`
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "sibling":
|
||||
// value is { first: string, then: string }
|
||||
const firstSibling = doc.querySelector(value.first);
|
||||
if (!firstSibling) {
|
||||
validationPassed = false;
|
||||
} else {
|
||||
// Check if "then" element comes after "first" element
|
||||
let nextEl = firstSibling.nextElementSibling;
|
||||
while (nextEl) {
|
||||
if (nextEl.matches(value.then)) {
|
||||
validationPassed = true;
|
||||
break;
|
||||
}
|
||||
nextEl = nextEl.nextElementSibling;
|
||||
}
|
||||
}
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || `${value.then} should follow ${value.first}`
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
// Fall back to text-based validations for simple checks
|
||||
case "contains":
|
||||
validationPassed = containsValidation(userHtml, value);
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || `Your HTML should include "${value}"`
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "not_contains":
|
||||
validationPassed = !containsValidation(userHtml, value);
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || `Your HTML should not include "${value}"`
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "regex":
|
||||
validationPassed = regexValidation(userHtml, value);
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || "Your HTML doesn't match the expected pattern"
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown HTML validation type: ${type}`);
|
||||
validationPassed = true;
|
||||
}
|
||||
|
||||
if (validationPassed) {
|
||||
result.validCases++;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result.validCases = validations.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateTailwindClasses(userClasses, lesson) {
|
||||
if (!lesson || !lesson.validations) {
|
||||
return { isValid: true, message: "No validations specified for this lesson." };
|
||||
|
||||
Reference in New Issue
Block a user