feat: US-012 - Frontend - Filter and sort controls
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user