feat: US-012 - Frontend - Filter and sort controls

This commit is contained in:
Echo
2026-02-10 17:15:17 +00:00
parent 8897de25ed
commit b99c13a325
3 changed files with 354 additions and 2 deletions

View File

@@ -30,6 +30,64 @@
color: var(--text-primary);
}
/* Filter bar */
.filter-bar {
display: flex;
gap: var(--space-3);
flex-wrap: wrap;
margin-bottom: var(--space-4);
padding: var(--space-3);
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
.filter-group {
display: flex;
flex-direction: column;
gap: var(--space-1);
min-width: 150px;
}
.filter-label {
font-size: var(--text-xs);
font-weight: 500;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.filter-select {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-primary);
color: var(--text-primary);
font-size: var(--text-sm);
cursor: pointer;
transition: all var(--transition-base);
}
.filter-select:hover {
border-color: var(--accent);
}
.filter-select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-alpha);
}
@media (max-width: 768px) {
.filter-bar {
flex-direction: column;
}
.filter-group {
width: 100%;
}
}
/* Habits grid */
.habits-grid {
display: grid;
@@ -661,6 +719,41 @@
</button>
</div>
<div class="filter-bar">
<div class="filter-group">
<label class="filter-label">Category</label>
<select id="categoryFilter" class="filter-select" onchange="applyFiltersAndSort()">
<option value="all">All</option>
<option value="work">Work</option>
<option value="health">Health</option>
<option value="growth">Growth</option>
<option value="personal">Personal</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Status</label>
<select id="statusFilter" class="filter-select" onchange="applyFiltersAndSort()">
<option value="all">All</option>
<option value="active_today">Active Today</option>
<option value="done_today">Done Today</option>
<option value="overdue">Overdue</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Sort By</label>
<select id="sortSelect" class="filter-select" onchange="applyFiltersAndSort()">
<option value="priority_asc">Priority (Low to High)</option>
<option value="priority_desc">Priority (High to Low)</option>
<option value="name_asc">Name A-Z</option>
<option value="name_desc">Name Z-A</option>
<option value="streak_desc">Streak (Highest)</option>
<option value="streak_asc">Streak (Lowest)</option>
</select>
</div>
</div>
<div id="habitsContainer">
<div class="empty-state">
<i data-lucide="loader"></i>
@@ -847,6 +940,93 @@
}
}
// Apply filters and sort
function applyFiltersAndSort() {
const categoryFilter = document.getElementById('categoryFilter').value;
const statusFilter = document.getElementById('statusFilter').value;
const sortSelect = document.getElementById('sortSelect').value;
// Save to localStorage
localStorage.setItem('habitCategoryFilter', categoryFilter);
localStorage.setItem('habitStatusFilter', statusFilter);
localStorage.setItem('habitSort', sortSelect);
renderHabits();
}
// Restore filters from localStorage
function restoreFilters() {
const categoryFilter = localStorage.getItem('habitCategoryFilter') || 'all';
const statusFilter = localStorage.getItem('habitStatusFilter') || 'all';
const sortSelect = localStorage.getItem('habitSort') || 'priority_asc';
document.getElementById('categoryFilter').value = categoryFilter;
document.getElementById('statusFilter').value = statusFilter;
document.getElementById('sortSelect').value = sortSelect;
}
// Filter habits based on selected filters
function filterHabits(habits) {
const categoryFilter = document.getElementById('categoryFilter').value;
const statusFilter = document.getElementById('statusFilter').value;
const today = new Date();
today.setHours(0, 0, 0, 0);
return habits.filter(habit => {
// Category filter
if (categoryFilter !== 'all' && habit.category !== categoryFilter) {
return false;
}
// Status filter
if (statusFilter !== 'all') {
const isDoneToday = isCheckedToday(habit);
const shouldCheckToday = habit.should_check_today; // Added by backend in GET endpoint
if (statusFilter === 'active_today' && !shouldCheckToday) {
return false;
}
if (statusFilter === 'done_today' && !isDoneToday) {
return false;
}
if (statusFilter === 'overdue' && (!shouldCheckToday || isDoneToday)) {
return false;
}
}
return true;
});
}
// Sort habits based on selected sort option
function sortHabits(habits) {
const sortOption = document.getElementById('sortSelect').value;
const sorted = [...habits];
switch (sortOption) {
case 'priority_asc':
sorted.sort((a, b) => (a.priority || 50) - (b.priority || 50));
break;
case 'priority_desc':
sorted.sort((a, b) => (b.priority || 50) - (a.priority || 50));
break;
case 'name_asc':
sorted.sort((a, b) => a.name.localeCompare(b.name));
break;
case 'name_desc':
sorted.sort((a, b) => b.name.localeCompare(a.name));
break;
case 'streak_desc':
sorted.sort((a, b) => (b.current_streak || 0) - (a.current_streak || 0));
break;
case 'streak_asc':
sorted.sort((a, b) => (a.current_streak || 0) - (b.current_streak || 0));
break;
}
return sorted;
}
// Render habits grid
function renderHabits() {
const container = document.getElementById('habitsContainer');
@@ -863,7 +1043,23 @@
return;
}
const habitsHtml = habits.map(habit => renderHabitCard(habit)).join('');
// Apply filters and sort
let filteredHabits = filterHabits(habits);
let sortedHabits = sortHabits(filteredHabits);
if (sortedHabits.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i data-lucide="filter"></i>
<p>No habits match your filters</p>
<p class="hint">Try adjusting the filters above</p>
</div>
`;
lucide.createIcons();
return;
}
const habitsHtml = sortedHabits.map(habit => renderHabitCard(habit)).join('');
container.innerHTML = `<div class="habits-grid">${habitsHtml}</div>`;
lucide.createIcons();
@@ -1662,6 +1858,7 @@
// Initialize page
lucide.createIcons();
restoreFilters();
loadHabits();
</script>
</body>