diff --git a/.gitignore b/.gitignore
index a6848d2..b696bb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@
node_modules
dist
coverage
+.env
+.env.local
# Claude Code local settings (user-specific)
.claude/settings.local.json
\ No newline at end of file
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
new file mode 100644
index 0000000..41d01da
--- /dev/null
+++ b/docs/ROADMAP.md
@@ -0,0 +1,297 @@
+# Code Crispies Roadmap
+
+## Current State (Updated)
+
+**Total Active Lessons:** 101 (excluding welcome, goodbye, playground)
+**Target:** 100 lessons for milestone system ✅ ACHIEVED
+
+### Current Module Breakdown
+
+| Module | Lessons | Category | Status |
+|--------|---------|----------|--------|
+| Basic Selectors | 10 | CSS | ✅ |
+| Colors | 4 | CSS | ✅ |
+| **Gradients** | 3 | CSS | ✅ NEW |
+| Typography | 6 | CSS | ✅ +2 |
+| Box Model | 8 | CSS | ✅ |
+| Flexbox | 6 | CSS | ✅ |
+| Grid | 6 | CSS | ✅ |
+| **Positioning** | 4 | CSS | ✅ NEW |
+| Units & Variables | 4 | CSS | ✅ |
+| Responsive | 4 | CSS | ✅ |
+| Transitions & Animations | 4 | CSS | ✅ |
+| **Filters** | 4 | CSS | ✅ NEW |
+| **Pseudo-elements** | 4 | CSS | ✅ NEW |
+| HTML Elements | 2 | HTML | ✅ |
+| **Semantic HTML** | 3 | HTML | ✅ NEW |
+| Figure | 3 | HTML | ✅ |
+| SVG | 3 | HTML | ✅ |
+| Details/Summary | 3 | HTML | ✅ |
+| Dialog | 2 | HTML | ✅ |
+| Progress/Meter | 3 | HTML | ✅ |
+| Forms Basic | 3 | HTML | ✅ |
+| Forms Validation | 1 | HTML | ✅ |
+| Fieldset | 3 | HTML | ✅ |
+| Datalist | 2 | HTML | ✅ |
+| Tables | 3 | HTML | ✅ +2 |
+| **Total** | **101** | | ✅ |
+
+---
+
+## Phase 1: Milestone Progress System ✅ COMPLETED
+
+### Design
+
+Replace percentage-based progress with milestone markers:
+
+```
+[1] [5] [10] [20] [30] [50] [75] [100]
+ ● ● ◐ ○ ○ ○ ○ ○
+```
+
+**Milestones:**
+- 1 lesson - First Step
+- 5 lessons - Getting Started
+- 10 lessons - Rookie
+- 20 lessons - Learner
+- 30 lessons - Intermediate
+- 50 lessons - Halfway Hero
+- 75 lessons - Advanced
+- 100 lessons - Master
+
+### Implementation ✅
+
+1. **Update `LessonEngine.getProgressStats()`** ✅
+ - Added `currentMilestone` and `nextMilestone` fields
+ - Added `milestonesReached: number[]`
+ - Added `progressToNext` percentage
+
+2. **Update Progress UI** ✅
+ - Added milestone dots with visual states (reached, current, next)
+ - Animate milestone completion
+ - Show current milestone badge
+
+3. **Add Milestone Celebration**
+ - Confetti/animation on reaching milestones
+ - Achievement unlocks in sidebar
+
+---
+
+## Phase 2: New Lessons (34 needed to reach 100)
+
+### Priority 1: Expand Existing Modules (+15 lessons)
+
+#### CSS Colors (+3)
+- Gradients (linear-gradient)
+- Color functions (hsl, rgb)
+- Opacity and transparency
+
+#### Typography (+3)
+- Web fonts (@font-face)
+- Text shadows
+- Letter/word spacing
+
+#### Responsive (+3)
+- Container queries
+- Aspect ratio
+- Clamp() for fluid typography
+
+#### Transitions & Animations (+3)
+- Keyframe animations
+- Animation timing functions
+- Transform origin
+
+#### Tables (+3)
+- Table styling (borders, spacing)
+- Responsive tables
+- Table accessibility
+
+### Priority 2: New CSS Modules (+12 lessons)
+
+#### Filters & Effects (4 lessons)
+- CSS filters (blur, brightness, contrast)
+- Backdrop filters
+- Mix-blend-mode
+- Box shadows advanced
+
+#### Positioning (4 lessons)
+- Relative positioning
+- Absolute positioning
+- Fixed/sticky positioning
+- Z-index stacking
+
+#### Pseudo-elements (4 lessons)
+- ::before and ::after
+- ::first-letter and ::first-line
+- ::marker for lists
+- Content property
+
+### Priority 3: New HTML Modules (+7 lessons)
+
+#### Semantic Structure (3 lessons)
+- Article vs Section
+- Header/Footer/Main
+- Nav and Aside
+
+#### Media Elements (2 lessons)
+- Picture element (responsive images)
+- Audio/Video basics
+
+#### Accessibility (2 lessons)
+- ARIA labels
+- Skip links
+- Focus management
+
+---
+
+## MDN Topics Reference
+
+### CSS Topics from MDN (prioritized for interactive lessons)
+
+**Layout Systems:**
+- [x] Flexbox (covered)
+- [x] Grid (covered)
+- [ ] Multi-column layout
+- [ ] Positioned layout (z-index, stacking)
+
+**Visual Effects:**
+- [x] Colors (partially covered)
+- [ ] Filters (blur, brightness, contrast, drop-shadow)
+- [ ] Blend modes (mix-blend-mode, background-blend-mode)
+- [ ] Masking and clipping
+- [ ] Shapes (shape-outside)
+
+**Typography:**
+- [x] Basic text (covered)
+- [ ] Web fonts (@font-face)
+- [ ] Variable fonts
+- [ ] Text decoration advanced
+
+**Animations:**
+- [x] Transitions (covered)
+- [ ] Keyframe animations
+- [ ] Scroll-driven animations (experimental)
+- [ ] View transitions
+
+**Advanced:**
+- [x] Custom properties (covered in units-variables)
+- [ ] Container queries
+- [ ] Anchor positioning (new)
+- [ ] Logical properties (for RTL support)
+
+### HTML Topics from MDN
+
+**Structural:**
+- [x] Basic elements (covered)
+- [x] Figure/figcaption (covered)
+- [ ] Article vs section semantics
+- [ ] Template element
+
+**Interactive:**
+- [x] Details/Summary (covered)
+- [x] Dialog (have JSON, not active)
+- [ ] Datalist (have JSON, not active)
+- [ ] Progress/Meter (have JSON, not active)
+
+**Forms:**
+- [x] Basic forms (covered)
+- [x] Validation (covered)
+- [x] Fieldset (have JSON, not active)
+- [ ] Input types exploration
+
+**Media:**
+- [x] SVG basics (covered)
+- [ ] Picture element
+- [ ] srcset and sizes
+- [ ] Audio/Video
+
+---
+
+## Inactive Lesson Files (Ready to Activate)
+
+These lesson files exist but aren't in the active module list:
+
+| File | Lessons | Topic |
+|------|---------|-------|
+| 24-html-progress-meter.json | 3 | Progress/Meter elements |
+| 25-html-datalist.json | 2 | Datalist for autocomplete |
+| 27-html-dialog.json | 2 | Native dialog element |
+| 28-html-forms-fieldset.json | 3 | Fieldset/legend grouping |
+| 31-html-marquee.json | 3 | Marquee (deprecated but fun) |
+| **Total** | **13** | |
+
+**Quick Win:** Activating these adds 13 lessons immediately → 79 total
+
+---
+
+## Implementation Order
+
+### Week 1: Foundation
+1. Design milestone UI component
+2. Implement milestone progress system
+3. Add milestone celebrations
+
+### Week 2: Quick Wins
+4. Activate 5 inactive HTML modules (+13 lessons)
+5. Test and fix translations
+
+### Week 3-4: New Content
+6. Create Filters & Effects module (+4)
+7. Create Positioning module (+4)
+8. Expand existing modules (+7)
+
+### Final Polish
+9. Reach 100 lessons milestone
+10. Add milestone achievements to sidebar
+11. Update landing page messaging
+
+---
+
+## Technical Notes
+
+### Milestone Data Structure
+
+```js
+const MILESTONES = [1, 5, 10, 20, 30, 50, 75, 100];
+
+function getMilestoneProgress(completed) {
+ const reached = MILESTONES.filter(m => completed >= m);
+ const current = reached[reached.length - 1] || 0;
+ const next = MILESTONES.find(m => m > completed) || 100;
+
+ return {
+ current,
+ next,
+ reached,
+ percentToNext: ((completed - current) / (next - current)) * 100
+ };
+}
+```
+
+### Progress Display Update
+
+```html
+
We build great things.
",
+ "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .title { margin: 0 0 1rem; } .title::after { content: \"\"; display: block; margin-top: 8px; } p { margin: 0; color: #666; }",
+ "sandboxCSS": "",
+ "codePrefix": ".title::after {\n ",
+ "initialCode": "",
+ "codeSuffix": "\n}",
+ "solution": "width: 40px;\n height: 3px;\n background: steelblue;",
+ "previewContainer": "preview-area",
+ "validations": [
+ {
+ "type": "property_value",
+ "value": { "property": "width", "expected": "40px" },
+ "message": "Set
diff --git a/src/main.css b/src/main.css
index 7423163..b4716ab 100644
--- a/src/main.css
+++ b/src/main.css
@@ -662,6 +662,18 @@ kbd {
pointer-events: none;
}
+/* Persistent glow for completed lessons */
+.preview-section.completed-glow::before {
+ content: "";
+ position: absolute;
+ inset: var(--spacing-md);
+ border-radius: var(--border-radius-md);
+ background: conic-gradient(from 0deg, #9163b8, #d45aa0, #1aafb8, #7c4dff, #9163b8);
+ filter: blur(30px);
+ opacity: 0.35;
+ pointer-events: none;
+}
+
.preview-header {
display: flex;
justify-content: space-between;
@@ -796,7 +808,6 @@ kbd {
}
100% {
--border-angle: -360deg;
- border-color: transparent;
}
}
@@ -814,7 +825,7 @@ kbd {
}
100% {
--border-angle: -360deg;
- opacity: 0;
+ opacity: 0.35;
}
}
@@ -981,6 +992,7 @@ nav.sidebar-section {
flex: 1;
overflow-y: auto;
min-height: 0;
+ padding-bottom: var(--spacing-md);
}
.sidebar-section h4 {
@@ -1007,7 +1019,8 @@ nav.sidebar-section {
.progress-fill {
height: 100%;
- background: linear-gradient(135deg, #9163b8, #d45aa0, #1aafb8, #7c4dff);
+ background: linear-gradient(90deg, #9163b8, #d45aa0, #1aafb8, #7c4dff);
+ background-size: calc(100% * 100 / var(--progress-percent, 100)) 100%;
border-radius: 4px;
transition: width 0.3s ease;
width: 0%;
@@ -1018,6 +1031,73 @@ nav.sidebar-section {
color: var(--light-text);
}
+/* Milestone Progress */
+.milestone-progress {
+ gap: var(--spacing-sm);
+}
+
+.milestones {
+ display: flex;
+ justify-content: space-between;
+ padding: 0 2px;
+}
+
+.milestone {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 24px;
+ height: 24px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ color: var(--light-text);
+ background: var(--border-color);
+ border-radius: 50%;
+ transition: all 0.3s ease;
+}
+
+.milestone.reached {
+ color: white;
+}
+
+/* Each milestone gets a portion of the gradient based on position */
+.milestone.reached:nth-child(1) { background: #9163b8; }
+.milestone.reached:nth-child(2) { background: linear-gradient(135deg, #9163b8, #a85dac); }
+.milestone.reached:nth-child(3) { background: linear-gradient(135deg, #9163b8, #d45aa0); }
+.milestone.reached:nth-child(4) { background: linear-gradient(135deg, #9163b8, #d45aa0, #e87aac); }
+.milestone.reached:nth-child(5) { background: linear-gradient(135deg, #9163b8, #d45aa0, #1aafb8); }
+.milestone.reached:nth-child(6) { background: linear-gradient(135deg, #9163b8, #d45aa0, #1aafb8, #4b8ecc); }
+.milestone.reached:nth-child(7) { background: linear-gradient(135deg, #9163b8, #d45aa0, #1aafb8, #7c4dff); }
+.milestone.reached:nth-child(8) { background: linear-gradient(135deg, #9163b8, #d45aa0, #1aafb8, #7c4dff); }
+
+.milestone.current {
+ color: white;
+ transform: scale(1.15);
+ box-shadow: 0 2px 8px rgba(145, 99, 184, 0.4);
+}
+
+.milestone.next {
+ border: 2px dashed var(--light-text);
+ background: transparent;
+}
+
+/* Milestone celebration animation */
+@keyframes milestone-pop {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.4);
+ }
+ 100% {
+ transform: scale(1.15);
+ }
+}
+
+.milestone.just-reached {
+ animation: milestone-pop 0.5s ease-out;
+}
+
/* Module List in Sidebar */
.module-list {
/* No max-height - parent nav.sidebar-section handles overflow */
@@ -1200,7 +1280,8 @@ button.lesson-list-item {
border-color: var(--section-color, var(--primary-color));
}
-.btn-icon img {
+.btn-icon img,
+.btn-icon svg {
width: 1rem;
height: 1rem;
margin: 0;
@@ -1238,6 +1319,28 @@ button.lesson-list-item {
color: var(--danger-color);
}
+.btn-danger {
+ background: var(--danger-color);
+ color: white;
+ border-color: var(--danger-color);
+}
+
+.btn-danger:hover {
+ background: #c82333;
+ border-color: #bd2130;
+}
+
+.btn-text.btn-danger {
+ background: transparent;
+ color: var(--danger-color);
+ border: none;
+}
+
+.btn-text.btn-danger:hover {
+ color: #c82333;
+ text-decoration: underline;
+}
+
#reset-code-btn {
background: var(--section-color, var(--primary-color));
color: white;
@@ -1493,6 +1596,265 @@ input:checked + .toggle-slider::before {
flex-direction: row-reverse;
}
+/* ================= AUTH DIALOG ================= */
+.auth-dialog {
+ max-width: 400px;
+}
+
+.auth-form {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-md);
+}
+
+.form-field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.form-field label {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--light-text);
+}
+
+.form-field input {
+ padding: 0.75rem 1rem;
+ border: 2px solid var(--border-color);
+ border-radius: var(--border-radius-md);
+ font-size: 1rem;
+ font-family: var(--font-main);
+ transition: border-color 0.2s;
+}
+
+.form-field input:focus {
+ outline: none;
+ border-color: var(--primary-color);
+}
+
+.btn-full {
+ width: 100%;
+}
+
+.btn-sm {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+}
+
+.auth-error {
+ color: var(--danger-color);
+ font-size: 0.875rem;
+ margin: 0;
+}
+
+.auth-success {
+ color: var(--success-color);
+ font-size: 0.875rem;
+ margin: 0;
+}
+
+.auth-instructions {
+ color: var(--light-text);
+ font-size: 0.9rem;
+ margin-bottom: var(--spacing-sm);
+}
+
+.auth-links {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: var(--spacing-md);
+ padding-top: var(--spacing-md);
+ border-top: 1px solid var(--border-color);
+}
+
+.auth-links .btn-text {
+ font-size: 0.875rem;
+ color: var(--primary-color);
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.auth-links .btn-text:hover {
+ color: var(--primary-color-dark, var(--primary-color));
+ text-decoration: underline;
+}
+
+/* Social Login */
+.auth-social {
+ margin-top: var(--spacing-lg);
+}
+
+.auth-divider {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-md);
+ color: var(--light-text);
+ font-size: 0.875rem;
+}
+
+.auth-divider::before,
+.auth-divider::after {
+ content: "";
+ flex: 1;
+ height: 1px;
+ background: var(--border-color);
+}
+
+.auth-social-buttons {
+ display: flex;
+ gap: 0.75rem;
+}
+
+.btn-social {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ border: 2px solid var(--border-color);
+ border-radius: var(--border-radius-md);
+ background: var(--panel-bg);
+ font-weight: 500;
+ cursor: pointer;
+ transition:
+ border-color 0.2s,
+ background 0.2s;
+}
+
+.btn-social:hover {
+ border-color: var(--primary-color);
+ background: var(--primary-bg-light);
+}
+
+.social-icon {
+ width: 1.25rem;
+ height: 1.25rem;
+}
+
+/* Header Auth Button */
+.user-menu {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.user-email {
+ font-size: 0.875rem;
+ color: var(--light-text);
+ max-width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Sidebar Auth Box (dark design) */
+.sidebar-auth-box {
+ margin-top: var(--spacing-md);
+ padding: var(--spacing-md);
+ background: #1a1a2e;
+ border-radius: var(--border-radius-md);
+ color: #e0e0e0;
+}
+
+.sidebar-auth-box h4 {
+ color: #fff;
+ margin-bottom: var(--spacing-sm);
+}
+
+.sidebar-auth-box .btn-outline {
+ background: transparent;
+ color: #e0e0e0;
+ border-color: #444;
+}
+
+.sidebar-auth-box .btn-outline:hover {
+ background: #2a2a4e;
+ border-color: #666;
+ color: #fff;
+}
+
+.user-menu-sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.user-menu-sidebar .user-email {
+ max-width: none;
+ word-break: break-all;
+ font-size: 0.875rem;
+ color: #aaa;
+ font-weight: 500;
+}
+
+.sidebar-auth-hint {
+ font-size: 0.8rem;
+ color: #888;
+ margin-top: var(--spacing-sm);
+}
+
+/* Footer Legal Links */
+.footer-legal {
+ margin-top: var(--spacing-xs);
+ font-size: 0.85rem;
+}
+
+.footer-legal .btn-text {
+ color: var(--light-text);
+ font-size: 0.85rem;
+ text-decoration: none;
+ padding: 0;
+}
+
+.footer-legal .btn-text:hover {
+ color: var(--text-color);
+ text-decoration: underline;
+}
+
+.footer-separator {
+ color: var(--light-text);
+ margin: 0 0.5rem;
+}
+
+/* Legal Dialogs (Privacy, Imprint) */
+.legal-dialog {
+ max-width: 600px;
+}
+
+.legal-content {
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+.legal-content h4 {
+ margin-top: var(--spacing-md);
+ margin-bottom: var(--spacing-xs);
+ font-size: 1rem;
+ color: var(--text-color);
+}
+
+.legal-content p {
+ margin-bottom: var(--spacing-sm);
+ line-height: 1.6;
+ color: var(--light-text);
+}
+
+.legal-content a {
+ color: var(--primary-color);
+}
+
+.legal-updated {
+ margin-top: var(--spacing-md);
+ font-size: 0.85rem;
+ font-style: italic;
+ color: var(--lighter-text);
+}
+
/* Project Cards in Help Dialog */
.project-cards {
display: flex;
@@ -1581,9 +1943,7 @@ input:checked + .toggle-slider::before {
}
.nav-link-ref {
- margin-left: 0.5rem;
- padding-left: 1rem;
- border-left: 1px solid var(--border-color);
+ margin-left: 1rem;
}
@media (min-width: 769px) {
@@ -1924,11 +2284,16 @@ input:checked + .toggle-slider::before {
}
.coming-soon-icon {
- font-size: 2rem;
display: block;
margin-bottom: 0.75rem;
}
+.coming-soon-icon svg {
+ width: 2rem;
+ height: 2rem;
+ stroke: var(--section-color);
+}
+
.coming-soon-card h3 {
font-size: 1rem;
margin-bottom: 0.5rem;
@@ -1954,6 +2319,71 @@ input:checked + .toggle-slider::before {
}
}
+/* Newsletter Signup */
+.newsletter-signup {
+ margin-top: var(--spacing-lg);
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.newsletter-signup p {
+ margin: 0;
+ color: var(--light-text);
+}
+
+.newsletter-form {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.newsletter-form input[type="email"] {
+ padding: 0.5rem 1rem;
+ border: 2px solid var(--border-color);
+ border-radius: var(--border-radius-sm);
+ background: var(--panel-bg);
+ color: var(--text);
+ font-size: 1rem;
+ min-width: 200px;
+}
+
+.newsletter-form input[type="email"]:focus {
+ outline: none;
+ border-color: var(--section-color);
+}
+
+.newsletter-signup .btn-outline {
+ border: 2px solid var(--section-color);
+ color: var(--section-color);
+ background: transparent;
+ padding: 0.5rem 1.5rem;
+ font-weight: 500;
+ transition: all 0.2s;
+}
+
+.newsletter-signup .btn-outline:hover {
+ background: var(--section-color);
+ color: white;
+}
+
+.newsletter-disclaimer {
+ font-size: 0.8rem;
+ opacity: 0.7;
+}
+
+.newsletter-thanks {
+ color: var(--success);
+ font-weight: 500;
+}
+
+.newsletter-thanks.hidden {
+ display: none;
+}
+
/* Device Notice */
.device-notice {
margin-top: var(--spacing-lg);
@@ -2211,12 +2641,12 @@ input:checked + .toggle-slider::before {
}
.section-overview strong {
- color: var(--primary-dark);
+ color: var(--section-color-dark, var(--primary-dark));
}
.section-overview code {
- background: var(--primary-bg-light);
- color: var(--primary-dark);
+ background: rgba(var(--section-color-rgb, 145, 99, 184), 0.1);
+ color: var(--section-color-dark, var(--primary-dark));
padding: 0.1rem 0.35rem;
border-radius: 4px;
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
@@ -2239,13 +2669,13 @@ input:checked + .toggle-slider::before {
.topic-text h2 {
font-size: 1.25rem;
- color: var(--primary-dark);
+ color: var(--section-color-dark, var(--primary-dark));
margin: 0 0 0.75rem;
}
.topic-text h3 {
font-size: 1rem;
- color: var(--primary-dark);
+ color: var(--section-color-dark, var(--primary-dark));
margin: 0 0 0.5rem;
}
@@ -2325,8 +2755,8 @@ input:checked + .toggle-slider::before {
/* Inline code in topic text */
.topic-text code {
- background: var(--primary-bg-light);
- color: var(--primary-dark);
+ background: rgba(var(--section-color-rgb, 145, 99, 184), 0.1);
+ color: var(--section-color-dark, var(--primary-dark));
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
@@ -3230,6 +3660,19 @@ body[data-section="tailwind"] .section-progress-bar .progress-fill {
color: #1aafb8;
}
+/* Lesson title h2 section colors */
+body[data-section="css"] #lesson-title {
+ color: #9163b8;
+}
+
+body[data-section="html"] #lesson-title {
+ color: #d45aa0;
+}
+
+body[data-section="tailwind"] #lesson-title {
+ color: #1aafb8;
+}
+
/* Section and Reference footer - override landing-footer styles */
.section-footer.landing-footer,
.reference-footer.landing-footer {
diff --git a/src/supabase.js b/src/supabase.js
new file mode 100644
index 0000000..a231968
--- /dev/null
+++ b/src/supabase.js
@@ -0,0 +1,106 @@
+import { createClient } from "@supabase/supabase-js";
+
+const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+// Check if Supabase is configured
+export const isConfigured = Boolean(supabaseUrl && supabaseAnonKey);
+
+// Only create client if configured
+const supabase = isConfigured
+ ? createClient(supabaseUrl, supabaseAnonKey)
+ : null;
+
+// Auth helpers - all return null/rejected promise if not configured
+export const auth = {
+ signUp: (email, password) =>
+ supabase?.auth.signUp({ email, password }) ??
+ Promise.resolve({ data: null, error: { message: "Not configured" } }),
+
+ signIn: (email, password) =>
+ supabase?.auth.signInWithPassword({ email, password }) ??
+ Promise.resolve({ data: null, error: { message: "Not configured" } }),
+
+ signOut: () =>
+ supabase?.auth.signOut() ??
+ Promise.resolve({ error: null }),
+
+ resetPassword: (email) =>
+ supabase?.auth.resetPasswordForEmail(email) ??
+ Promise.resolve({ data: null, error: { message: "Not configured" } }),
+
+ signInWithGoogle: () =>
+ supabase?.auth.signInWithOAuth({ provider: "google" }) ??
+ Promise.resolve({ data: null, error: { message: "Not configured" } }),
+
+ signInWithGitHub: () =>
+ supabase?.auth.signInWithOAuth({ provider: "github" }) ??
+ Promise.resolve({ data: null, error: { message: "Not configured" } }),
+
+ getUser: () =>
+ supabase?.auth.getUser() ??
+ Promise.resolve({ data: { user: null }, error: null }),
+
+ getSession: () =>
+ supabase?.auth.getSession() ??
+ Promise.resolve({ data: { session: null }, error: null }),
+
+ setSession: ({ access_token, refresh_token }) =>
+ supabase?.auth.setSession({ access_token, refresh_token }) ??
+ Promise.resolve({ data: { session: null }, error: { message: "Not configured" } }),
+
+ onAuthStateChange: (callback) =>
+ supabase?.auth.onAuthStateChange(callback) ?? { data: { subscription: { unsubscribe: () => {} } } },
+
+ deleteAccount: async () => {
+ if (!supabase) return { error: { message: "Not configured" } };
+ const { error } = await supabase.rpc("delete_own_account");
+ return { error };
+ },
+};
+
+// Progress sync helpers
+export const progressDB = {
+ async load(userId) {
+ if (!supabase) return { data: null, error: { message: "Not configured" } };
+ const { data, error } = await supabase
+ .from("user_progress")
+ .select("*")
+ .eq("user_id", userId)
+ .single();
+ return { data, error };
+ },
+
+ async save(userId, progress, userCode, settings, language) {
+ if (!supabase) return { error: { message: "Not configured" } };
+ const { error } = await supabase.from("user_progress").upsert(
+ {
+ user_id: userId,
+ progress,
+ user_code: userCode,
+ settings,
+ language,
+ },
+ { onConflict: "user_id" }
+ );
+ return { error };
+ },
+};
+
+// Newsletter subscription helper
+export const newsletter = {
+ async subscribe(email) {
+ if (!supabase) return { error: { message: "Not configured" } };
+ // Use insert with ignoreDuplicates since RLS only allows INSERT
+ const { error } = await supabase.from("newsletter_subscribers").insert(
+ {
+ email: email.toLowerCase().trim(),
+ subscribed_at: new Date().toISOString(),
+ },
+ { onConflict: "email", ignoreDuplicates: true }
+ );
+ // Ignore duplicate email errors (already subscribed)
+ if (error?.code === "23505") return { error: null };
+ return { error };
+ },
+};
diff --git a/supabase-setup.sql b/supabase-setup.sql
new file mode 100644
index 0000000..0134774
--- /dev/null
+++ b/supabase-setup.sql
@@ -0,0 +1,58 @@
+-- CODE CRISPIES - Supabase Database Setup
+-- Run this in Supabase Dashboard → SQL Editor → New Query
+
+-- Drop existing objects first
+ DROP FUNCTION IF EXISTS delete_own_account();
+ DROP TABLE IF EXISTS user_progress;
+ DROP TABLE IF EXISTS newsletter_subscribers;
+
+-- User progress table
+CREATE TABLE user_progress (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
+ progress JSONB NOT NULL DEFAULT '{}',
+ user_code JSONB NOT NULL DEFAULT '{}',
+ settings JSONB NOT NULL DEFAULT '{}',
+ language TEXT DEFAULT 'en',
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ UNIQUE(user_id)
+);
+
+-- Newsletter subscribers table
+CREATE TABLE newsletter_subscribers (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ email TEXT UNIQUE NOT NULL,
+ subscribed_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Row Level Security
+ALTER TABLE user_progress ENABLE ROW LEVEL SECURITY;
+ALTER TABLE newsletter_subscribers ENABLE ROW LEVEL SECURITY;
+
+-- Users can only access their own progress
+CREATE POLICY "Users can CRUD own progress"
+ ON user_progress FOR ALL
+ USING (auth.uid() = user_id)
+ WITH CHECK (auth.uid() = user_id);
+
+-- Anyone can subscribe to newsletter (public insert)
+CREATE POLICY "Anyone can subscribe to newsletter"
+ ON newsletter_subscribers FOR INSERT
+ WITH CHECK (true);
+
+-- Function to delete own account (called via RPC)
+CREATE OR REPLACE FUNCTION delete_own_account()
+RETURNS void
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public
+AS $$
+BEGIN
+ -- Delete user's progress (CASCADE should handle this, but be explicit)
+ DELETE FROM user_progress WHERE user_id = auth.uid();
+
+ -- Delete the user from auth.users
+ DELETE FROM auth.users WHERE id = auth.uid();
+END;
+$$;
diff --git a/vite.config.js b/vite.config.js
index 23a2c56..8adbdeb 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -3,6 +3,7 @@ import { defineConfig } from "vite";
export default defineConfig((env) => ({
base: "/",
root: "./src",
+ envDir: "..",
publicDir: "../public",
build: {
outDir: "../dist",