Skip to content

Commit 4a10171

Browse files
feat: add categories, priorities, search, sorting, and stats
1 parent bba37e7 commit 4a10171

1 file changed

Lines changed: 115 additions & 14 deletions

File tree

src/App.tsx

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,46 @@
1-
import { useState, useEffect } from "react";
1+
import { useState, useEffect, useMemo } from "react";
22

33
interface Todo {
44
id: string;
55
text: string;
66
done: boolean;
7+
category: string;
8+
priority: "low" | "medium" | "high";
9+
dueDate?: string;
710
createdAt: number;
811
}
912

13+
const CATEGORIES = ["Personal", "Work", "Shopping", "Health", "Other"];
14+
const PRIORITY_COLORS = { low: "#22c55e", medium: "#eab308", high: "#ef4444" };
15+
1016
function App() {
1117
const [todos, setTodos] = useState<Todo[]>(() => {
12-
const saved = localStorage.getItem("todos");
18+
const saved = localStorage.getItem("todos-v2");
1319
return saved ? JSON.parse(saved) : [];
1420
});
1521
const [input, setInput] = useState("");
1622
const [filter, setFilter] = useState<"all" | "active" | "done">("all");
23+
const [search, setSearch] = useState("");
24+
const [category, setCategory] = useState("Personal");
25+
const [priority, setPriority] = useState<"low" | "medium" | "high">("medium");
26+
const [categoryFilter, setCategoryFilter] = useState<string>("all");
27+
const [sortBy, setSortBy] = useState<"date" | "priority" | "name">("date");
1728

1829
useEffect(() => {
19-
localStorage.setItem("todos", JSON.stringify(todos));
30+
localStorage.setItem("todos-v2", JSON.stringify(todos));
2031
}, [todos]);
2132

2233
const addTodo = () => {
2334
const text = input.trim();
2435
if (!text) return;
25-
setTodos(prev => [...prev, { id: crypto.randomUUID(), text, done: false, createdAt: Date.now() }]);
36+
setTodos(prev => [...prev, {
37+
id: crypto.randomUUID(),
38+
text,
39+
done: false,
40+
category,
41+
priority,
42+
createdAt: Date.now(),
43+
}]);
2644
setInput("");
2745
};
2846

@@ -36,21 +54,69 @@ function App() {
3654

3755
const clearDone = () => setTodos(prev => prev.filter(t => !t.done));
3856

39-
const filtered = todos.filter(t => {
40-
if (filter === "active") return !t.done;
41-
if (filter === "done") return t.done;
42-
return true;
43-
});
57+
const filtered = useMemo(() => {
58+
let result = todos;
59+
60+
// Status filter
61+
if (filter === "active") result = result.filter(t => !t.done);
62+
if (filter === "done") result = result.filter(t => t.done);
63+
64+
// Category filter
65+
if (categoryFilter !== "all") result = result.filter(t => t.category === categoryFilter);
66+
67+
// Search
68+
if (search) {
69+
const q = search.toLowerCase();
70+
result = result.filter(t => t.text.toLowerCase().includes(q));
71+
}
72+
73+
// Sort
74+
if (sortBy === "priority") {
75+
const order = { high: 0, medium: 1, low: 2 };
76+
result = [...result].sort((a, b) => order[a.priority] - order[b.priority]);
77+
} else if (sortBy === "name") {
78+
result = [...result].sort((a, b) => a.text.localeCompare(b.text));
79+
} else {
80+
result = [...result].sort((a, b) => b.createdAt - a.createdAt);
81+
}
82+
83+
return result;
84+
}, [todos, filter, categoryFilter, search, sortBy]);
4485

4586
const activeCount = todos.filter(t => !t.done).length;
87+
const doneCount = todos.filter(t => t.done).length;
88+
89+
const stats = useMemo(() => {
90+
const byCat: Record<string, number> = {};
91+
const byPri: Record<string, number> = {};
92+
todos.forEach(t => {
93+
byCat[t.category] = (byCat[t.category] || 0) + 1;
94+
byPri[t.priority] = (byPri[t.priority] || 0) + 1;
95+
});
96+
return { byCat, byPri };
97+
}, [todos]);
4698

4799
return (
48100
<div className="app">
49101
<header>
50102
<h1>Todo</h1>
51-
<p className="subtitle">{activeCount} task{activeCount !== 1 ? "s" : ""} remaining</p>
103+
<div className="stats-row">
104+
<span className="stat">{activeCount} active</span>
105+
<span className="stat done-stat">{doneCount} done</span>
106+
<span className="stat">{todos.length} total</span>
107+
</div>
52108
</header>
53109

110+
{/* Search */}
111+
<input
112+
type="search"
113+
className="search-input"
114+
placeholder="Search tasks..."
115+
value={search}
116+
onChange={e => setSearch(e.target.value)}
117+
/>
118+
119+
{/* Add todo */}
54120
<div className="input-row">
55121
<input
56122
type="text"
@@ -63,6 +129,24 @@ function App() {
63129
<button className="add-btn" onClick={addTodo}>+</button>
64130
</div>
65131

132+
{/* Category & Priority selectors */}
133+
<div className="options-row">
134+
<select value={category} onChange={e => setCategory(e.target.value)}>
135+
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
136+
</select>
137+
<select value={priority} onChange={e => setPriority(e.target.value as Todo["priority"])}>
138+
<option value="low">Low</option>
139+
<option value="medium">Medium</option>
140+
<option value="high">High</option>
141+
</select>
142+
<select value={sortBy} onChange={e => setSortBy(e.target.value as "date" | "priority" | "name")}>
143+
<option value="date">Sort: Date</option>
144+
<option value="priority">Sort: Priority</option>
145+
<option value="name">Sort: Name</option>
146+
</select>
147+
</div>
148+
149+
{/* Filters */}
66150
<div className="filters">
67151
{(["all", "active", "done"] as const).map(f => (
68152
<button key={f} className={filter === f ? "active" : ""} onClick={() => setFilter(f)}>
@@ -71,25 +155,42 @@ function App() {
71155
))}
72156
</div>
73157

158+
{/* Category filter pills */}
159+
<div className="category-pills">
160+
<button className={categoryFilter === "all" ? "active" : ""} onClick={() => setCategoryFilter("all")}>All</button>
161+
{CATEGORIES.map(c => (
162+
<button key={c} className={categoryFilter === c ? "active" : ""} onClick={() => setCategoryFilter(c)}>
163+
{c} ({stats.byCat[c] || 0})
164+
</button>
165+
))}
166+
</div>
167+
168+
{/* Todo list */}
74169
<ul className="todo-list">
75170
{filtered.length === 0 && (
76171
<li className="empty">
77-
{filter === "done" ? "No completed tasks" : filter === "active" ? "All done!" : "Add your first task"}
172+
{search ? "No matching tasks" : filter === "done" ? "No completed tasks" : "Add your first task"}
78173
</li>
79174
)}
80175
{filtered.map(todo => (
81176
<li key={todo.id} className={todo.done ? "done" : ""}>
82177
<button className="check" onClick={() => toggleTodo(todo.id)}>
83178
{todo.done ? "\u2713" : ""}
84179
</button>
85-
<span className="text">{todo.text}</span>
180+
<div className="todo-content">
181+
<span className="text">{todo.text}</span>
182+
<div className="todo-meta">
183+
<span className="category-badge">{todo.category}</span>
184+
<span className="priority-dot" style={{ background: PRIORITY_COLORS[todo.priority] }} title={todo.priority} />
185+
</div>
186+
</div>
86187
<button className="delete" onClick={() => deleteTodo(todo.id)}>\u00d7</button>
87188
</li>
88189
))}
89190
</ul>
90191

91-
{todos.some(t => t.done) && (
92-
<button className="clear-btn" onClick={clearDone}>Clear completed</button>
192+
{doneCount > 0 && (
193+
<button className="clear-btn" onClick={clearDone}>Clear {doneCount} completed</button>
93194
)}
94195
</div>
95196
);

0 commit comments

Comments
 (0)