feat: 10.0 - Frontend - Check habit interaction

This commit is contained in:
Echo
2026-02-10 12:28:40 +00:00
parent 0483d73ef8
commit 775f1715d1
2 changed files with 323 additions and 1 deletions

View File

@@ -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 ? '<i data-lucide="check"></i>' : '';
// Create card HTML
card.innerHTML = `
<div class="${checkboxClass}" data-habit-id="${habit.id}" onclick="checkHabit('${habit.id}', this)">
${checkIcon}
</div>
<div class="habit-icon">
<i data-lucide="${iconName}"></i>
</div>
@@ -585,7 +633,7 @@
<div class="habit-frequency">${habit.frequency === 'daily' ? 'Zilnic' : 'Săptămânal'}</div>
</div>
<div class="habit-streak">
<span>${habit.streak || 0}</span>
<span id="streak-${habit.id}">${habit.streak || 0}</span>
<span>🔥</span>
</div>
`;
@@ -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 = '<i data-lucide="check"></i>';
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();

View File

@@ -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())