diff --git a/dashboard/habits.html b/dashboard/habits.html
index 6ec297c..d930605 100644
--- a/dashboard/habits.html
+++ b/dashboard/habits.html
@@ -144,6 +144,46 @@
flex-shrink: 0;
}
+ /* Habit checkbox */
+ .habit-checkbox {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ border: 2px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ flex-shrink: 0;
+ background: var(--bg-base);
+ }
+
+ .habit-checkbox:hover:not(.disabled) {
+ border-color: var(--accent);
+ background: var(--accent-light, rgba(99, 102, 241, 0.1));
+ }
+
+ .habit-checkbox.checked {
+ background: var(--accent);
+ border-color: var(--accent);
+ }
+
+ .habit-checkbox.checked svg {
+ color: white;
+ }
+
+ .habit-checkbox.disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+ }
+
+ .habit-checkbox svg {
+ width: 18px;
+ height: 18px;
+ color: var(--accent);
+ }
+
/* Loading state */
.loading-state {
text-align: center;
@@ -575,8 +615,16 @@
// Determine icon based on frequency
const iconName = habit.frequency === 'daily' ? 'calendar' : 'clock';
+ // Checkbox state
+ const isChecked = habit.checkedToday || false;
+ const checkboxClass = isChecked ? 'habit-checkbox checked disabled' : 'habit-checkbox';
+ const checkIcon = isChecked ? '' : '';
+
// Create card HTML
card.innerHTML = `
+
+ ${checkIcon}
+
@@ -585,7 +633,7 @@
${habit.frequency === 'daily' ? 'Zilnic' : 'Săptămânal'}
- ${habit.streak || 0}
+ ${habit.streak || 0}
🔥
`;
@@ -600,6 +648,58 @@
return div.innerHTML;
}
+ // Check habit (mark as done for today)
+ async function checkHabit(habitId, checkboxElement) {
+ // Don't allow checking if already checked
+ if (checkboxElement.classList.contains('disabled')) {
+ return;
+ }
+
+ // Optimistic UI update
+ checkboxElement.classList.add('checked', 'disabled');
+ checkboxElement.innerHTML = '';
+ lucide.createIcons();
+
+ // Store original state for rollback
+ const originalCheckbox = checkboxElement.cloneNode(true);
+ const streakElement = document.getElementById(`streak-${habitId}`);
+ const originalStreak = streakElement ? streakElement.textContent : '0';
+
+ try {
+ const response = await fetch(`/api/habits/${habitId}/check`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to check habit');
+ }
+
+ const data = await response.json();
+
+ // Update streak with server value
+ if (streakElement && data.habit && data.habit.streak !== undefined) {
+ streakElement.textContent = data.habit.streak;
+ }
+
+ showToast('Obișnuință bifată! 🎉');
+
+ } catch (error) {
+ console.error('Error checking habit:', error);
+
+ // Revert checkbox on error
+ checkboxElement.classList.remove('checked', 'disabled');
+ checkboxElement.innerHTML = '';
+
+ // Revert streak
+ if (streakElement) {
+ streakElement.textContent = originalStreak;
+ }
+
+ showToast('Eroare la bifarea obișnuinței. Încearcă din nou.');
+ }
+ }
+
// Load habits on page load
document.addEventListener('DOMContentLoaded', () => {
loadHabits();
diff --git a/dashboard/test_habits_check_ui.py b/dashboard/test_habits_check_ui.py
new file mode 100644
index 0000000..ef1ddd5
--- /dev/null
+++ b/dashboard/test_habits_check_ui.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+"""
+Tests for Story 10.0: Frontend - Check habit interaction
+"""
+
+import sys
+import re
+
+def load_html():
+ """Load habits.html content"""
+ try:
+ with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
+ return f.read()
+ except FileNotFoundError:
+ print("ERROR: dashboard/habits.html not found")
+ sys.exit(1)
+
+def test_checkbox_css_exists():
+ """Test that checkbox CSS styles are defined"""
+ html = load_html()
+
+ # Check for checkbox class
+ assert '.habit-checkbox' in html, "Missing .habit-checkbox CSS class"
+
+ # Check for circular shape (border-radius: 50%)
+ assert 'border-radius: 50%' in html, "Checkbox should be circular (border-radius: 50%)"
+
+ # Check for checked state
+ assert '.habit-checkbox.checked' in html, "Missing .habit-checkbox.checked CSS"
+
+ # Check for disabled state
+ assert '.habit-checkbox.disabled' in html, "Missing .habit-checkbox.disabled CSS"
+
+ # Check for hover state
+ assert '.habit-checkbox:hover' in html, "Missing .habit-checkbox:hover CSS"
+
+ print("✓ Checkbox CSS styles exist")
+
+def test_checkbox_in_habit_card():
+ """Test that createHabitCard includes checkbox"""
+ html = load_html()
+
+ # Check that createHabitCard creates a checkbox element
+ assert 'habit-checkbox' in html, "createHabitCard should include checkbox element"
+
+ # Check for data-habit-id attribute
+ assert 'data-habit-id' in html, "Checkbox should have data-habit-id attribute"
+
+ # Check for onclick handler
+ assert 'onclick="checkHabit' in html, "Checkbox should have onclick='checkHabit' handler"
+
+ print("✓ Checkbox is included in habit card")
+
+def test_checkbox_checked_state():
+ """Test that checkbox uses checkedToday to determine state"""
+ html = load_html()
+
+ # Look for logic that checks habit.checkedToday
+ assert 'checkedToday' in html, "Should check habit.checkedToday property"
+
+ # Check for conditional checked class
+ assert 'checked' in html, "Should add 'checked' class when checkedToday is true"
+
+ # Check for check icon
+ assert 'data-lucide="check"' in html, "Should show check icon when checked"
+
+ print("✓ Checkbox state reflects checkedToday")
+
+def test_check_habit_function_exists():
+ """Test that checkHabit function is defined"""
+ html = load_html()
+
+ # Check for function definition
+ assert 'function checkHabit' in html or 'async function checkHabit' in html, \
+ "checkHabit function should be defined"
+
+ # Check for parameters
+ assert re.search(r'function checkHabit\s*\(\s*habitId', html) or \
+ re.search(r'async function checkHabit\s*\(\s*habitId', html), \
+ "checkHabit should accept habitId parameter"
+
+ print("✓ checkHabit function exists")
+
+def test_check_habit_api_call():
+ """Test that checkHabit calls POST /api/habits/{id}/check"""
+ html = load_html()
+
+ # Check for fetch call
+ assert 'fetch(' in html, "checkHabit should use fetch API"
+
+ # Check for POST method
+ assert "'POST'" in html or '"POST"' in html, "checkHabit should use POST method"
+
+ # Check for /api/habits/ endpoint
+ assert '/api/habits/' in html, "Should call /api/habits/{id}/check endpoint"
+
+ # Check for /check path
+ assert '/check' in html, "Should call endpoint with /check path"
+
+ print("✓ checkHabit calls POST /api/habits/{id}/check")
+
+def test_optimistic_ui_update():
+ """Test that checkbox updates immediately (optimistic)"""
+ html = load_html()
+
+ # Check for classList.add before fetch
+ # The pattern should be: add 'checked' class, then fetch
+ checkHabitFunc = html[html.find('async function checkHabit'):html.find('async function checkHabit') + 2000]
+
+ # Check for immediate classList modification
+ assert 'classList.add' in checkHabitFunc, "Should add class immediately (optimistic update)"
+ assert "'checked'" in checkHabitFunc or '"checked"' in checkHabitFunc, \
+ "Should add 'checked' class optimistically"
+
+ print("✓ Optimistic UI update implemented")
+
+def test_error_handling_revert():
+ """Test that checkbox reverts on error"""
+ html = load_html()
+
+ # Check for catch block
+ assert 'catch' in html, "checkHabit should have error handling (catch)"
+
+ # Check for classList.remove in error handler
+ checkHabitFunc = html[html.find('async function checkHabit'):]
+
+ # Find the catch block
+ if 'catch' in checkHabitFunc:
+ catchBlock = checkHabitFunc[checkHabitFunc.find('catch'):]
+
+ # Check for revert logic
+ assert 'classList.remove' in catchBlock, "Should revert checkbox on error"
+ assert "'checked'" in catchBlock or '"checked"' in catchBlock, \
+ "Should remove 'checked' class on error"
+
+ print("✓ Error handling reverts checkbox")
+
+def test_disabled_when_checked():
+ """Test that checkbox is disabled when already checked"""
+ html = load_html()
+
+ # Check for disabled class on checked habits
+ assert 'disabled' in html, "Should add 'disabled' class to checked habits"
+
+ # Check for early return if disabled
+ checkHabitFunc = html[html.find('async function checkHabit'):html.find('async function checkHabit') + 1000]
+ assert 'disabled' in checkHabitFunc, "Should check if checkbox is disabled"
+ assert 'return' in checkHabitFunc, "Should return early if disabled"
+
+ print("✓ Checkbox disabled when already checked")
+
+def test_streak_updates():
+ """Test that streak updates after successful check"""
+ html = load_html()
+
+ # Check for streak element ID
+ assert 'streak-' in html, "Should use ID for streak element (e.g., streak-${habit.id})"
+
+ # Check for getElementById to update streak
+ checkHabitFunc = html[html.find('async function checkHabit'):]
+ assert 'getElementById' in checkHabitFunc or 'getElementById' in html, \
+ "Should get streak element by ID to update it"
+
+ # Check that response data is used to update streak
+ assert '.streak' in checkHabitFunc or 'streak' in checkHabitFunc, \
+ "Should update streak from response data"
+
+ print("✓ Streak updates after successful check")
+
+def test_check_icon_display():
+ """Test that check icon is shown when checked"""
+ html = load_html()
+
+ # Check for lucide check icon
+ assert 'data-lucide="check"' in html, "Should use lucide check icon"
+
+ # Check that icon is created/shown after checking
+ checkHabitFunc = html[html.find('async function checkHabit'):html.find('async function checkHabit') + 1500]
+ assert 'lucide.createIcons()' in checkHabitFunc, \
+ "Should reinitialize lucide icons after adding check icon"
+
+ print("✓ Check icon displays correctly")
+
+def main():
+ """Run all tests"""
+ print("Running Story 10.0 Frontend Check Interaction Tests...\n")
+
+ tests = [
+ test_checkbox_css_exists,
+ test_checkbox_in_habit_card,
+ test_checkbox_checked_state,
+ test_check_habit_function_exists,
+ test_check_habit_api_call,
+ test_optimistic_ui_update,
+ test_error_handling_revert,
+ test_disabled_when_checked,
+ test_streak_updates,
+ test_check_icon_display,
+ ]
+
+ passed = 0
+ failed = 0
+
+ for test in tests:
+ try:
+ test()
+ passed += 1
+ except AssertionError as e:
+ print(f"✗ {test.__name__}: {e}")
+ failed += 1
+ except Exception as e:
+ print(f"✗ {test.__name__}: Unexpected error: {e}")
+ failed += 1
+
+ print(f"\n{'='*50}")
+ print(f"Results: {passed} passed, {failed} failed")
+ print(f"{'='*50}")
+
+ return 0 if failed == 0 else 1
+
+if __name__ == '__main__':
+ sys.exit(main())