1- import { useState , useEffect } from "react" ;
1+ import { useState , useEffect , useMemo } from "react" ;
22
33interface 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+
1016function 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