11const form = document . getElementById ( 'form' ) ;
22const input = document . getElementById ( 'input' ) ;
3+ const dueInput = document . getElementById ( 'due' ) ;
34const list = document . getElementById ( 'list' ) ;
5+ const counts = document . getElementById ( 'counts' ) ;
6+ const clearCompletedBtn = document . getElementById ( 'clear-completed' ) ;
7+ const filterButtons = document . querySelectorAll ( '.filter-btn' ) ;
8+ const sortSelect = document . getElementById ( 'sort' ) ;
49
5- let items = [ ] ;
10+ let items = JSON . parse ( localStorage . getItem ( 'todo-items' ) || '[]' ) ;
11+ let filter = 'all' ;
12+ let sortBy = 'created' ;
13+
14+ function uid ( ) {
15+ if ( typeof crypto !== 'undefined' && typeof crypto . randomUUID === 'function' ) return crypto . randomUUID ( ) ;
16+ // fallback: timestamp + random
17+ return 'id-' + Date . now ( ) . toString ( 36 ) + '-' + Math . random ( ) . toString ( 36 ) . slice ( 2 , 9 ) ;
18+ }
619
720function escapeHTML ( text ) {
821 const div = document . createElement ( 'div' ) ;
@@ -12,39 +25,136 @@ function escapeHTML(text) {
1225
1326form . addEventListener ( 'submit' , ( e ) => {
1427 e . preventDefault ( ) ;
15- items . push ( { id : crypto . randomUUID ( ) , text : input . value . trim ( ) , done : false } ) ;
28+ const text = input . value . trim ( ) ;
29+ if ( ! text ) return ;
30+ const due = dueInput . value ? new Date ( dueInput . value ) . toISOString ( ) : null ;
31+ items . push ( { id : uid ( ) , text, done : false , created : Date . now ( ) , due } ) ;
1632 input . value = '' ;
17- render ( ) ;
33+ dueInput . value = '' ;
34+ saveAndRender ( ) ;
1835} ) ;
1936
2037function render ( ) {
2138 list . innerHTML = '' ;
22- for ( const it of items ) {
39+
40+ // Apply filter and sort
41+ let visible = items . slice ( ) ;
42+ if ( filter === 'active' ) visible = visible . filter ( i => ! i . done ) ;
43+ if ( filter === 'completed' ) visible = visible . filter ( i => i . done ) ;
44+
45+ if ( sortBy === 'due' ) {
46+ visible . sort ( ( a , b ) => {
47+ if ( ! a . due && ! b . due ) return a . created - b . created ;
48+ if ( ! a . due ) return 1 ;
49+ if ( ! b . due ) return - 1 ;
50+ return new Date ( a . due ) - new Date ( b . due ) ;
51+ } ) ;
52+ } else {
53+ visible . sort ( ( a , b ) => a . created - b . created ) ;
54+ }
55+
56+ for ( const it of visible ) {
2357 const li = document . createElement ( 'li' ) ;
2458 if ( it . done ) li . classList . add ( 'done' ) ;
2559
60+ // overdue detection
61+ const isOverdue = it . due && ! it . done && ( new Date ( it . due ) < startOfToday ( ) ) ;
62+ if ( isOverdue ) li . classList . add ( 'overdue' ) ;
63+
64+ const dueMeta = it . due ? `<small class="meta">Due ${ new Date ( it . due ) . toLocaleDateString ( ) } </small>` : '' ;
65+
2666 li . innerHTML = `
2767 <label>
28- <input type="checkbox" ${ it . done ? 'checked' : '' } >
29- <span>${ escapeHTML ( it . text ) } </span>
68+ <input type="checkbox" ${ it . done ? 'checked' : '' } aria-label="Mark task as done" >
69+ <span>${ escapeHTML ( it . text ) } ${ dueMeta } </span>
3070 </label>
31- <button class="del" aria-label="Delete">×</button>
71+ <button class="del" aria-label="Delete task ">×</button>
3272 ` ;
3373
34- const checkbox = li . querySelector ( 'input' ) ;
74+ const checkbox = li . querySelector ( 'input[type=checkbox] ' ) ;
3575 checkbox . addEventListener ( 'change' , ( e ) => {
3676 it . done = e . target . checked ;
3777 li . classList . toggle ( 'done' , it . done ) ;
78+ saveAndRender ( ) ;
3879 } ) ;
3980
4081 li . querySelector ( '.del' ) . addEventListener ( 'click' , ( ) => {
4182 items = items . filter ( ( x ) => x . id !== it . id ) ;
42- render ( ) ;
83+ saveAndRender ( ) ;
4384 } ) ;
4485
4586 list . appendChild ( li ) ;
4687 }
88+
89+ updateCounts ( ) ;
90+ }
91+
92+ function startOfToday ( ) {
93+ const d = new Date ( ) ;
94+ d . setHours ( 0 , 0 , 0 , 0 ) ;
95+ return d ;
96+ }
97+
98+ function updateCounts ( ) {
99+ const total = items . length ;
100+ const active = items . filter ( i => ! i . done ) . length ;
101+ counts . textContent = `${ active } active • ${ total } total` ;
102+ // Render pending tasks as short text list (up to 5)
103+ const pendingEl = document . getElementById ( 'pending-list' ) ;
104+ if ( pendingEl ) {
105+ const pending = items . filter ( i => ! i . done ) ;
106+ if ( ! pending . length ) {
107+ pendingEl . textContent = 'No pending tasks' ;
108+ } else {
109+ const max = 5 ;
110+ const slice = pending . slice ( 0 , max ) ;
111+ pendingEl . innerHTML = slice . map ( i => `<div class="pending-item">${ escapeHTML ( i . text ) } </div>` ) . join ( '' ) ;
112+ if ( pending . length > max ) {
113+ const more = pending . length - max ;
114+ pendingEl . insertAdjacentHTML ( 'beforeend' , `<div class="muted">+${ more } more</div>` ) ;
115+ }
116+ }
117+ }
47118}
48119
120+ function saveAndRender ( ) {
121+ localStorage . setItem ( 'todo-items' , JSON . stringify ( items ) ) ;
122+ render ( ) ;
123+ }
124+
125+ // initialize
49126render ( ) ;
50- // TODOs: save to localStorage; add filter (all/active/done); sort; theme switcher
127+
128+ // wire filters
129+ filterButtons . forEach ( btn => btn . addEventListener ( 'click' , ( e ) => {
130+ filterButtons . forEach ( b => b . classList . remove ( 'active' ) ) ;
131+ btn . classList . add ( 'active' ) ;
132+ filter = btn . dataset . filter ;
133+ render ( ) ;
134+ } ) ) ;
135+
136+ sortSelect . addEventListener ( 'change' , ( e ) => { sortBy = e . target . value ; render ( ) ; } ) ;
137+
138+ clearCompletedBtn . addEventListener ( 'click' , ( ) => { items = items . filter ( i => ! i . done ) ; saveAndRender ( ) ; } ) ;
139+
140+ // theme sync: read global theme from localStorage (some apps use body.dark-mode), apply data-theme
141+ function syncTheme ( ) {
142+ const theme = localStorage . getItem ( 'theme' ) || ( document . body . classList . contains ( 'dark-mode' ) ? 'dark' : 'light' ) ;
143+ if ( theme === 'light' ) {
144+ document . documentElement . setAttribute ( 'data-theme' , 'light' ) ;
145+ } else {
146+ document . documentElement . removeAttribute ( 'data-theme' ) ;
147+ }
148+ }
149+
150+ syncTheme ( ) ;
151+
152+ // respond to storage changes (theme toggled in another tab or via global control)
153+ window . addEventListener ( 'storage' , ( e ) => {
154+ if ( e . key === 'theme' ) syncTheme ( ) ;
155+ } ) ;
156+
157+ // keyboard: quick add focus
158+ document . addEventListener ( 'keydown' , ( e ) => {
159+ if ( e . key === 'n' ) { input . focus ( ) ; }
160+ } ) ;
0 commit comments