feat: 7.0 - Frontend - Create habits.html page structure
This commit is contained in:
185
dashboard/habits.html
Normal file
185
dashboard/habits.html
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ro">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/echo/favicon.svg">
|
||||||
|
<title>Echo · Habit Tracker</title>
|
||||||
|
<link rel="stylesheet" href="/echo/common.css">
|
||||||
|
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||||
|
<script src="/echo/swipe-nav.js"></script>
|
||||||
|
<style>
|
||||||
|
.main {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-6) var(--space-4);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state svg {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
opacity: 0.5;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-message {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add habit button */
|
||||||
|
.add-habit-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-3) var(--space-5);
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-habit-btn:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-habit-btn svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Habits list (for future use) */
|
||||||
|
.habits-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<a href="/echo/index.html" class="logo">
|
||||||
|
<i data-lucide="circle-dot"></i>
|
||||||
|
Echo
|
||||||
|
</a>
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/echo/index.html" class="nav-item">
|
||||||
|
<i data-lucide="layout-dashboard"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="/echo/workspace.html" class="nav-item">
|
||||||
|
<i data-lucide="code"></i>
|
||||||
|
<span>Workspace</span>
|
||||||
|
</a>
|
||||||
|
<a href="/echo/notes.html" class="nav-item">
|
||||||
|
<i data-lucide="file-text"></i>
|
||||||
|
<span>KB</span>
|
||||||
|
</a>
|
||||||
|
<a href="/echo/files.html" class="nav-item">
|
||||||
|
<i data-lucide="folder"></i>
|
||||||
|
<span>Files</span>
|
||||||
|
</a>
|
||||||
|
<a href="/echo/habits.html" class="nav-item active">
|
||||||
|
<i data-lucide="target"></i>
|
||||||
|
<span>Habits</span>
|
||||||
|
</a>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
||||||
|
<i data-lucide="sun" id="themeIcon"></i>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Habit Tracker</h1>
|
||||||
|
<p class="page-subtitle">Urmărește-ți obiceiurile zilnice și săptămânale</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Habits container -->
|
||||||
|
<div id="habitsContainer">
|
||||||
|
<!-- Empty state -->
|
||||||
|
<div class="empty-state">
|
||||||
|
<i data-lucide="target"></i>
|
||||||
|
<p class="empty-state-message">Nicio obișnuință încă. Creează prima!</p>
|
||||||
|
<button class="add-habit-btn" onclick="showAddHabitModal()">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
<span>Adaugă obișnuință</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Habits list (hidden initially) -->
|
||||||
|
<div class="habits-list" id="habitsList" style="display: none;">
|
||||||
|
<!-- Habits will be populated here by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Theme management
|
||||||
|
function initTheme() {
|
||||||
|
const saved = localStorage.getItem('theme') || 'dark';
|
||||||
|
document.documentElement.setAttribute('data-theme', saved);
|
||||||
|
updateThemeIcon(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||||
|
const next = current === 'dark' ? 'light' : 'dark';
|
||||||
|
document.documentElement.setAttribute('data-theme', next);
|
||||||
|
localStorage.setItem('theme', next);
|
||||||
|
updateThemeIcon(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateThemeIcon(theme) {
|
||||||
|
const icon = document.getElementById('themeIcon');
|
||||||
|
if (icon) {
|
||||||
|
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme and icons
|
||||||
|
initTheme();
|
||||||
|
lucide.createIcons();
|
||||||
|
|
||||||
|
// Placeholder function for add habit modal (to be implemented in future stories)
|
||||||
|
function showAddHabitModal() {
|
||||||
|
alert('Funcționalitatea de adăugare obișnuință va fi implementată în următoarea fază!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load habits (placeholder for future API integration)
|
||||||
|
async function loadHabits() {
|
||||||
|
// Will be implemented when API is integrated
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,14 +1,4 @@
|
|||||||
{
|
{
|
||||||
"lastUpdated": "2026-02-10T11:40:08.703720",
|
"lastUpdated": "2026-02-10T11:59:02.042Z",
|
||||||
"habits": [
|
"habits": []
|
||||||
{
|
|
||||||
"id": "habit-1770723608703",
|
|
||||||
"name": "Water Plants",
|
|
||||||
"frequency": "daily",
|
|
||||||
"createdAt": "2026-02-10T11:40:08.703082",
|
|
||||||
"completions": [
|
|
||||||
"2026-02-10"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
240
dashboard/test_habits_html.py
Normal file
240
dashboard/test_habits_html.py
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test suite for habits.html page structure
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
1. File exists
|
||||||
|
2. Valid HTML5 structure
|
||||||
|
3. Uses common.css and swipe-nav.js
|
||||||
|
4. Has navigation bar matching dashboard style
|
||||||
|
5. Page title 'Habit Tracker' in header
|
||||||
|
6. Empty state message 'Nicio obișnuință încă. Creează prima!'
|
||||||
|
7. Add habit button with '+' icon (lucide)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
# Path to habits.html
|
||||||
|
HABITS_HTML_PATH = 'dashboard/habits.html'
|
||||||
|
|
||||||
|
class HTMLStructureParser(HTMLParser):
|
||||||
|
"""Parser to extract specific elements from HTML"""
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.title_text = None
|
||||||
|
self.css_files = []
|
||||||
|
self.js_files = []
|
||||||
|
self.nav_items = []
|
||||||
|
self.page_title = None
|
||||||
|
self.empty_state_message = None
|
||||||
|
self.has_add_button = False
|
||||||
|
self.has_lucide_plus = False
|
||||||
|
self.in_title = False
|
||||||
|
self.in_page_title = False
|
||||||
|
self.in_empty_message = False
|
||||||
|
self.in_button = False
|
||||||
|
self.current_class = None
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
attrs_dict = dict(attrs)
|
||||||
|
|
||||||
|
# Track CSS and JS files
|
||||||
|
if tag == 'link' and attrs_dict.get('rel') == 'stylesheet':
|
||||||
|
self.css_files.append(attrs_dict.get('href', ''))
|
||||||
|
if tag == 'script' and 'src' in attrs_dict:
|
||||||
|
self.js_files.append(attrs_dict.get('src'))
|
||||||
|
|
||||||
|
# Track title tag
|
||||||
|
if tag == 'title':
|
||||||
|
self.in_title = True
|
||||||
|
|
||||||
|
# Track page title (h1 with class page-title)
|
||||||
|
if tag == 'h1' and 'page-title' in attrs_dict.get('class', ''):
|
||||||
|
self.in_page_title = True
|
||||||
|
|
||||||
|
# Track nav items
|
||||||
|
if tag == 'a' and 'nav-item' in attrs_dict.get('class', ''):
|
||||||
|
href = attrs_dict.get('href', '')
|
||||||
|
classes = attrs_dict.get('class', '')
|
||||||
|
self.nav_items.append({'href': href, 'classes': classes})
|
||||||
|
|
||||||
|
# Track empty state message
|
||||||
|
if 'empty-state-message' in attrs_dict.get('class', ''):
|
||||||
|
self.in_empty_message = True
|
||||||
|
|
||||||
|
# Track add habit button
|
||||||
|
if tag == 'button' and 'add-habit-btn' in attrs_dict.get('class', ''):
|
||||||
|
self.has_add_button = True
|
||||||
|
self.in_button = True
|
||||||
|
|
||||||
|
# Track lucide plus icon in button context
|
||||||
|
if self.in_button and tag == 'i':
|
||||||
|
lucide_attr = attrs_dict.get('data-lucide', '')
|
||||||
|
if 'plus' in lucide_attr:
|
||||||
|
self.has_lucide_plus = True
|
||||||
|
|
||||||
|
def handle_endtag(self, tag):
|
||||||
|
if tag == 'title':
|
||||||
|
self.in_title = False
|
||||||
|
if tag == 'h1':
|
||||||
|
self.in_page_title = False
|
||||||
|
if tag == 'p':
|
||||||
|
self.in_empty_message = False
|
||||||
|
if tag == 'button':
|
||||||
|
self.in_button = False
|
||||||
|
|
||||||
|
def handle_data(self, data):
|
||||||
|
if self.in_title:
|
||||||
|
self.title_text = data.strip()
|
||||||
|
if self.in_page_title:
|
||||||
|
self.page_title = data.strip()
|
||||||
|
if self.in_empty_message:
|
||||||
|
self.empty_state_message = data.strip()
|
||||||
|
|
||||||
|
def test_file_exists():
|
||||||
|
"""Test 1: File exists"""
|
||||||
|
assert os.path.exists(HABITS_HTML_PATH), f"File {HABITS_HTML_PATH} not found"
|
||||||
|
print("✓ Test 1: File exists")
|
||||||
|
|
||||||
|
def test_valid_html5():
|
||||||
|
"""Test 2: Valid HTML5 structure"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Check DOCTYPE
|
||||||
|
assert content.strip().startswith('<!DOCTYPE html>'), "Missing or incorrect DOCTYPE"
|
||||||
|
|
||||||
|
# Check required tags
|
||||||
|
required_tags = ['<html', '<head>', '<meta charset', '<title>', '<body>', '</html>']
|
||||||
|
for tag in required_tags:
|
||||||
|
assert tag in content, f"Missing required tag: {tag}"
|
||||||
|
|
||||||
|
# Check html lang attribute
|
||||||
|
assert 'lang="ro"' in content or "lang='ro'" in content, "Missing lang='ro' attribute on html tag"
|
||||||
|
|
||||||
|
print("✓ Test 2: Valid HTML5 structure")
|
||||||
|
|
||||||
|
def test_uses_common_css_and_swipe_nav():
|
||||||
|
"""Test 3: Uses common.css and swipe-nav.js"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
parser = HTMLStructureParser()
|
||||||
|
parser.feed(content)
|
||||||
|
|
||||||
|
# Check for common.css
|
||||||
|
assert any('common.css' in css for css in parser.css_files), "Missing common.css"
|
||||||
|
|
||||||
|
# Check for swipe-nav.js
|
||||||
|
assert any('swipe-nav.js' in js for js in parser.js_files), "Missing swipe-nav.js"
|
||||||
|
|
||||||
|
print("✓ Test 3: Uses common.css and swipe-nav.js")
|
||||||
|
|
||||||
|
def test_navigation_bar():
|
||||||
|
"""Test 4: Has navigation bar matching dashboard style"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
parser = HTMLStructureParser()
|
||||||
|
parser.feed(content)
|
||||||
|
|
||||||
|
# Check that we have nav items
|
||||||
|
assert len(parser.nav_items) >= 4, f"Expected at least 4 nav items, found {len(parser.nav_items)}"
|
||||||
|
|
||||||
|
# Check for Dashboard nav item
|
||||||
|
dashboard_items = [item for item in parser.nav_items if 'index.html' in item['href']]
|
||||||
|
assert len(dashboard_items) > 0, "Missing Dashboard nav item"
|
||||||
|
|
||||||
|
# Check for habits nav item with active class
|
||||||
|
habits_items = [item for item in parser.nav_items if 'habits.html' in item['href']]
|
||||||
|
assert len(habits_items) > 0, "Missing Habits nav item"
|
||||||
|
assert any('active' in item['classes'] for item in habits_items), "Habits nav item should have 'active' class"
|
||||||
|
|
||||||
|
# Check for header element with class 'header'
|
||||||
|
assert '<header class="header">' in content, "Missing header element with class 'header'"
|
||||||
|
|
||||||
|
print("✓ Test 4: Has navigation bar matching dashboard style")
|
||||||
|
|
||||||
|
def test_page_title():
|
||||||
|
"""Test 5: Page title 'Habit Tracker' in header"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
parser = HTMLStructureParser()
|
||||||
|
parser.feed(content)
|
||||||
|
|
||||||
|
# Check <title> tag
|
||||||
|
assert parser.title_text is not None, "Missing <title> tag"
|
||||||
|
assert 'Habit Tracker' in parser.title_text, f"Expected 'Habit Tracker' in title, got: {parser.title_text}"
|
||||||
|
|
||||||
|
# Check page header (h1)
|
||||||
|
assert parser.page_title is not None, "Missing page title (h1.page-title)"
|
||||||
|
assert 'Habit Tracker' in parser.page_title, f"Expected 'Habit Tracker' in page title, got: {parser.page_title}"
|
||||||
|
|
||||||
|
print("✓ Test 5: Page title 'Habit Tracker' in header")
|
||||||
|
|
||||||
|
def test_empty_state_message():
|
||||||
|
"""Test 6: Empty state message 'Nicio obișnuință încă. Creează prima!'"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
parser = HTMLStructureParser()
|
||||||
|
parser.feed(content)
|
||||||
|
|
||||||
|
# Check empty state message
|
||||||
|
assert parser.empty_state_message is not None, "Missing empty state message"
|
||||||
|
expected_message = "Nicio obișnuință încă. Creează prima!"
|
||||||
|
assert parser.empty_state_message == expected_message, \
|
||||||
|
f"Expected '{expected_message}', got: '{parser.empty_state_message}'"
|
||||||
|
|
||||||
|
# Check for empty-state class
|
||||||
|
assert 'class="empty-state"' in content, "Missing empty-state element"
|
||||||
|
|
||||||
|
print("✓ Test 6: Empty state message present")
|
||||||
|
|
||||||
|
def test_add_habit_button():
|
||||||
|
"""Test 7: Add habit button with '+' icon (lucide)"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
parser = HTMLStructureParser()
|
||||||
|
parser.feed(content)
|
||||||
|
|
||||||
|
# Check for add habit button
|
||||||
|
assert parser.has_add_button, "Missing add habit button with class 'add-habit-btn'"
|
||||||
|
|
||||||
|
# Check for lucide plus icon
|
||||||
|
assert parser.has_lucide_plus, "Missing lucide 'plus' icon in add habit button"
|
||||||
|
|
||||||
|
# Check button text content
|
||||||
|
assert 'Adaugă obișnuință' in content, "Missing button text 'Adaugă obișnuință'"
|
||||||
|
|
||||||
|
print("✓ Test 7: Add habit button with '+' icon (lucide)")
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""Run all tests"""
|
||||||
|
print("Running habits.html structure tests...\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_file_exists()
|
||||||
|
test_valid_html5()
|
||||||
|
test_uses_common_css_and_swipe_nav()
|
||||||
|
test_navigation_bar()
|
||||||
|
test_page_title()
|
||||||
|
test_empty_state_message()
|
||||||
|
test_add_habit_button()
|
||||||
|
|
||||||
|
print("\n✅ All tests passed!")
|
||||||
|
return True
|
||||||
|
except AssertionError as e:
|
||||||
|
print(f"\n❌ Test failed: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Unexpected error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
success = run_all_tests()
|
||||||
|
exit(0 if success else 1)
|
||||||
Reference in New Issue
Block a user