feat: add landing footer with donation support, change license to Unlicense

- Add extended landing footer with module links grouped by section
- Integrate Liberapay donation widget with Umami tracking
- Add support section to help dialog and goodbye lesson
- Change license from MIT to Unlicense (public domain)
- Disable Tailwind section (not yet activated)
- Update German CTA copy
- Update all 6 language translations for license text

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
2026-01-16 03:33:41 +01:00
parent f1496e7232
commit 1368f1c079
7 changed files with 323 additions and 14 deletions

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View File

@@ -1,16 +1,16 @@
![Code Crispies Logo](./public/android-chrome-192x192.png)
# Code Crispies
An interactive platform for learning CSS and Tailwind CSS through practical challenges.
An interactive platform for learning HTML, CSS, and Tailwind CSS through practical challenges.
## 📚 Overview
Code Crispies is a web-based learning platform designed to help users master CSS and Tailwind CSS through hands-on exercises. The application presents a series of progressive challenges organized into themed modules, allowing learners to build their skills step by step while receiving immediate feedback.
Code Crispies is a web-based learning platform designed to help users master HTML, CSS, and Tailwind CSS through hands-on exercises. The application presents a series of progressive challenges organized into themed modules, allowing learners to build their skills step by step while receiving immediate feedback.
### Key Features
- **Interactive Lessons**: Learn CSS and Tailwind concepts through practical, hands-on challenges
- **Dual Mode Support**: Switch between CSS and Tailwind CSS learning modes
- **Interactive Lessons**: Learn HTML, CSS, and Tailwind concepts through practical, hands-on challenges
- **Multiple Learning Paths**: CSS fundamentals, HTML semantics, and Tailwind CSS utilities
- **Progressive Difficulty**: Modules are structured to build skills gradually from basic to advanced
- **Real-Time Feedback**: Get immediate validation on your code solutions with comprehensive feedback
- **Progress Tracking**: Your learning progress is automatically saved in the browser
@@ -256,4 +256,4 @@ When adding new lessons:
## 📄 License
Copyright (c) 2026 Michael Czechowski. Licensed under the [./LICENSE](WTFPL).
This project is released into the public domain under the [Unlicense](./LICENSE). You are free to copy, modify, publish, use, compile, sell, or distribute this software for any purpose.

View File

@@ -10,7 +10,7 @@
{
"id": "next-steps",
"title": "Keep Going!",
"description": "<strong>Great progress!</strong> You're building real web development skills.<br><br><strong>Continue learning:</strong><br>• <a href=\"https://developer.mozilla.org\" target=\"_blank\">MDN Web Docs</a> - The definitive reference<br>• <a href=\"https://css-tricks.com\" target=\"_blank\">CSS-Tricks</a> - Practical techniques<br><br><strong>Practice ideas:</strong><br>• Build your portfolio site<br>• Recreate a website you like<br>• Try the <a href=\"#playground/0\">Playground</a> to experiment freely<br><br><strong>Contribute:</strong> Code Crispies is <a href=\"https://git.librete.ch/libretech/code-crispies\" target=\"_blank\">open source</a>. Add lessons, fix bugs, or translate!",
"description": "<strong>Great progress!</strong> You're building real web development skills.<br><br><strong>Continue learning:</strong><br>• <a href=\"https://developer.mozilla.org\" target=\"_blank\">MDN Web Docs</a> - The definitive reference<br>• <a href=\"https://css-tricks.com\" target=\"_blank\">CSS-Tricks</a> - Practical techniques<br><br><strong>Practice ideas:</strong><br>• Build your portfolio site<br>• Recreate a website you like<br>• Try the <a href=\"#playground/0\">Playground</a> to experiment freely<br><br><strong>Contribute:</strong> Code Crispies is <a href=\"https://git.librete.ch/libretech/code-crispies\" target=\"_blank\">open source</a>. Add lessons, fix bugs, or translate!<br><br><strong>Support:</strong> Help keep it free and open source!<br><a href=\"https://liberapay.com/libretech/donate\" target=\"_blank\" onclick=\"typeof umami !== 'undefined' && umami.track('support_click', {location: 'goodbye'})\"><img alt=\"Donate using Liberapay\" src=\"https://liberapay.com/assets/widgets/donate.svg\" style=\"margin-top: 8px;\"></a>",
"task": "Type <code>Thank you!</code>",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 1.5rem; color: #6366f1; }",

View File

@@ -155,6 +155,7 @@ const elements = {
sidebarBackdrop: document.getElementById("sidebar-backdrop"),
closeSidebar: document.getElementById("close-sidebar"),
moduleList: document.getElementById("module-list"),
footerLessonLinks: document.getElementById("footer-lesson-links"),
progressFill: document.getElementById("progress-fill"),
progressText: document.getElementById("progress-text"),
resetBtn: document.getElementById("reset-btn"),
@@ -1991,6 +1992,40 @@ function showLandingPage() {
// Update section progress on landing page
updateLandingProgress();
// Render footer lesson links
renderFooterLessonLinks();
}
/**
* 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: [] };
modules.forEach((module) => {
if (module.excludeFromProgress) return;
const sectionId = getModuleSection(module);
if (sectionId && sectionGroups[sectionId]) {
sectionGroups[sectionId].push(module);
}
});
let html = "";
Object.entries(sectionGroups).forEach(([sectionId, sectionModules]) => {
if (sectionModules.length === 0) return;
const sectionName = sectionId.toUpperCase();
html += `<div class="footer-section-group"><strong><a href="#${sectionId}">${sectionName}</a></strong>`;
sectionModules.forEach((module) => {
html += `<a href="#${module.id}/0">${module.title}</a>`;
});
html += "</div>";
});
elements.footerLessonLinks.innerHTML = html;
}
/**
@@ -2387,6 +2422,8 @@ function init() {
track("landing_cta", { href: target.getAttribute("href") });
} else if (target.classList.contains("section-card")) {
track("landing_section", { section: target.dataset.section });
} else if (target.closest(".footer-support")) {
track("support_click", { location: "landing" });
}
});
}

View File

@@ -134,7 +134,20 @@ const translations = {
landingTailwindDesc: "Utility-first CSS framework",
landingCtaTitle: "Start Learning Today",
landingCtaSub: "Free and open source. No account required. Progress saved locally.",
landingCtaButton: "Begin Your Journey"
landingCtaButton: "Begin Your Journey",
// Footer
footerModules: "Modules",
footerResources: "Resources",
footerPlayground: "Playground",
footerAbout: "About",
footerSupport: "Support",
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."
},
de: {
@@ -271,7 +284,20 @@ const translations = {
landingTailwindDesc: "Utility-first CSS-Framework",
landingCtaTitle: "Heute noch anfangen",
landingCtaSub: "Kostenlos und Open Source. Kein Konto erforderlich. Fortschritt wird lokal gespeichert.",
landingCtaButton: "Starte deine Reise"
landingCtaButton: "Jetzt erste Schritte machen",
// Footer
footerModules: "Module",
footerResources: "Ressourcen",
footerPlayground: "Playground",
footerAbout: "Über uns",
footerSupport: "Unterstützen",
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."
},
// Polish
@@ -408,7 +434,20 @@ const translations = {
landingTailwindDesc: "Framework CSS oparty na klasach utility",
landingCtaTitle: "Zacznij naukę już dziś",
landingCtaSub: "Darmowe i open source. Bez konta. Postęp zapisywany lokalnie.",
landingCtaButton: "Rozpocznij swoją podróż"
landingCtaButton: "Rozpocznij swoją podróż",
// Footer
footerModules: "Moduły",
footerResources: "Zasoby",
footerPlayground: "Plac zabaw",
footerAbout: "O nas",
footerSupport: "Wsparcie",
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."
},
// Spanish
@@ -547,7 +586,20 @@ const translations = {
landingTailwindDesc: "Framework CSS basado en utilidades",
landingCtaTitle: "Empieza a aprender hoy",
landingCtaSub: "Gratis y de código abierto. Sin cuenta requerida. Progreso guardado localmente.",
landingCtaButton: "Comienza tu viaje"
landingCtaButton: "Comienza tu viaje",
// Footer
footerModules: "Módulos",
footerResources: "Recursos",
footerPlayground: "Zona de pruebas",
footerAbout: "Acerca de",
footerSupport: "Apoyar",
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."
},
// Arabic
@@ -681,7 +733,20 @@ const translations = {
landingTailwindDesc: "إطار CSS قائم على الأدوات",
landingCtaTitle: "ابدأ التعلم اليوم",
landingCtaSub: "مجاني ومفتوح المصدر. لا حاجة لحساب. يُحفظ التقدم محليًا.",
landingCtaButton: "ابدأ رحلتك"
landingCtaButton: "ابدأ رحلتك",
// Footer
footerModules: "الوحدات",
footerResources: "الموارد",
footerPlayground: "ساحة التجربة",
footerAbout: "حول",
footerSupport: "الدعم",
footerSupportText: "ساعد في إبقاء Code Crispies مجانيًا ومفتوح المصدر.",
footerLicense: "مُطلق للملكية العامة.",
// Help Dialog Support
supportTitle: "ادعم المشروع",
supportText: "ساعد في إبقاء Code Crispies مجانيًا ومفتوح المصدر."
},
// Ukrainian
@@ -817,7 +882,20 @@ const translations = {
landingTailwindDesc: "CSS-фреймворк на основі утиліт",
landingCtaTitle: "Почни вчитися сьогодні",
landingCtaSub: "Безкоштовно та з відкритим кодом. Без реєстрації. Прогрес зберігається локально.",
landingCtaButton: "Розпочни свою подорож"
landingCtaButton: "Розпочни свою подорож",
// Footer
footerModules: "Модулі",
footerResources: "Ресурси",
footerPlayground: "Пісочниця",
footerAbout: "Про нас",
footerSupport: "Підтримка",
footerSupportText: "Допоможи зберегти Code Crispies безкоштовним та з відкритим кодом.",
footerLicense: "Передано у суспільне надбання.",
// Help Dialog Support
supportTitle: "Підтримати проєкт",
supportText: "Допоможи зберегти Code Crispies безкоштовним та з відкритим кодом."
}
};

View File

@@ -74,7 +74,8 @@
<nav class="main-nav" id="main-nav" aria-label="Main sections">
<a href="#css" class="nav-link" data-section="css">CSS</a>
<a href="#html" class="nav-link" data-section="html">HTML</a>
<a href="#tailwind" class="nav-link" data-section="tailwind">Tailwind</a>
<!-- Tailwind disabled until lessons are ready -->
<!-- <a href="#tailwind" class="nav-link" data-section="tailwind">Tailwind</a> -->
<a href="#reference/css" class="nav-link nav-link-ref" data-section="reference">Reference</a>
</nav>
<button id="help-btn" class="help-toggle" data-i18n-aria-label="help" aria-label="Help">?</button>
@@ -150,12 +151,15 @@
<p data-i18n="landingHtmlDesc">Semantic markup and native elements</p>
<span class="section-card-progress" id="html-progress"></span>
</a>
<!-- Tailwind disabled until lessons are ready -->
<!--
<a href="#tailwind" class="section-card" data-section="tailwind">
<div class="section-card-icon" style="background: #06b6d4">TW</div>
<h3>Tailwind CSS</h3>
<p data-i18n="landingTailwindDesc">Utility-first CSS framework</p>
<span class="section-card-progress" id="tailwind-progress"></span>
</a>
-->
</div>
</section>
@@ -164,6 +168,39 @@
<a href="#welcome/0" class="cta-button" data-i18n="landingCtaButton">Begin Your Journey</a>
<p class="cta-sub" data-i18n="landingCtaSub">Free and open source. No account required. Progress saved locally.</p>
</section>
<footer class="landing-footer">
<div class="footer-grid">
<section class="footer-section footer-modules">
<div id="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>
</div>
@@ -356,7 +393,7 @@
<li data-i18n-html="modeHtml"><strong>HTML</strong> - Practice semantic markup and native elements</li>
</ul>
<p class="help-nav-links">
Jump to: <a href="#css">CSS</a> | <a href="#html">HTML</a> | <a href="#tailwind">Tailwind</a> |
Jump to: <a href="#css">CSS</a> | <a href="#html">HTML</a> |
<a href="#reference/css">Reference</a>
</p>
@@ -415,6 +452,13 @@
<li><a href="https://github.com/nextlevelshit/code-crispies" target="_blank">GitHub</a> Public mirror</li>
<li><a href="https://www.linkedin.com/in/michael-werner-czechowski" target="_blank">LinkedIn</a> Michael Czechowski</li>
</ul>
<h4 data-i18n="supportTitle">Support the Project</h4>
<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>
</div>
</div>
</dialog>

View File

@@ -1865,6 +1865,132 @@ input:checked + .toggle-slider::before {
margin-top: 1rem;
}
/* ================= LANDING FOOTER ================= */
.landing-footer {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--border-color);
}
.footer-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
.footer-section h4 {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-color);
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.footer-links {
list-style: none;
padding: 0;
margin: 0;
}
.footer-links li {
margin-bottom: 0.5rem;
}
.footer-links a {
color: var(--light-text);
text-decoration: none;
font-size: 0.875rem;
transition: color 0.2s;
}
.footer-links a:hover {
color: var(--primary-color);
}
#footer-lesson-links {
display: flex;
gap: 2rem;
}
.footer-section-group {
display: flex;
flex-direction: column;
gap: 0.25rem;
min-width: 0;
}
.footer-section-group strong {
margin-bottom: 0.5rem;
}
.footer-section-group strong a {
color: var(--text-color);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.footer-section-group a {
display: inline-block;
color: #888;
text-decoration: none;
font-size: 0.7rem;
font-weight: 500;
background: rgba(0, 0, 0, 0.03);
padding: 4px 10px;
border-radius: 12px;
max-width: 140px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.footer-section-group a:hover {
color: var(--primary-color);
background: rgba(0, 0, 0, 0.06);
}
.footer-support p {
color: var(--light-text);
font-size: 0.875rem;
margin-bottom: 1rem;
}
.footer-bottom {
text-align: center;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
}
.footer-bottom p {
color: var(--light-text);
font-size: 0.8rem;
margin: 0;
}
.footer-bottom a {
color: var(--light-text);
}
.footer-bottom a:hover {
color: var(--primary-color);
}
@media (max-width: 768px) {
.footer-grid {
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
}
@media (max-width: 480px) {
.footer-grid {
grid-template-columns: 1fr;
}
}
/* ================= SECTION PAGE ================= */
.section-page {
flex: 1;