diff --git a/dashboard/habits.html b/dashboard/habits.html index b7480b6..1b64cdd 100644 --- a/dashboard/habits.html +++ b/dashboard/habits.html @@ -79,13 +79,17 @@ opacity: 0.7; } - /* Habit card (placeholder for next story) */ + /* Habit card */ .habit-card { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-lg); + border-left: 4px solid var(--accent); padding: var(--space-4); transition: all var(--transition-base); + display: flex; + flex-direction: column; + gap: var(--space-3); } .habit-card:hover { @@ -94,17 +98,157 @@ box-shadow: var(--shadow-md); } - .habit-name { + .habit-card-header { + display: flex; + align-items: center; + gap: var(--space-2); + } + + .habit-card-icon { + width: 20px; + height: 20px; + color: var(--text-primary); + flex-shrink: 0; + } + + .habit-card-name { + flex: 1; font-size: var(--text-base); font-weight: 600; color: var(--text-primary); - margin-bottom: var(--space-2); } - .habit-meta { + .habit-card-actions { + display: flex; + gap: var(--space-2); + } + + .habit-card-action-btn { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: var(--space-1); + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + transition: all var(--transition-base); + } + + .habit-card-action-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); + } + + .habit-card-action-btn svg { + width: 16px; + height: 16px; + } + + .habit-card-streaks { + display: flex; + gap: var(--space-4); font-size: var(--text-sm); color: var(--text-muted); } + + .habit-card-streak { + display: flex; + align-items: center; + gap: var(--space-1); + } + + .habit-card-check-btn { + width: 100%; + padding: var(--space-3); + border: 2px solid var(--accent); + background: var(--accent); + color: white; + border-radius: var(--radius-md); + font-size: var(--text-base); + font-weight: 600; + cursor: pointer; + transition: all var(--transition-base); + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + } + + .habit-card-check-btn:hover:not(:disabled) { + background: var(--accent-hover); + transform: scale(1.02); + } + + .habit-card-check-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + background: var(--bg-muted); + border-color: var(--border); + color: var(--text-muted); + } + + .habit-card-last-check { + font-size: var(--text-sm); + color: var(--text-muted); + text-align: center; + } + + .habit-card-lives { + display: flex; + justify-content: center; + gap: var(--space-1); + font-size: var(--text-lg); + } + + .habit-card-completion { + font-size: var(--text-sm); + color: var(--text-muted); + text-align: center; + } + + .habit-card-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: var(--space-2); + border-top: 1px solid var(--border); + } + + .habit-card-category { + font-size: var(--text-xs); + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-sm); + background: var(--bg-muted); + color: var(--text-muted); + } + + .habit-card-priority { + font-size: var(--text-xs); + color: var(--text-muted); + display: flex; + align-items: center; + gap: var(--space-1); + } + + .priority-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + } + + .priority-high { + background: var(--error); + } + + .priority-medium { + background: var(--warning); + } + + .priority-low { + background: var(--success); + } @@ -222,24 +366,134 @@ lucide.createIcons(); } - // Render single habit card (placeholder - full card in next story) + // Render single habit card function renderHabitCard(habit) { + const isDoneToday = isCheckedToday(habit); + const lastCheckInfo = getLastCheckInfo(habit); + const livesHtml = renderLives(habit.lives || 3); + const completionRate = habit.completion_rate_30d || 0; + return ` -
-
${escapeHtml(habit.name)}
-
- Frequency: ${habit.frequency.type} - ${habit.category ? ` ยท ${habit.category}` : ''} +
+
+ + ${escapeHtml(habit.name)} +
+ + +
+
+ +
+
+ ๐Ÿ”ฅ ${habit.streak?.current || 0} +
+
+ ๐Ÿ† ${habit.streak?.best || 0} +
+
+ + + +
${lastCheckInfo}
+ +
${livesHtml}
+ +
${completionRate}% (30d)
+ +
`; } + // Check if habit was checked today + function isCheckedToday(habit) { + if (!habit.completions || habit.completions.length === 0) { + return false; + } + const today = new Date().toISOString().split('T')[0]; + return habit.completions.some(c => c.date === today); + } + + // Get last check-in info text + function getLastCheckInfo(habit) { + if (!habit.completions || habit.completions.length === 0) { + return 'Last: Never'; + } + + const lastCompletion = habit.completions[habit.completions.length - 1]; + const lastDate = new Date(lastCompletion.date); + const today = new Date(); + today.setHours(0, 0, 0, 0); + lastDate.setHours(0, 0, 0, 0); + + const diffDays = Math.floor((today - lastDate) / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + return 'Last: Today'; + } else if (diffDays === 1) { + return 'Last: Yesterday'; + } else { + return `Last: ${diffDays} days ago`; + } + } + + // Render lives as hearts + function renderLives(lives) { + const totalLives = 3; + let html = ''; + for (let i = 0; i < totalLives; i++) { + html += i < lives ? 'โค๏ธ' : '๐Ÿ–ค'; + } + return html; + } + + // Get priority level string + function getPriorityLevel(priority) { + if (priority === 1) return 'high'; + if (priority === 2) return 'medium'; + return 'low'; + } + // Show add habit modal (placeholder - full modal in next stories) function showAddHabitModal() { alert('Add Habit modal - coming in next story!'); } + // Show edit habit modal (placeholder) + function showEditHabitModal(habitId) { + alert('Edit Habit modal - coming in next story!'); + } + + // Delete habit (placeholder) + async function deleteHabit(habitId) { + if (!confirm('Are you sure you want to delete this habit?')) { + return; + } + alert('Delete functionality - coming in next story!'); + } + + // Check in habit (placeholder) + async function checkInHabit(habitId) { + alert('Check-in functionality - coming in next story!'); + } + // Show error message function showError(message) { const container = document.getElementById('habitsContainer'); diff --git a/dashboard/tests/test_habits_frontend.py b/dashboard/tests/test_habits_frontend.py index b4f7340..8766149 100644 --- a/dashboard/tests/test_habits_frontend.py +++ b/dashboard/tests/test_habits_frontend.py @@ -1,6 +1,7 @@ """ Test suite for Habits frontend page structure and navigation Story US-006: Frontend - Page structure, layout, and navigation link +Story US-007: Frontend - Habit card component """ import sys @@ -149,9 +150,168 @@ def test_typecheck(): print("โœ“ Test 10: HTML structure is well-formed") +def test_card_colored_border(): + """Test 11: Habit card has colored left border matching habit.color""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + assert 'border-left-color' in content or 'borderLeftColor' in content, \ + "Card should have colored left border" + assert 'habit.color' in content, "Card should use habit.color for border" + print("โœ“ Test 11: Card has colored left border") + +def test_card_header_icons(): + """Test 12: Card header shows icon, name, settings, and delete""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check for icon display + assert 'habit.icon' in content or 'habit-card-icon' in content, \ + "Card should display habit icon" + + # Check for name display + assert 'habit.name' in content or 'habit-card-name' in content, \ + "Card should display habit name" + + # Check for settings (gear) icon + assert 'settings' in content.lower(), "Card should have settings icon" + + # Check for delete (trash) icon + assert 'trash' in content.lower(), "Card should have delete icon" + + print("โœ“ Test 12: Card header has icon, name, settings, and delete") + +def test_card_streak_display(): + """Test 13: Streak displays with fire emoji for current and trophy for best""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + assert '๐Ÿ”ฅ' in content, "Card should have fire emoji for current streak" + assert '๐Ÿ†' in content, "Card should have trophy emoji for best streak" + assert 'habit.streak' in content or 'streak?.current' in content or 'streak.current' in content, \ + "Card should display streak.current" + assert 'streak?.best' in content or 'streak.best' in content, \ + "Card should display streak.best" + + print("โœ“ Test 13: Streak display with fire and trophy emojis") + +def test_card_checkin_button(): + """Test 14: Check-in button is large and centered""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + assert 'habit-card-check-btn' in content or 'check-btn' in content or 'checkin' in content.lower(), \ + "Card should have check-in button" + assert 'Check In' in content or 'Check in' in content, \ + "Button should have 'Check In' text" + + # Check for button styling (large/centered) + assert 'width: 100%' in content or 'width:100%' in content, \ + "Check-in button should be full-width" + + print("โœ“ Test 14: Check-in button is large and centered") + +def test_card_checkin_disabled_when_done(): + """Test 15: Check-in button disabled when already checked today""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + assert 'disabled' in content, "Button should have disabled state" + assert 'Done today' in content or 'Done' in content, \ + "Button should show 'Done today' when disabled" + assert 'isCheckedToday' in content or 'isDoneToday' in content, \ + "Should have function to check if habit is done today" + + print("โœ“ Test 15: Check-in button disabled when done today") + +def test_card_lives_display(): + """Test 16: Lives display shows filled and empty hearts (total 3)""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + assert 'โค๏ธ' in content or 'โ™ฅ' in content, "Card should have filled heart emoji" + assert '๐Ÿ–ค' in content or 'โ™ก' in content, "Card should have empty heart emoji" + assert 'habit.lives' in content or 'renderLives' in content, \ + "Card should display lives" + + # Check for lives rendering function + assert 'renderLives' in content or 'lives' in content.lower(), \ + "Should have lives rendering logic" + + print("โœ“ Test 16: Lives display with hearts") + +def test_card_completion_rate(): + """Test 17: Completion rate percentage is displayed""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + assert 'completion_rate' in content or 'completion' in content, \ + "Card should display completion rate" + assert '(30d)' in content or '30d' in content, \ + "Completion rate should show 30-day period" + assert '%' in content, "Completion rate should show percentage" + + print("โœ“ Test 17: Completion rate displayed") + +def test_card_footer_category_priority(): + """Test 18: Footer shows category badge and priority""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + assert 'habit.category' in content or 'habit-card-category' in content, \ + "Card should display category" + assert 'habit.priority' in content or 'priority' in content.lower(), \ + "Card should display priority" + assert 'habit-card-footer' in content or 'footer' in content.lower(), \ + "Card should have footer section" + + print("โœ“ Test 18: Footer shows category and priority") + +def test_card_lucide_createicons(): + """Test 19: lucide.createIcons() is called after rendering cards""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check that createIcons is called after rendering + render_pos = content.find('renderHabits') + if render_pos != -1: + after_render = content[render_pos:] + assert 'lucide.createIcons()' in after_render, \ + "lucide.createIcons() should be called after rendering" + + print("โœ“ Test 19: lucide.createIcons() called after rendering") + +def test_card_common_css_variables(): + """Test 20: Card uses common.css variables for styling""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check for common.css variable usage + assert '--bg-surface' in content or '--text-primary' in content or '--border' in content, \ + "Card should use common.css variables" + assert 'var(--' in content, "Should use CSS variables" + + print("โœ“ Test 20: Card uses common.css variables") + +def test_typecheck_us007(): + """Test 21: Typecheck passes for US-007""" + habits_path = Path(__file__).parent.parent / 'habits.html' + assert habits_path.exists(), "habits.html should exist" + + # Check that all functions are properly defined + content = habits_path.read_text() + assert 'function renderHabitCard(' in content, "renderHabitCard function should be defined" + assert 'function isCheckedToday(' in content, "isCheckedToday function should be defined" + assert 'function getLastCheckInfo(' in content, "getLastCheckInfo function should be defined" + assert 'function renderLives(' in content, "renderLives function should be defined" + assert 'function getPriorityLevel(' in content, "getPriorityLevel function should be defined" + + print("โœ“ Test 21: Typecheck passes (all functions defined)") + def run_all_tests(): """Run all tests in sequence""" tests = [ + # US-006 tests test_habits_html_exists, test_habits_html_structure, test_page_has_header, @@ -162,9 +322,21 @@ def run_all_tests(): test_habit_card_rendering, test_no_console_errors_structure, test_typecheck, + # US-007 tests + test_card_colored_border, + test_card_header_icons, + test_card_streak_display, + test_card_checkin_button, + test_card_checkin_disabled_when_done, + test_card_lives_display, + test_card_completion_rate, + test_card_footer_category_priority, + test_card_lucide_createicons, + test_card_common_css_variables, + test_typecheck_us007, ] - print(f"\nRunning {len(tests)} frontend tests for US-006...\n") + print(f"\nRunning {len(tests)} frontend tests for US-006 and US-007...\n") failed = [] for test in tests: