feat: complete section color coding with logo, hints, editor themes, and footers

- Add section-specific CodeMirror syntax highlighting (purple selectors for CSS)
- Logo now uses section colors (CSS purple as default, changes per section)
- Add section color coding for hints
- Add full footer to section and reference pages
- Fix nav highlight updates for sidebar and prev/next navigation
- Change welcome module mode to CSS for purple theme on first lesson
- Rebrand "Code Crispies" to "CODE CRISPIES" across all translations
- Fix scroll to top on section page navigation
- Change HTML section color to raspberry (#c75b7a)
This commit is contained in:
2026-01-16 04:32:55 +01:00
parent 5ebb007568
commit 0a03d51e63
12 changed files with 321 additions and 123 deletions

View File

@@ -3,7 +3,7 @@
"id": "welcome",
"title": "Welcome",
"description": "Get started with Code Crispies",
"mode": "html",
"mode": "css",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [

View File

@@ -3,7 +3,7 @@
"id": "welcome",
"title": "مرحباً",
"description": "ابدأ مع Code Crispies",
"mode": "html",
"mode": "css",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [

View File

@@ -3,7 +3,7 @@
"id": "welcome",
"title": "Willkommen",
"description": "Erste Schritte mit Code Crispies",
"mode": "html",
"mode": "css",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [

View File

@@ -3,7 +3,7 @@
"id": "welcome",
"title": "Bienvenido",
"description": "Comienza con Code Crispies",
"mode": "html",
"mode": "css",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [

View File

@@ -3,7 +3,7 @@
"id": "welcome",
"title": "Witaj",
"description": "Rozpocznij przygodę z Code Crispies",
"mode": "html",
"mode": "css",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [

View File

@@ -3,7 +3,7 @@
"id": "welcome",
"title": "Ласкаво просимо",
"description": "Почніть з Code Crispies",
"mode": "html",
"mode": "css",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [

View File

@@ -156,6 +156,8 @@ const elements = {
closeSidebar: document.getElementById("close-sidebar"),
moduleList: document.getElementById("module-list"),
footerLessonLinks: document.getElementById("footer-lesson-links"),
refFooterLessonLinks: document.getElementById("ref-footer-lesson-links"),
sectionFooterLessonLinks: document.getElementById("section-footer-lesson-links"),
progressFill: document.getElementById("progress-fill"),
progressText: document.getElementById("progress-text"),
resetBtn: document.getElementById("reset-btn"),
@@ -456,6 +458,13 @@ function selectLesson(moduleId, lessonIndex) {
loadCurrentLesson();
// Update section color coding (after loadCurrentLesson to ensure content is loaded first)
const newState = lessonEngine.getCurrentState();
updateSectionColor(getModuleSection(newState.module));
// Update nav highlight
updateNavHighlight({ type: RouteType.LESSON, moduleId, lessonIndex });
// Close sidebar after selection on mobile
if (window.innerWidth <= 768) {
closeSidebar();
@@ -694,11 +703,15 @@ function nextLesson() {
// Update URL
updateHash(newState.module.id, newState.lessonIndex);
if (newState.module.id !== prevModuleId) {
const moduleChanged = newState.module.id !== prevModuleId;
if (moduleChanged) {
updateModuleHighlight(newState.module.id);
updateSectionColor(getModuleSection(newState.module));
}
loadCurrentLesson();
if (moduleChanged) {
updateSectionColor(getModuleSection(newState.module));
updateNavHighlight({ type: RouteType.LESSON, moduleId: newState.module.id, lessonIndex: newState.lessonIndex });
}
}
}
@@ -711,11 +724,15 @@ function prevLesson() {
// Update URL
updateHash(newState.module.id, newState.lessonIndex);
if (newState.module.id !== prevModuleId) {
const moduleChanged = newState.module.id !== prevModuleId;
if (moduleChanged) {
updateModuleHighlight(newState.module.id);
updateSectionColor(getModuleSection(newState.module));
}
loadCurrentLesson();
if (moduleChanged) {
updateSectionColor(getModuleSection(newState.module));
updateNavHighlight({ type: RouteType.LESSON, moduleId: newState.module.id, lessonIndex: newState.lessonIndex });
}
}
}
@@ -1852,7 +1869,7 @@ function stripHtml(html) {
* Update page meta tags based on current route for SEO
*/
function updatePageMeta(route) {
const defaultTitle = "Code Crispies - Learn HTML & CSS Interactively | Free Coding Practice";
const defaultTitle = "CODE CRISPIES - Learn HTML & CSS Interactively | Free Coding Practice";
const defaultDesc =
"Master HTML, CSS, and Tailwind through hands-on coding exercises. Free, open-source learning platform with instant feedback. No account required.";
@@ -1872,7 +1889,7 @@ function updatePageMeta(route) {
case RouteType.SECTION: {
const sectionNames = { css: "CSS", html: "HTML", tailwind: "Tailwind CSS" };
const sectionName = sectionNames[route.sectionId] || route.sectionId;
title = `${sectionName} Lessons - Code Crispies | Learn ${sectionName}`;
title = `${sectionName} Lessons - CODE CRISPIES | Learn ${sectionName}`;
description = `Learn ${sectionName} through interactive coding exercises. Hands-on practice with instant feedback.`;
break;
}
@@ -1881,7 +1898,7 @@ function updatePageMeta(route) {
const module = lessonEngine.modules.find((m) => m.id === route.moduleId);
const lesson = module?.lessons[route.lessonIndex];
if (module && lesson) {
title = `${lesson.title} - ${module.title} | Code Crispies`;
title = `${lesson.title} - ${module.title} | CODE CRISPIES`;
const lessonDesc = stripHtml(lesson.description || lesson.task);
description = lessonDesc.length > 155 ? lessonDesc.slice(0, 152) + "..." : lessonDesc || defaultDesc;
}
@@ -1897,7 +1914,7 @@ function updatePageMeta(route) {
html: "HTML Elements"
};
const refName = refNames[route.refId] || "Reference";
title = `${refName} Reference - Code Crispies`;
title = `${refName} Reference - CODE CRISPIES`;
description = `Quick reference guide for ${refName}. Syntax, examples, and common patterns for web development.`;
break;
}
@@ -1913,13 +1930,13 @@ function updatePageMeta(route) {
// Update Open Graph tags
const ogTitle = document.querySelector('meta[property="og:title"]');
const ogDesc = document.querySelector('meta[property="og:description"]');
if (ogTitle) ogTitle.setAttribute("content", title.replace(" | Code Crispies", "").replace(" - Code Crispies", ""));
if (ogTitle) ogTitle.setAttribute("content", title.replace(" | CODE CRISPIES", "").replace(" - CODE CRISPIES", ""));
if (ogDesc) ogDesc.setAttribute("content", description);
// Update Twitter tags
const twitterTitle = document.querySelector('meta[name="twitter:title"]');
const twitterDesc = document.querySelector('meta[name="twitter:description"]');
if (twitterTitle) twitterTitle.setAttribute("content", title.replace(" | Code Crispies", "").replace(" - Code Crispies", ""));
if (twitterTitle) twitterTitle.setAttribute("content", title.replace(" | CODE CRISPIES", "").replace(" - CODE CRISPIES", ""));
if (twitterDesc) twitterDesc.setAttribute("content", description);
}
@@ -1994,6 +2011,11 @@ function updateSectionColor(sectionId) {
} else {
document.body.removeAttribute("data-section");
}
// Update code editor theme for section
if (codeEditor) {
codeEditor.setSection(sectionId);
}
}
/**
@@ -2018,8 +2040,6 @@ function showLandingPage() {
* Render module links in the landing page footer, grouped by section
*/
function renderFooterLessonLinks() {
if (!elements.footerLessonLinks) return;
const modules = lessonEngine.modules || [];
const sectionGroups = { css: [], html: [] };
@@ -2042,7 +2062,16 @@ function renderFooterLessonLinks() {
html += "</div>";
});
elements.footerLessonLinks.innerHTML = html;
// Update all footer lesson links
if (elements.footerLessonLinks) {
elements.footerLessonLinks.innerHTML = html;
}
if (elements.refFooterLessonLinks) {
elements.refFooterLessonLinks.innerHTML = html;
}
if (elements.sectionFooterLessonLinks) {
elements.sectionFooterLessonLinks.innerHTML = html;
}
}
/**
@@ -2077,7 +2106,6 @@ function updateLandingProgress() {
function showSectionPage(sectionId) {
hideAllPages();
elements.sectionPage?.classList.remove("hidden");
window.scrollTo(0, 0);
// Update section color
updateSectionColor(sectionId);
@@ -2120,6 +2148,9 @@ function showSectionPage(sectionId) {
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
if (elements.sectionProgressFill) elements.sectionProgressFill.style.width = `${percent}%`;
if (elements.sectionProgressText) elements.sectionProgressText.textContent = `${completed} of ${total} lessons complete`;
// Scroll to top after content is rendered
requestAnimationFrame(() => window.scrollTo(0, 0));
}
/**

View File

@@ -15,7 +15,7 @@ export const sections = {
id: "html",
title: "HTML",
description: "Semantic markup and native elements",
color: "#d4637b",
color: "#c75b7a",
order: 2
},
tailwind: {

View File

@@ -1,11 +1,11 @@
/**
* Internationalization module for Code Crispies
* Internationalization module for CODE CRISPIES
*/
const translations = {
en: {
// Page
pageTitle: "Code Crispies - Learn HTML & CSS Interactively",
pageTitle: "CODE CRISPIES - Learn HTML & CSS Interactively",
skipLink: "Skip to main content",
// Header
@@ -48,9 +48,9 @@ const translations = {
// Help dialog
helpTitle: "Help",
aboutTitle: "About Code Crispies",
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!",
"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: "<strong>CSS</strong> - Write CSS rules to style elements",
modeTailwind: "<strong>Tailwind</strong> - Apply utility classes directly in HTML",
@@ -85,7 +85,7 @@ const translations = {
// Contact
contactTitle: "Contact & Links",
contactText: 'Code Crispies is developed by <a href="https://librete.ch" target="_blank">LibreTECH</a>',
contactText: 'CODE CRISPIES is developed by <a href="https://librete.ch" target="_blank">LibreTECH</a>',
// Reset dialog
resetDialogTitle: "Reset Progress",
@@ -121,7 +121,7 @@ const translations = {
landingHeroHighlight: "By Writing Real Code",
landingHeroSubtitle: "Master HTML, CSS, and Tailwind through hands-on exercises with instant feedback. Free and open source.",
landingCtaStart: "Start Learning NOW",
landingWhyTitle: "Why Code Crispies Works",
landingWhyTitle: "Why CODE CRISPIES Works",
landingBenefit1Title: "Learn by Doing",
landingBenefit1Text: "Write real code from lesson one. No videos to watch—just you, an editor, and instant feedback on every keystroke.",
landingBenefit2Title: "Practice at Your Pace",
@@ -145,17 +145,17 @@ const translations = {
footerPlayground: "Playground",
footerAbout: "About",
footerSupport: "Support",
footerSupportText: "Help keep Code Crispies free and open source.",
footerSupportText: "Help keep CODE CRISPIES free and open source.",
footerLicense: "Released into the public domain.",
// Help Dialog Support
supportTitle: "Support the Project",
supportText: "Help keep Code Crispies free and open source."
supportText: "Help keep CODE CRISPIES free and open source."
},
de: {
// Page
pageTitle: "Code Crispies - HTML & CSS interaktiv lernen",
pageTitle: "CODE CRISPIES - HTML & CSS interaktiv lernen",
skipLink: "Zum Hauptinhalt springen",
// Header
@@ -198,9 +198,9 @@ const translations = {
// Help dialog
helpTitle: "Hilfe",
aboutTitle: "Über Code Crispies",
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!",
"CODE CRISPIES ist eine kostenlose Open-Source-Plattform zum Erlernen von Webentwicklung durch praktische Übungen. Kein Konto erforderlich - einfach loslegen!",
learningModesTitle: "Lernmodi",
modeCss: "<strong>CSS</strong> - Schreibe CSS-Regeln zum Stylen von Elementen",
modeTailwind: "<strong>Tailwind</strong> - Wende Utility-Klassen direkt im HTML an",
@@ -236,7 +236,7 @@ const translations = {
// Contact
contactTitle: "Kontakt & Links",
contactText: 'Code Crispies wird von <a href="https://librete.ch" target="_blank">LibreTECH</a> entwickelt',
contactText: 'CODE CRISPIES wird von <a href="https://librete.ch" target="_blank">LibreTECH</a> entwickelt',
// Reset dialog
resetDialogTitle: "Fortschritt zurücksetzen",
@@ -272,7 +272,7 @@ const translations = {
landingHeroHighlight: "Selbstständig lernen",
landingHeroSubtitle: "Meistere HTML, CSS und Tailwind durch praktische Übungen mit sofortigem Feedback. Kostenlos und Open Source.",
landingCtaStart: "Jetzt starten",
landingWhyTitle: "Warum Code Crispies funktioniert",
landingWhyTitle: "Warum CODE CRISPIES funktioniert",
landingBenefit1Title: "Lernen durch Praxis",
landingBenefit1Text:
"Schreibe echten Code ab der ersten Lektion. Keine Videos nur du, ein Editor und sofortiges Feedback bei jedem Tastendruck.",
@@ -298,18 +298,18 @@ const translations = {
footerPlayground: "Playground",
footerAbout: "Über uns",
footerSupport: "Unterstützen",
footerSupportText: "Hilf mit, Code Crispies kostenlos und Open Source zu halten.",
footerSupportText: "Hilf mit, CODE CRISPIES kostenlos und Open Source zu halten.",
footerLicense: "Gemeinfrei (Public Domain).",
// Help Dialog Support
supportTitle: "Projekt unterstützen",
supportText: "Hilf mit, Code Crispies kostenlos und Open Source zu halten."
supportText: "Hilf mit, CODE CRISPIES kostenlos und Open Source zu halten."
},
// Polish
pl: {
// Page
pageTitle: "Code Crispies - Nauka HTML i CSS interaktywnie",
pageTitle: "CODE CRISPIES - Nauka HTML i CSS interaktywnie",
skipLink: "Przejdź do głównej treści",
// Header
@@ -352,9 +352,9 @@ const translations = {
// Help dialog
helpTitle: "Pomoc",
aboutTitle: "O Code Crispies",
aboutTitle: "O CODE CRISPIES",
aboutText:
"Code Crispies to darmowa platforma open-source do nauki tworzenia stron internetowych poprzez praktyczne ćwiczenia. Nie wymaga konta - po prostu zacznij kodować!",
"CODE CRISPIES to darmowa platforma open-source do nauki tworzenia stron internetowych poprzez praktyczne ćwiczenia. Nie wymaga konta - po prostu zacznij kodować!",
learningModesTitle: "Tryby nauki",
modeCss: "<strong>CSS</strong> - Pisz reguły CSS do stylizowania elementów",
modeTailwind: "<strong>Tailwind</strong> - Stosuj klasy utility bezpośrednio w HTML",
@@ -389,7 +389,7 @@ const translations = {
// Contact
contactTitle: "Kontakt i linki",
contactText: 'Code Crispies jest rozwijany przez <a href="https://librete.ch" target="_blank">LibreTECH</a>',
contactText: 'CODE CRISPIES jest rozwijany przez <a href="https://librete.ch" target="_blank">LibreTECH</a>',
// Reset dialog
resetDialogTitle: "Resetuj postęp",
@@ -425,7 +425,7 @@ const translations = {
landingHeroHighlight: "Pisząc prawdziwy kod",
landingHeroSubtitle: "Opanuj HTML, CSS i Tailwind poprzez praktyczne ćwiczenia z natychmiastową informacją zwrotną. Darmowe i open source.",
landingCtaStart: "Zacznij TERAZ",
landingWhyTitle: "Dlaczego Code Crispies działa",
landingWhyTitle: "Dlaczego CODE CRISPIES działa",
landingBenefit1Title: "Ucz się przez praktykę",
landingBenefit1Text:
"Pisz prawdziwy kod od pierwszej lekcji. Żadnych filmów tylko ty, edytor i natychmiastowa informacja zwrotna przy każdym naciśnięciu klawisza.",
@@ -451,18 +451,18 @@ const translations = {
footerPlayground: "Plac zabaw",
footerAbout: "O nas",
footerSupport: "Wsparcie",
footerSupportText: "Pomóż utrzymać Code Crispies darmowym i open source.",
footerSupportText: "Pomóż utrzymać CODE CRISPIES darmowym i open source.",
footerLicense: "Udostępnione jako domena publiczna.",
// Help Dialog Support
supportTitle: "Wesprzyj projekt",
supportText: "Pomóż utrzymać Code Crispies darmowym i open source."
supportText: "Pomóż utrzymać CODE CRISPIES darmowym i open source."
},
// Spanish
es: {
// Page
pageTitle: "Code Crispies - Aprende HTML y CSS de forma interactiva",
pageTitle: "CODE CRISPIES - Aprende HTML y CSS de forma interactiva",
skipLink: "Saltar al contenido principal",
// Header
@@ -505,9 +505,9 @@ const translations = {
// Help dialog
helpTitle: "Ayuda",
aboutTitle: "Acerca de Code Crispies",
aboutTitle: "Acerca de CODE CRISPIES",
aboutText:
"Code Crispies es una plataforma gratuita de código abierto para aprender desarrollo web a través de ejercicios prácticos. No se requiere cuenta, ¡solo empieza a programar!",
"CODE CRISPIES es una plataforma gratuita de código abierto para aprender desarrollo web a través de ejercicios prácticos. No se requiere cuenta, ¡solo empieza a programar!",
learningModesTitle: "Modos de aprendizaje",
modeCss: "<strong>CSS</strong> - Escribe reglas CSS para estilizar elementos",
modeTailwind: "<strong>Tailwind</strong> - Aplica clases de utilidad directamente en HTML",
@@ -543,7 +543,7 @@ const translations = {
// Contact
contactTitle: "Contacto y enlaces",
contactText: 'Code Crispies es desarrollado por <a href="https://librete.ch" target="_blank">LibreTECH</a>',
contactText: 'CODE CRISPIES es desarrollado por <a href="https://librete.ch" target="_blank">LibreTECH</a>',
// Reset dialog
resetDialogTitle: "Reiniciar progreso",
@@ -580,7 +580,7 @@ const translations = {
landingHeroSubtitle:
"Domina HTML, CSS y Tailwind a través de ejercicios prácticos con retroalimentación instantánea. Gratis y de código abierto.",
landingCtaStart: "Empieza AHORA",
landingWhyTitle: "Por qué funciona Code Crispies",
landingWhyTitle: "Por qué funciona CODE CRISPIES",
landingBenefit1Title: "Aprende haciendo",
landingBenefit1Text:
"Escribe código real desde la primera lección. Sin videos que ver—solo tú, un editor y retroalimentación instantánea en cada tecla.",
@@ -606,18 +606,18 @@ const translations = {
footerPlayground: "Zona de pruebas",
footerAbout: "Acerca de",
footerSupport: "Apoyar",
footerSupportText: "Ayuda a mantener Code Crispies gratis y de código abierto.",
footerSupportText: "Ayuda a mantener CODE CRISPIES gratis y de código abierto.",
footerLicense: "Liberado al dominio público.",
// Help Dialog Support
supportTitle: "Apoyar el proyecto",
supportText: "Ayuda a mantener Code Crispies gratis y de código abierto."
supportText: "Ayuda a mantener CODE CRISPIES gratis y de código abierto."
},
// Arabic
ar: {
// Page
pageTitle: "Code Crispies - تعلم HTML و CSS بشكل تفاعلي",
pageTitle: "CODE CRISPIES - تعلم HTML و CSS بشكل تفاعلي",
skipLink: "انتقل إلى المحتوى الرئيسي",
// Header
@@ -660,8 +660,8 @@ const translations = {
// Help dialog
helpTitle: "مساعدة",
aboutTitle: "عن Code Crispies",
aboutText: "Code Crispies هي منصة مجانية مفتوحة المصدر لتعلم تطوير الويب من خلال تمارين عملية. لا يلزم حساب - فقط ابدأ البرمجة!",
aboutTitle: "عن CODE CRISPIES",
aboutText: "CODE CRISPIES هي منصة مجانية مفتوحة المصدر لتعلم تطوير الويب من خلال تمارين عملية. لا يلزم حساب - فقط ابدأ البرمجة!",
learningModesTitle: "أوضاع التعلم",
modeCss: "<strong>CSS</strong> - اكتب قواعد CSS لتنسيق العناصر",
modeTailwind: "<strong>Tailwind</strong> - طبق فئات الأدوات مباشرة في HTML",
@@ -696,7 +696,7 @@ const translations = {
// Contact
contactTitle: "التواصل والروابط",
contactText: 'Code Crispies تم تطويره بواسطة <a href="https://librete.ch" target="_blank">LibreTECH</a>',
contactText: 'CODE CRISPIES تم تطويره بواسطة <a href="https://librete.ch" target="_blank">LibreTECH</a>',
// Reset dialog
resetDialogTitle: "إعادة تعيين التقدم",
@@ -732,7 +732,7 @@ const translations = {
landingHeroHighlight: "بكتابة كود حقيقي",
landingHeroSubtitle: "أتقن HTML و CSS و Tailwind من خلال تمارين عملية مع ملاحظات فورية. مجاني ومفتوح المصدر.",
landingCtaStart: "ابدأ الآن",
landingWhyTitle: "لماذا Code Crispies فعال",
landingWhyTitle: "لماذا CODE CRISPIES فعال",
landingBenefit1Title: "تعلم بالممارسة",
landingBenefit1Text: "اكتب كودًا حقيقيًا من الدرس الأول. لا فيديوهات للمشاهدة—فقط أنت ومحرر وملاحظات فورية مع كل ضغطة مفتاح.",
landingBenefit2Title: "بسرعتك الخاصة",
@@ -756,18 +756,18 @@ const translations = {
footerPlayground: "ساحة التجربة",
footerAbout: "حول",
footerSupport: "الدعم",
footerSupportText: "ساعد في إبقاء Code Crispies مجانيًا ومفتوح المصدر.",
footerSupportText: "ساعد في إبقاء CODE CRISPIES مجانيًا ومفتوح المصدر.",
footerLicense: "مُطلق للملكية العامة.",
// Help Dialog Support
supportTitle: "ادعم المشروع",
supportText: "ساعد في إبقاء Code Crispies مجانيًا ومفتوح المصدر."
supportText: "ساعد في إبقاء CODE CRISPIES مجانيًا ومفتوح المصدر."
},
// Ukrainian
uk: {
// Page
pageTitle: "Code Crispies - Вивчай HTML та CSS інтерактивно",
pageTitle: "CODE CRISPIES - Вивчай HTML та CSS інтерактивно",
skipLink: "Перейти до основного вмісту",
// Header
@@ -810,9 +810,9 @@ const translations = {
// Help dialog
helpTitle: "Допомога",
aboutTitle: "Про Code Crispies",
aboutTitle: "Про CODE CRISPIES",
aboutText:
"Code Crispies — це безкоштовна платформа з відкритим кодом для вивчення веб-розробки через практичні вправи. Обліковий запис не потрібен — просто починайте кодувати!",
"CODE CRISPIES — це безкоштовна платформа з відкритим кодом для вивчення веб-розробки через практичні вправи. Обліковий запис не потрібен — просто починайте кодувати!",
learningModesTitle: "Режими навчання",
modeCss: "<strong>CSS</strong> - Пишіть правила CSS для стилізації елементів",
modeTailwind: "<strong>Tailwind</strong> - Застосовуйте утилітарні класи безпосередньо в HTML",
@@ -847,7 +847,7 @@ const translations = {
// Contact
contactTitle: "Контакти та посилання",
contactText: 'Code Crispies розроблено <a href="https://librete.ch" target="_blank">LibreTECH</a>',
contactText: 'CODE CRISPIES розроблено <a href="https://librete.ch" target="_blank">LibreTECH</a>',
// Reset dialog
resetDialogTitle: "Скинути прогрес",
@@ -883,7 +883,7 @@ const translations = {
landingHeroHighlight: "Пишучи справжній код",
landingHeroSubtitle: "Опануй HTML, CSS та Tailwind через практичні вправи з миттєвим зворотним зв'язком. Безкоштовно та з відкритим кодом.",
landingCtaStart: "Почни ЗАРАЗ",
landingWhyTitle: "Чому Code Crispies працює",
landingWhyTitle: "Чому CODE CRISPIES працює",
landingBenefit1Title: "Вчись на практиці",
landingBenefit1Text:
"Пиши справжній код з першого уроку. Жодних відео—тільки ти, редактор і миттєвий зворотний зв'язок при кожному натисканні клавіші.",
@@ -908,12 +908,12 @@ const translations = {
footerPlayground: "Пісочниця",
footerAbout: "Про нас",
footerSupport: "Підтримка",
footerSupportText: "Допоможи зберегти Code Crispies безкоштовним та з відкритим кодом.",
footerSupportText: "Допоможи зберегти CODE CRISPIES безкоштовним та з відкритим кодом.",
footerLicense: "Передано у суспільне надбання.",
// Help Dialog Support
supportTitle: "Підтримати проєкт",
supportText: "Допоможи зберегти Code Crispies безкоштовним та з відкритим кодом."
supportText: "Допоможи зберегти CODE CRISPIES безкоштовним та з відкритим кодом."
}
};

View File

@@ -60,8 +60,8 @@ const crispyTheme = EditorView.theme(
{ dark: true }
);
// Syntax highlighting with purple accent
const crispyHighlight = HighlightStyle.define([
// Default syntax highlighting (blue accent)
const defaultHighlight = HighlightStyle.define([
{ tag: tags.keyword, color: "#c9a6eb" },
{ tag: tags.operator, color: "#cdd6f4" },
{ tag: tags.variableName, color: "#89b4fa" },
@@ -83,8 +83,42 @@ const crispyHighlight = HighlightStyle.define([
{ tag: tags.color, color: "#f9e2af" }
]);
// Combined theme export
export const crispyEditorTheme = [crispyTheme, syntaxHighlighting(crispyHighlight)];
// CSS section highlighting (purple selectors)
const cssHighlight = HighlightStyle.define([
{ tag: tags.keyword, color: "#c9a6eb" },
{ tag: tags.operator, color: "#cdd6f4" },
{ tag: tags.variableName, color: "#c9a6eb" },
{ tag: tags.propertyName, color: "#89b4fa" },
{ tag: tags.attributeName, color: "#89b4fa" },
{ tag: tags.className, color: "#c9a6eb" },
{ tag: tags.tagName, color: "#c9a6eb" },
{ tag: tags.string, color: "#a6e3a1" },
{ tag: tags.number, color: "#fab387" },
{ tag: tags.bool, color: "#fab387" },
{ tag: tags.null, color: "#fab387" },
{ tag: tags.comment, color: "#6c7086", fontStyle: "italic" },
{ tag: tags.bracket, color: "#cdd6f4" },
{ tag: tags.punctuation, color: "#cdd6f4" },
{ tag: tags.definition(tags.variableName), color: "#c9a6eb" },
{ tag: tags.function(tags.variableName), color: "#89b4fa" },
{ tag: tags.atom, color: "#c9a6eb" },
{ tag: tags.unit, color: "#a6e3a1" },
{ tag: tags.color, color: "#f9e2af" }
]);
// Get highlight style based on section
function getHighlightForSection(section) {
if (section === "css") return cssHighlight;
return defaultHighlight;
}
// Get theme with section-specific highlighting
export function getEditorTheme(section) {
return [crispyTheme, syntaxHighlighting(getHighlightForSection(section))];
}
// Default combined theme export (for backwards compatibility)
export const crispyEditorTheme = [crispyTheme, syntaxHighlighting(defaultHighlight)];
// Custom overrides for editor styling
const editorTheme = EditorView.theme(
@@ -110,6 +144,7 @@ export class CodeEditor {
this.options = options;
this.view = null;
this.mode = options.mode || "css";
this.section = options.section || null;
this.onChange = options.onChange || (() => {});
}
@@ -126,7 +161,7 @@ export class CodeEditor {
// Build extensions array
const extensions = [
langExtension,
crispyEditorTheme,
getEditorTheme(this.section),
editorTheme,
// History for undo/redo
history(),
@@ -215,6 +250,17 @@ export class CodeEditor {
this.init(currentValue);
}
/**
* Set section for theme (css, html, tailwind)
*/
setSection(section) {
if (this.section === section) return;
this.section = section;
const currentValue = this.getValue();
this.init(currentValue);
}
/**
* Focus the editor
*/

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags -->
<title>Code Crispies - Learn HTML & CSS Interactively | Free Coding Practice</title>
<title>CODE CRISPIES - Learn HTML & CSS Interactively | Free Coding Practice</title>
<meta
name="description"
content="Master HTML, CSS, and Tailwind through hands-on coding exercises. Free, open-source learning platform with instant feedback. No account required."
@@ -19,14 +19,14 @@
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://codecrispi.es/" />
<meta property="og:title" content="Code Crispies - Learn HTML & CSS Interactively" />
<meta property="og:title" content="CODE CRISPIES - Learn HTML & CSS Interactively" />
<meta property="og:description" content="Master HTML, CSS, and Tailwind through hands-on coding exercises. Free and open source." />
<meta property="og:image" content="https://codecrispi.es/og-image.png" />
<meta property="og:site_name" content="Code Crispies" />
<meta property="og:site_name" content="CODE CRISPIES" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Code Crispies - Learn HTML & CSS Interactively" />
<meta name="twitter:title" content="CODE CRISPIES - Learn HTML & CSS Interactively" />
<meta name="twitter:description" content="Master HTML, CSS, and Tailwind through hands-on coding exercises." />
<meta name="twitter:image" content="https://codecrispi.es/og-image.png" />
@@ -35,7 +35,7 @@
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Code Crispies",
"name": "CODE CRISPIES",
"description": "Interactive platform for learning HTML, CSS, and Tailwind through hands-on coding exercises",
"url": "https://codecrispi.es/",
"applicationCategory": "EducationalApplication",
@@ -100,7 +100,7 @@
</section>
<section class="why-it-works">
<h2 data-i18n="landingWhyTitle">Why Code Crispies Works</h2>
<h2 data-i18n="landingWhyTitle">Why CODE CRISPIES Works</h2>
<div class="benefits-grid">
<article class="benefit-card">
<svg class="benefit-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -198,7 +198,7 @@
</section>
<section class="footer-section footer-support">
<h4 data-i18n="footerSupport">Support</h4>
<p data-i18n="footerSupportText">Help keep Code Crispies free and open source.</p>
<p data-i18n="footerSupportText">Help keep CODE CRISPIES free and open source.</p>
<script src="https://liberapay.com/libretech/widgets/button.js"></script>
<noscript><a href="https://liberapay.com/libretech/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a></noscript>
</section>
@@ -227,6 +227,38 @@
<!-- Educational content with integrated module links -->
<div class="section-intro" id="section-intro"></div>
</article>
<footer class="section-footer landing-footer">
<div class="footer-grid">
<section class="footer-section footer-modules">
<div id="section-footer-lesson-links" class="footer-links"></div>
</section>
<section class="footer-section">
<h4 data-i18n="footerResources">Resources</h4>
<ul class="footer-links">
<li><a href="#reference/css">CSS Reference</a></li>
<li><a href="#reference/html">HTML Reference</a></li>
<li><a href="#playground/0" data-i18n="footerPlayground">Playground</a></li>
</ul>
</section>
<section class="footer-section">
<h4 data-i18n="footerAbout">About</h4>
<ul class="footer-links">
<li><a href="https://librete.ch" target="_blank">LibreTECH</a></li>
<li><a href="https://git.librete.ch/libretech/code-crispies" target="_blank">Source Code</a></li>
<li><a href="https://github.com/nextlevelshit/code-crispies" target="_blank">GitHub</a></li>
</ul>
</section>
<section class="footer-section footer-support">
<h4 data-i18n="footerSupport">Support</h4>
<p data-i18n="footerSupportText">Help keep CODE CRISPIES free and open source.</p>
<script src="https://liberapay.com/libretech/widgets/button.js"></script>
<noscript><a href="https://liberapay.com/libretech/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a></noscript>
</section>
</div>
<div class="footer-bottom">
<p>&copy; 2025 <a href="https://librete.ch">LibreTECH</a>. <span data-i18n="footerLicense">Open source under MIT License.</span></p>
</div>
</footer>
</div>
<!-- Reference/Cheatsheet Pages -->
@@ -243,8 +275,37 @@
<!-- Reference content injected by JS -->
</div>
</article>
<footer class="reference-footer">
<p>&copy; 2025 <a href="https://librete.ch">LibreTECH</a>. <span data-i18n="footerLicense">Released into public domain.</span></p>
<footer class="reference-footer landing-footer">
<div class="footer-grid">
<section class="footer-section footer-modules">
<div id="ref-footer-lesson-links" class="footer-links"></div>
</section>
<section class="footer-section">
<h4 data-i18n="footerResources">Resources</h4>
<ul class="footer-links">
<li><a href="#reference/css">CSS Reference</a></li>
<li><a href="#reference/html">HTML Reference</a></li>
<li><a href="#playground/0" data-i18n="footerPlayground">Playground</a></li>
</ul>
</section>
<section class="footer-section">
<h4 data-i18n="footerAbout">About</h4>
<ul class="footer-links">
<li><a href="https://librete.ch" target="_blank">LibreTECH</a></li>
<li><a href="https://git.librete.ch/libretech/code-crispies" target="_blank">Source Code</a></li>
<li><a href="https://github.com/nextlevelshit/code-crispies" target="_blank">GitHub</a></li>
</ul>
</section>
<section class="footer-section footer-support">
<h4 data-i18n="footerSupport">Support</h4>
<p data-i18n="footerSupportText">Help keep CODE CRISPIES free and open source.</p>
<script src="https://liberapay.com/libretech/widgets/button.js"></script>
<noscript><a href="https://liberapay.com/libretech/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a></noscript>
</section>
</div>
<div class="footer-bottom">
<p>&copy; 2025 <a href="https://librete.ch">LibreTECH</a>. <span data-i18n="footerLicense">Open source under MIT License.</span></p>
</div>
</footer>
</div>
@@ -389,9 +450,9 @@
<button id="help-dialog-close" class="dialog-close" aria-label="Close">&times;</button>
</div>
<div class="dialog-content">
<h4 data-i18n="aboutTitle">About Code Crispies</h4>
<h4 data-i18n="aboutTitle">About CODE CRISPIES</h4>
<p data-i18n="aboutText">
Code Crispies is a free, open-source platform for learning web development through hands-on exercises. No account required -
CODE CRISPIES is a free, open-source platform for learning web development through hands-on exercises. No account required -
just start coding!
</p>
@@ -455,7 +516,7 @@
</div>
<h4 data-i18n="contactTitle">Contact & Links</h4>
<p data-i18n-html="contactText">Code Crispies is developed by <a href="https://librete.ch" target="_blank">LibreTECH</a></p>
<p data-i18n-html="contactText">CODE CRISPIES is developed by <a href="https://librete.ch" target="_blank">LibreTECH</a></p>
<ul>
<li><a href="https://git.librete.ch/libretech/code-crispies" target="_blank">Gitea</a> Self-hosted source repository</li>
<li><a href="https://github.com/nextlevelshit/code-crispies" target="_blank">GitHub</a> Public mirror</li>
@@ -463,7 +524,7 @@
</ul>
<h4 data-i18n="supportTitle">Support the Project</h4>
<p data-i18n="supportText">Help keep Code Crispies free and open source.</p>
<p data-i18n="supportText">Help keep CODE CRISPIES free and open source.</p>
<div class="help-support" onclick="typeof umami !== 'undefined' && umami.track('support_click', {location: 'help'})">
<script src="https://liberapay.com/libretech/widgets/button.js"></script>
<noscript><a href="https://liberapay.com/libretech/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a></noscript>

View File

@@ -252,11 +252,11 @@ kbd {
}
.logo h1 .code-text {
color: var(--primary-color);
color: #8b6bc4;
}
.logo h1 .crispies-text {
background: var(--primary-color);
background: #8b6bc4;
color: white;
padding: 0.15rem 0.35rem;
border-radius: 4px;
@@ -264,6 +264,31 @@ kbd {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
/* Logo section color coding */
[data-section="css"] .logo h1 .code-text {
color: #8b6bc4;
}
[data-section="css"] .logo h1 .crispies-text {
background: #8b6bc4;
}
[data-section="html"] .logo h1 .code-text {
color: #c75b7a;
}
[data-section="html"] .logo h1 .crispies-text {
background: #c75b7a;
}
[data-section="tailwind"] .logo h1 .code-text {
color: #26a69a;
}
[data-section="tailwind"] .logo h1 .crispies-text {
background: #26a69a;
}
.help-toggle {
width: 28px;
height: 28px;
@@ -1954,7 +1979,9 @@ input:checked + .toggle-slider::before {
color: var(--primary-color);
}
#footer-lesson-links {
#footer-lesson-links,
#ref-footer-lesson-links,
#section-footer-lesson-links {
display: flex;
gap: 2rem;
}
@@ -2856,12 +2883,12 @@ input:checked + .toggle-slider::before {
--section-color-rgb: 139, 107, 196;
}
/* HTML Section - Balanced Rose */
/* HTML Section - Raspberry */
[data-section="html"] {
--section-color: #d4637b;
--section-color-light: #e08899;
--section-color-dark: #b84d63;
--section-color-rgb: 212, 99, 123;
--section-color: #c75b7a;
--section-color-light: #d97a94;
--section-color-dark: #a84862;
--section-color-rgb: 199, 91, 122;
}
/* Tailwind Section - Balanced Teal */
@@ -2878,7 +2905,7 @@ input:checked + .toggle-slider::before {
}
.nav-link[data-section="html"] {
color: #d4637b;
color: #c75b7a;
}
.nav-link[data-section="tailwind"] {
@@ -2893,8 +2920,8 @@ input:checked + .toggle-slider::before {
.nav-link[data-section="html"]:hover,
.nav-link[data-section="html"].active {
background: rgba(212, 99, 123, 0.1);
color: #b84d63;
background: rgba(199, 91, 122, 0.1);
color: #a84862;
}
.nav-link[data-section="tailwind"]:hover,
@@ -2903,17 +2930,45 @@ input:checked + .toggle-slider::before {
color: #00897b;
}
/* Logo color coding based on section */
[data-section="css"] .code-text {
color: #8b6bc4;
/* Hint section colors */
body[data-section="css"] .hint {
background: rgba(139, 107, 196, 0.3);
border-left-color: #a98cd6;
}
[data-section="html"] .code-text {
color: #d4637b;
body[data-section="css"] .hint-progress {
background: #8b6bc4;
}
[data-section="tailwind"] .code-text {
color: #26a69a;
body[data-section="html"] .hint {
background: rgba(199, 91, 122, 0.3);
border-left-color: #d97a94;
}
body[data-section="html"] .hint-progress {
background: #c75b7a;
}
body[data-section="tailwind"] .hint {
background: rgba(38, 166, 154, 0.3);
border-left-color: #4db6ac;
}
body[data-section="tailwind"] .hint-progress {
background: #26a69a;
}
/* RTL hint border */
[dir="rtl"] body[data-section="css"] .hint {
border-right-color: #a98cd6;
}
[dir="rtl"] body[data-section="html"] .hint {
border-right-color: #d97a94;
}
[dir="rtl"] body[data-section="tailwind"] .hint {
border-right-color: #4db6ac;
}
/* Reference nav link colors */
@@ -2937,13 +2992,13 @@ input:checked + .toggle-slider::before {
}
.ref-nav-link[data-ref="html"] {
color: #d4637b;
color: #c75b7a;
}
.ref-nav-link[data-ref="html"]:hover,
.ref-nav-link[data-ref="html"].active {
background: rgba(212, 99, 123, 0.15);
color: #b84d63;
background: rgba(199, 91, 122, 0.15);
color: #a84862;
}
/* CodeMirror section color overrides */
@@ -2966,21 +3021,21 @@ body[data-section="css"] .cm-editor .cm-activeLine {
}
body[data-section="html"] .cm-editor .cm-content {
caret-color: #d4637b !important;
caret-color: #c75b7a !important;
}
body[data-section="html"] .cm-editor .cm-cursor,
body[data-section="html"] .cm-editor .cm-dropCursor {
border-left-color: #d4637b !important;
border-left-color: #c75b7a !important;
}
body[data-section="html"] .cm-editor .cm-selectionBackground,
body[data-section="html"] .cm-editor .cm-content ::selection {
background-color: rgba(212, 99, 123, 0.25) !important;
background-color: rgba(199, 91, 122, 0.25) !important;
}
body[data-section="html"] .cm-editor .cm-activeLine {
background-color: rgba(212, 99, 123, 0.08) !important;
background-color: rgba(199, 91, 122, 0.08) !important;
}
body[data-section="tailwind"] .cm-editor .cm-content {
@@ -3012,12 +3067,12 @@ body[data-section="css"] .module-pill .level-indicator {
}
body[data-section="html"] .module-pill {
background: rgba(212, 99, 123, 0.1);
color: #d4637b;
background: rgba(199, 91, 122, 0.1);
color: #c75b7a;
}
body[data-section="html"] .module-pill .level-indicator {
color: #b84d63;
color: #a84862;
}
body[data-section="tailwind"] .module-pill {
@@ -3035,7 +3090,7 @@ body[data-section="css"] .code-block {
}
body[data-section="html"] .code-block {
border-color: rgba(212, 99, 123, 0.4);
border-color: rgba(199, 91, 122, 0.4);
}
body[data-section="tailwind"] .code-block {
@@ -3061,7 +3116,7 @@ body[data-section="tailwind"] .code-block .cm-editor .cm-line {
}
[data-section="html"] .task-instruction {
background: rgba(212, 99, 123, 0.92);
background: rgba(199, 91, 122, 0.92);
}
[data-section="tailwind"] .task-instruction {
@@ -3074,7 +3129,7 @@ body[data-section="css"] .section-progress-bar .progress-fill {
}
body[data-section="html"] .section-progress-bar .progress-fill {
background: #d4637b;
background: #c75b7a;
}
body[data-section="tailwind"] .section-progress-bar .progress-fill {
@@ -3087,23 +3142,28 @@ body[data-section="tailwind"] .section-progress-bar .progress-fill {
}
[data-section="html"] .section-hero h1 {
color: #d4637b;
color: #c75b7a;
}
[data-section="tailwind"] .section-hero h1 {
color: #26a69a;
}
/* Reference footer */
.reference-footer {
text-align: center;
padding: 2rem 1rem;
margin-top: 3rem;
border-top: 1px solid var(--border-color);
color: var(--light-text);
font-size: 0.9rem;
/* Section and Reference footer - override landing-footer styles */
.section-footer.landing-footer,
.reference-footer.landing-footer {
max-width: 900px;
margin: 6rem auto 0;
padding: 0 var(--spacing-lg) var(--spacing-lg);
border-top: none;
}
.reference-footer a {
.section-footer .footer-links a,
.reference-footer .footer-links a {
color: var(--light-text);
}
.section-footer .footer-links a:hover,
.reference-footer .footer-links a:hover {
color: var(--section-color, var(--primary-color));
}