Feature: Habit Tracker with Streak Calculation #1

Closed
Marius wants to merge 26 commits from feature/habit-tracker into master
16 changed files with 4226 additions and 0 deletions
Showing only changes of commit c1d4ed1b03 - Show all commits

View File

@@ -87,7 +87,7 @@
.habit-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
border-radius: var(--radius-lg);
padding: var(--space-4);
display: flex;
align-items: center;
@@ -100,6 +100,10 @@
transform: translateY(-1px);
}
.habit-card.checked {
background: rgba(34, 197, 94, 0.1);
}
.habit-icon {
width: 40px;
height: 40px;
@@ -129,8 +133,13 @@
}
.habit-frequency {
display: inline-block;
font-size: var(--text-xs);
color: var(--text-muted);
color: var(--text-secondary);
background: var(--bg-elevated);
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: var(--radius-sm);
text-transform: capitalize;
}
@@ -138,7 +147,7 @@
display: flex;
align-items: center;
gap: var(--space-1);
font-size: var(--text-lg);
font-size: var(--text-xl);
font-weight: 600;
color: var(--accent);
flex-shrink: 0;
@@ -162,6 +171,12 @@
.habit-checkbox:hover:not(.disabled) {
border-color: var(--accent);
background: var(--accent-light, rgba(99, 102, 241, 0.1));
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.habit-checkbox.checked {
@@ -343,6 +358,41 @@
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.main {
padding: var(--space-3);
}
.habit-card {
width: 100%;
}
.habits-list {
width: 100%;
}
.habit-icon {
width: 36px;
height: 36px;
}
.habit-icon svg {
width: 18px;
height: 18px;
}
.habit-checkbox {
width: 28px;
height: 28px;
}
.habit-checkbox svg {
width: 16px;
height: 16px;
}
}
</style>
</head>
<body>
@@ -627,13 +677,13 @@
// Create habit card element
function createHabitCard(habit) {
const card = document.createElement('div');
card.className = 'habit-card';
const isChecked = habit.checkedToday || false;
card.className = isChecked ? 'habit-card checked' : 'habit-card';
// 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>' : '';
@@ -677,6 +727,12 @@
checkboxElement.innerHTML = '<i data-lucide="check"></i>';
lucide.createIcons();
// Add 'checked' class to parent card for green background
const card = checkboxElement.closest('.habit-card');
if (card) {
card.classList.add('checked');
}
// Store original state for rollback
const originalCheckbox = checkboxElement.cloneNode(true);
const streakElement = document.getElementById(`streak-${habitId}`);
@@ -708,6 +764,12 @@
checkboxElement.classList.remove('checked', 'disabled');
checkboxElement.innerHTML = '';
// Revert card background
const card = checkboxElement.closest('.habit-card');
if (card) {
card.classList.remove('checked');
}
// Revert streak
if (streakElement) {
streakElement.textContent = originalStreak;

View File

@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""
Tests for Story 12.0: Frontend - Habit card styling
Tests styling enhancements for habit cards
"""
def test_file_exists():
"""Test that habits.html exists"""
import os
assert os.path.exists('dashboard/habits.html'), "habits.html file should exist"
print("✓ habits.html exists")
def test_card_border_radius():
"""Test that cards use --radius-lg border radius"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Check that habit-card has border-radius: var(--radius-lg)
assert 'border-radius: var(--radius-lg);' in content, "habit-card should use --radius-lg border radius"
# Check it's in the .habit-card CSS rule
habit_card_start = content.find('.habit-card {')
habit_card_end = content.find('}', habit_card_start)
habit_card_css = content[habit_card_start:habit_card_end]
assert 'border-radius: var(--radius-lg)' in habit_card_css, "habit-card should have --radius-lg in its CSS"
print("✓ Cards use --radius-lg border radius")
def test_streak_font_size():
"""Test that streak uses --text-xl font size"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Find .habit-streak CSS rule
streak_start = content.find('.habit-streak {')
assert streak_start > 0, ".habit-streak CSS rule should exist"
streak_end = content.find('}', streak_start)
streak_css = content[streak_start:streak_end]
# Check for font-size: var(--text-xl)
assert 'font-size: var(--text-xl)' in streak_css, "Streak should use --text-xl font size"
print("✓ Streak displayed prominently with --text-xl font size")
def test_checked_habit_background():
"""Test that checked habits have subtle green background tint"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Check for .habit-card.checked CSS rule
assert '.habit-card.checked' in content, "Should have .habit-card.checked CSS rule"
# Find the CSS rule
checked_start = content.find('.habit-card.checked {')
assert checked_start > 0, ".habit-card.checked CSS rule should exist"
checked_end = content.find('}', checked_start)
checked_css = content[checked_start:checked_end]
# Check for green background (using rgba with green color and low opacity)
assert 'background: rgba(34, 197, 94, 0.1)' in checked_css, "Checked cards should have green background tint"
# Check JavaScript adds 'checked' class to card
assert "card.classList.add('checked')" in content, "JavaScript should add 'checked' class to card"
print("✓ Checked habits have subtle green background tint")
def test_checkbox_pulse_animation():
"""Test that unchecked habits have subtle pulse animation on checkbox hover"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Check for animation on hover (not disabled)
hover_start = content.find('.habit-checkbox:hover:not(.disabled) {')
assert hover_start > 0, "Should have hover rule for unchecked checkboxes"
hover_end = content.find('}', hover_start)
hover_css = content[hover_start:hover_end]
# Check for pulse animation
assert 'animation: pulse' in hover_css, "Unchecked checkboxes should have pulse animation on hover"
# Check for @keyframes pulse definition
assert '@keyframes pulse' in content, "Should have pulse keyframes definition"
# Verify pulse animation scales element
keyframes_start = content.find('@keyframes pulse {')
keyframes_end = content.find('}', keyframes_start)
keyframes_css = content[keyframes_start:keyframes_end]
assert 'scale(' in keyframes_css, "Pulse animation should scale the element"
print("✓ Unchecked habits have subtle pulse animation on checkbox hover")
def test_frequency_badge_styling():
"""Test that frequency badge uses dashboard tag styling"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Find .habit-frequency CSS rule
freq_start = content.find('.habit-frequency {')
assert freq_start > 0, ".habit-frequency CSS rule should exist"
freq_end = content.find('}', freq_start)
freq_css = content[freq_start:freq_end]
# Check for tag-like styling
assert 'display: inline-block' in freq_css, "Frequency should be inline-block"
assert 'background: var(--bg-elevated)' in freq_css, "Frequency should use --bg-elevated"
assert 'border: 1px solid var(--border)' in freq_css, "Frequency should have border"
assert 'padding:' in freq_css, "Frequency should have padding"
assert 'border-radius:' in freq_css, "Frequency should have border-radius"
print("✓ Frequency badge uses dashboard tag styling")
def test_card_uses_css_variables():
"""Test that cards use --bg-surface with --border"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Find .habit-card CSS rule
card_start = content.find('.habit-card {')
assert card_start > 0, ".habit-card CSS rule should exist"
card_end = content.find('}', card_start)
card_css = content[card_start:card_end]
# Check for CSS variables
assert 'background: var(--bg-surface)' in card_css, "Cards should use --bg-surface"
assert 'border: 1px solid var(--border)' in card_css, "Cards should use --border"
print("✓ Cards use --bg-surface with --border")
def test_mobile_responsiveness():
"""Test that cards are responsive on mobile (full width < 768px)"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Check for media query
assert '@media (max-width: 768px)' in content, "Should have mobile media query"
# Find mobile media query
mobile_start = content.find('@media (max-width: 768px)')
assert mobile_start > 0, "Mobile media query should exist"
mobile_end = content.find('}', content.find('}', content.find('}', mobile_start) + 1) + 1)
mobile_css = content[mobile_start:mobile_end]
# Check for habit-card width
assert '.habit-card {' in mobile_css or 'habit-card' in mobile_css, "Mobile styles should target habit-card"
assert 'width: 100%' in mobile_css, "Cards should be full width on mobile"
# Check for reduced spacing
assert '.main {' in mobile_css, "Main container should have mobile styling"
print("✓ Responsive on mobile (full width < 768px)")
def test_checked_class_in_createHabitCard():
"""Test that createHabitCard adds 'checked' class to card"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Find createHabitCard function
func_start = content.find('function createHabitCard(habit) {')
assert func_start > 0, "createHabitCard function should exist"
func_end = content.find('return card;', func_start)
func_code = content[func_start:func_end]
# Check for checked class logic
assert "isChecked ? 'habit-card checked' : 'habit-card'" in func_code, "Should add 'checked' class to card when habit is checked"
print("✓ createHabitCard adds 'checked' class when appropriate")
def test_all_acceptance_criteria():
"""Summary test: verify all 7 acceptance criteria are met"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
criteria = []
# 1. Cards use --bg-surface with --border
card_start = content.find('.habit-card {')
card_end = content.find('}', card_start)
card_css = content[card_start:card_end]
if 'background: var(--bg-surface)' in card_css and 'border: 1px solid var(--border)' in card_css:
criteria.append("✓ Cards use --bg-surface with --border")
# 2. Streak displayed prominently with --text-xl
streak_start = content.find('.habit-streak {')
streak_end = content.find('}', streak_start)
streak_css = content[streak_start:streak_end]
if 'font-size: var(--text-xl)' in streak_css:
criteria.append("✓ Streak displayed prominently with --text-xl")
# 3. Checked habits have subtle green background tint
if '.habit-card.checked' in content and 'rgba(34, 197, 94, 0.1)' in content:
criteria.append("✓ Checked habits have subtle green background tint")
# 4. Unchecked habits have subtle pulse animation on checkbox hover
if 'animation: pulse' in content and '@keyframes pulse' in content:
criteria.append("✓ Unchecked habits have pulse animation on hover")
# 5. Frequency badge uses dashboard tag styling
freq_start = content.find('.habit-frequency {')
freq_end = content.find('}', freq_start)
freq_css = content[freq_start:freq_end]
if 'display: inline-block' in freq_css and 'background: var(--bg-elevated)' in freq_css:
criteria.append("✓ Frequency badge uses dashboard tag styling")
# 6. Cards have --radius-lg border radius
if 'border-radius: var(--radius-lg)' in card_css:
criteria.append("✓ Cards have --radius-lg border radius")
# 7. Responsive on mobile (full width < 768px)
if '@media (max-width: 768px)' in content and 'width: 100%' in content[content.find('@media (max-width: 768px)'):]:
criteria.append("✓ Responsive on mobile (full width < 768px)")
for criterion in criteria:
print(criterion)
assert len(criteria) == 7, f"Should meet all 7 acceptance criteria, met {len(criteria)}"
print(f"\n✓ All 7 acceptance criteria met!")
if __name__ == '__main__':
import os
os.chdir('/home/moltbot/clawd')
tests = [
test_file_exists,
test_card_uses_css_variables,
test_card_border_radius,
test_streak_font_size,
test_checked_habit_background,
test_checkbox_pulse_animation,
test_frequency_badge_styling,
test_mobile_responsiveness,
test_checked_class_in_createHabitCard,
test_all_acceptance_criteria
]
print("Running tests for Story 12.0: Frontend - Habit card styling\n")
for test in tests:
try:
test()
except AssertionError as e:
print(f"{test.__name__} failed: {e}")
exit(1)
except Exception as e:
print(f"{test.__name__} error: {e}")
exit(1)
print(f"\n{'='*60}")
print(f"All {len(tests)} tests passed! ✓")
print(f"{'='*60}")