Skip to content

Commit e87b819

Browse files
committed
feat: Allow filtering/sorting by priority and tag
1 parent 602b115 commit e87b819

4 files changed

Lines changed: 320 additions & 65 deletions

File tree

index.html

Lines changed: 114 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -328,63 +328,125 @@ <h2 class="lobster text-center mb-3 reveal-on-scroll">
328328
</h2>
329329

330330
<!-- Tasks Filters -->
331-
<div
332-
class="tasks-filters btn-group d-flex justify-content-center"
333-
role="group"
334-
aria-label="Filter tasks"
335-
>
336-
<button
337-
type="button"
338-
class="btn btn-outline-primary active"
339-
data-filter="all"
331+
<div class="tasks-filters-wrapper">
332+
<div
333+
class="tasks-filters btn-group d-flex justify-content-center"
334+
role="group"
335+
aria-label="Filter tasks"
340336
>
341-
<span class="filter-text">All</span>
342-
<span
343-
class="badge text-bg-secondary filter-badge"
344-
data-status="all"
337+
<button
338+
type="button"
339+
class="btn btn-outline-primary active"
340+
data-filter="all"
345341
>
346-
0
347-
</span>
348-
</button>
349-
<button
350-
type="button"
351-
class="btn btn-outline-primary"
352-
data-filter="pending"
353-
>
354-
<span class="filter-text">Pending</span>
355-
<span
356-
class="badge text-bg-primary filter-badge"
357-
data-status="pending"
342+
<span class="filter-text">All</span>
343+
<span
344+
class="badge text-bg-secondary filter-badge"
345+
data-status="all"
346+
>
347+
0
348+
</span>
349+
</button>
350+
<button
351+
type="button"
352+
class="btn btn-outline-primary"
353+
data-filter="pending"
358354
>
359-
0
360-
</span>
361-
</button>
362-
<button
363-
type="button"
364-
class="btn btn-outline-primary"
365-
data-filter="completed"
366-
>
367-
<span class="filter-text">Completed</span>
368-
<span
369-
class="badge text-bg-success filter-badge"
370-
data-status="completed"
355+
<span class="filter-text">Pending</span>
356+
<span
357+
class="badge text-bg-primary filter-badge"
358+
data-status="pending"
359+
>
360+
0
361+
</span>
362+
</button>
363+
<button
364+
type="button"
365+
class="btn btn-outline-primary"
366+
data-filter="completed"
371367
>
372-
0
373-
</span>
374-
</button>
375-
<button
376-
type="button"
377-
class="btn btn-outline-primary"
378-
data-filter="overdue"
379-
>
380-
<span class="filter-text">Overdue</span>
381-
<span
382-
class="badge rounded-pill text-bg-danger ms-1 filter-badge"
383-
data-status="overdue"
368+
<span class="filter-text">Completed</span>
369+
<span
370+
class="badge text-bg-success filter-badge"
371+
data-status="completed"
372+
>
373+
0
374+
</span>
375+
</button>
376+
<button
377+
type="button"
378+
class="btn btn-outline-primary"
379+
data-filter="overdue"
384380
>
385-
0
386-
</span>
387-
</button>
381+
<span class="filter-text">Overdue</span>
382+
<span
383+
class="badge rounded-pill text-bg-danger ms-1 filter-badge"
384+
data-status="overdue"
385+
>
386+
0
387+
</span>
388+
</button>
389+
</div>
390+
391+
<!-- Advanced filters toggle -->
392+
<div class="text-center mt-2">
393+
<button
394+
type="button"
395+
id="toggle-advanced-filters"
396+
class="btn btn-link p-0 small advanced-filters-toggle d-flex align-items-center gap-1 mx-auto"
397+
aria-expanded="false"
398+
>
399+
<span class="advanced-filters-toggle-text">Show advanced filters</span>
400+
<i class="fa-solid fa-angle-down trans-3"></i>
401+
</button>
402+
</div>
403+
404+
<!-- Advanced list filters: priority, tag, sort -->
405+
<div id="advanced-filters-container" class="tasks-advanced-filters row g-2 align-items-center mt-2">
406+
<div class="col-12 col-md-4">
407+
<label
408+
for="filter-priority"
409+
class="form-label form-label-sm mb-1"
410+
>Priority</label
411+
>
412+
<select
413+
id="filter-priority"
414+
class="form-select form-select-sm"
415+
>
416+
<option value="all" selected>All priorities</option>
417+
<option value="high">High</option>
418+
<option value="medium">Medium</option>
419+
<option value="low">Low</option>
420+
</select>
421+
</div>
422+
<div class="col-12 col-md-4">
423+
<label
424+
for="filter-tag"
425+
class="form-label form-label-sm mb-1"
426+
>Tag</label
427+
>
428+
<input
429+
type="text"
430+
id="filter-tag"
431+
class="form-control form-control-sm"
432+
placeholder="e.g. Work"
433+
/>
434+
</div>
435+
<div class="col-12 col-md-4">
436+
<label
437+
for="sort-mode"
438+
class="form-label form-label-sm mb-1"
439+
>Sort by</label
440+
>
441+
<select
442+
id="sort-mode"
443+
class="form-select form-select-sm"
444+
>
445+
<option value="default" selected>Default order</option>
446+
<option value="priority">Priority (High → Low)</option>
447+
</select>
448+
</div>
449+
</div>
388450
</div>
389451

390452
<!-- Weekly completed indicator -->

js/app.js

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ const form = document.getElementById("tasks-form");
2828
const toastsContainer = document.getElementById("notifications-container");
2929
const tasksContainer = document.querySelector(".list-group");
3030
const filterButtons = document.querySelectorAll(".tasks-filters [data-filter]");
31+
const priorityFilterSelect = document.getElementById("filter-priority");
32+
const tagFilterInput = document.getElementById("filter-tag");
33+
const sortModeSelect = document.getElementById("sort-mode");
3134
const themeToggleBtn = document.getElementById("theme-toggle");
3235
const installBtn = document.getElementById("install-btn");
3336
const weeklySummaryEl = document.getElementById("task-weekly-summary");
@@ -37,6 +40,12 @@ const advancedFieldsToggleBtn = document.getElementById(
3740
const advancedFieldsContainer = document.getElementById(
3841
"advanced-task-options",
3942
);
43+
const advancedFiltersToggleBtn = document.getElementById(
44+
"toggle-advanced-filters",
45+
);
46+
const advancedFiltersContainer = document.getElementById(
47+
"advanced-filters-container",
48+
);
4049

4150
// Global Variables
4251
let activeTab = "schedule"; // or: no-time
@@ -97,11 +106,48 @@ if (advancedFieldsToggleBtn && advancedFieldsContainer) {
97106
});
98107
}
99108

100-
// initialize filters module for filter buttons
101-
initFilters(filterButtons, () => {
102-
const tasksToRender = getFilteredTasks();
103-
renderTasksInPage(tasksToRender);
104-
});
109+
// toggle advanced filters visibility
110+
if (advancedFiltersToggleBtn && advancedFiltersContainer) {
111+
// start collapsed
112+
advancedFiltersContainer.classList.remove(
113+
"tasks-advanced-filters--open",
114+
);
115+
advancedFiltersToggleBtn.setAttribute("aria-expanded", "false");
116+
117+
advancedFiltersToggleBtn.addEventListener("click", () => {
118+
const isOpen = advancedFiltersContainer.classList.contains(
119+
"tasks-advanced-filters--open",
120+
);
121+
122+
advancedFiltersContainer.classList.toggle(
123+
"tasks-advanced-filters--open",
124+
!isOpen,
125+
);
126+
127+
advancedFiltersToggleBtn.setAttribute(
128+
"aria-expanded",
129+
!isOpen ? "true" : "false",
130+
);
131+
document.querySelector(".advanced-filters-toggle-text").textContent = !isOpen
132+
? "Hide advanced filters"
133+
: "Show advanced filters";
134+
document.querySelector("#toggle-advanced-filters i").classList.toggle("active", !isOpen);
135+
});
136+
}
137+
138+
// initialize filters module for filter buttons and advanced filters
139+
initFilters(
140+
filterButtons,
141+
() => {
142+
const tasksToRender = getFilteredTasks();
143+
renderTasksInPage(tasksToRender);
144+
},
145+
{
146+
prioritySelect: priorityFilterSelect,
147+
tagInput: tagFilterInput,
148+
sortSelect: sortModeSelect,
149+
},
150+
);
105151

106152
// theme toggle button
107153
if (themeToggleBtn) {

js/filters.js

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
let currentFilter = "all";
44
let cachedTasks = [];
5+
let priorityFilter = "all"; // all, high, medium, low
6+
let tagFilter = ""; // free-text, matches tag labels
7+
let sortMode = "default"; // default, priority
58

6-
// Initialize filter buttons with click handlers
7-
export function initFilters(filterButtons, onFilterChange) {
9+
// Initialize filter buttons and optional extra filter controls
10+
export function initFilters(filterButtons, onFilterChange, extraControls = {}) {
811
if (!filterButtons || typeof filterButtons.forEach !== "function") return;
912

1013
filterButtons.forEach((btn) => {
@@ -30,24 +33,112 @@ export function initFilters(filterButtons, onFilterChange) {
3033
}
3134
});
3235
});
36+
37+
const { prioritySelect, tagInput, sortSelect } = extraControls || {};
38+
39+
if (prioritySelect) {
40+
prioritySelect.addEventListener("change", () => {
41+
const value = (prioritySelect.value || "all").toLowerCase();
42+
priorityFilter = value;
43+
if (typeof onFilterChange === "function") {
44+
onFilterChange();
45+
}
46+
});
47+
}
48+
49+
if (tagInput) {
50+
tagInput.addEventListener("input", () => {
51+
tagFilter = (tagInput.value || "").trim().toLowerCase();
52+
if (typeof onFilterChange === "function") {
53+
onFilterChange();
54+
}
55+
});
56+
}
57+
58+
if (sortSelect) {
59+
sortSelect.addEventListener("change", () => {
60+
const value = sortSelect.value || "default";
61+
sortMode = value;
62+
if (typeof onFilterChange === "function") {
63+
onFilterChange();
64+
}
65+
});
66+
}
3367
}
3468

3569
// Update cached tasks list
3670
export function setTasks(tasks) {
3771
cachedTasks = Array.isArray(tasks) ? tasks : [];
3872
}
3973

40-
// Get tasks filtered by current filter
74+
// Get tasks filtered by current filter / priority / tag, and sorted if needed
4175
export function getFilteredTasks() {
4276
if (!Array.isArray(cachedTasks)) return [];
4377

44-
if (currentFilter === "all") {
45-
return cachedTasks;
78+
let tasks = cachedTasks.slice();
79+
80+
// status filter
81+
if (currentFilter !== "all") {
82+
tasks = tasks.filter(
83+
(task) => task && task.status === currentFilter,
84+
);
85+
}
86+
87+
// priority filter
88+
if (priorityFilter !== "all") {
89+
tasks = tasks.filter((task) => {
90+
if (!task || !task.priority) return false;
91+
const value =
92+
typeof task.priority === "string"
93+
? task.priority.toLowerCase()
94+
: String(task.priority).toLowerCase();
95+
return value === priorityFilter;
96+
});
97+
}
98+
99+
// tag filter (free text, matches any tag containing the text)
100+
if (tagFilter) {
101+
tasks = tasks.filter((task) => {
102+
if (!task || !Array.isArray(task.tags) || task.tags.length === 0)
103+
return false;
104+
return task.tags.some((tag) => {
105+
if (typeof tag !== "string") return false;
106+
return tag.toLowerCase().includes(tagFilter);
107+
});
108+
});
109+
}
110+
111+
// sorting
112+
if (sortMode === "priority") {
113+
const priorityRank = {
114+
high: 0,
115+
medium: 1,
116+
low: 2,
117+
};
118+
119+
tasks.sort((a, b) => {
120+
const aVal = a && typeof a.priority === "string"
121+
? a.priority.toLowerCase()
122+
: "";
123+
const bVal = b && typeof b.priority === "string"
124+
? b.priority.toLowerCase()
125+
: "";
126+
127+
const aRank = priorityRank.hasOwnProperty(aVal)
128+
? priorityRank[aVal]
129+
: 3;
130+
const bRank = priorityRank.hasOwnProperty(bVal)
131+
? priorityRank[bVal]
132+
: 3;
133+
134+
if (aRank !== bRank) return aRank - bRank;
135+
136+
// tie-breaker: keep original order by not changing when ranks equal
137+
return 0;
138+
});
46139
}
47140

48-
return cachedTasks.filter(
49-
(task) => task && task.status === currentFilter,
50-
);
141+
return tasks;
51142
}
52143

53144

0 commit comments

Comments
 (0)