From 775f1715d14594476f5589ad8d23e6cf14daf2dc Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 10 Feb 2026 12:28:40 +0000 Subject: [PATCH] feat: 10.0 - Frontend - Check habit interaction --- dashboard/habits.html | 102 +++++++++++++- dashboard/test_habits_check_ui.py | 222 ++++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 dashboard/test_habits_check_ui.py 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())