| tsx |
| import React, { useState, useEffect } from 'react'; |
|
|
| interface Todo { |
| id: string; |
| text: string; |
| deadline: string; |
| status: 'finished' | 'unfinished'; |
| finishedTime: string | null; |
| } |
|
|
| const ToDoList: React.FC = () => { |
| const [todos, setTodos] = useState<Todo[]>([]); |
| const [inputText, setInputText] = useState(''); |
| const [deadline, setDeadline] = useState(''); |
| const [searchTerm, setSearchTerm] = useState(''); |
| const [filter, setFilter] = useState<'all' | 'finished' | 'unfinished'>('all'); |
| const [sortBy, setSortBy] = useState<'deadline'>('deadline'); |
|
|
| |
| useEffect(() => { |
| const savedTodos = localStorage.getItem('todos'); |
| if (savedTodos) { |
| setTodos(JSON.parse(savedTodos)); |
| } |
| }, []); |
|
|
| |
| useEffect(() => { |
| localStorage.setItem('todos', JSON.stringify(todos)); |
| }, [todos]); |
|
|
| |
| const addTodo = () => { |
| if (!inputText.trim() || !deadline) return; |
| |
| const newTodo: Todo = { |
| id: Date.now().toString(), |
| text: inputText, |
| deadline, |
| status: 'unfinished', |
| finishedTime: null |
| }; |
|
|
| setTodos([...todos, newTodo]); |
| setInputText(''); |
| setDeadline(''); |
| }; |
|
|
| |
| const toggleTodo = (id: string) => { |
| setTodos(todos.map(todo => { |
| if (todo.id === id) { |
| return { |
| ...todo, |
| status: todo.status === 'unfinished' ? 'finished' : 'unfinished', |
| finishedTime: todo.status === 'unfinished' ? new Date().toISOString() : null |
| }; |
| } |
| return todo; |
| })); |
| }; |
|
|
| |
| const deleteTodo = (id: string) => { |
| setTodos(todos.filter(todo => todo.id !== id)); |
| }; |
|
|
| |
| const getFilteredSortedTodos = () => { |
| let filtered = todos; |
| |
| |
| if (searchTerm) { |
| filtered = filtered.filter(todo => |
| todo.text.toLowerCase().includes(searchTerm.toLowerCase()) |
| ); |
| } |
|
|
| |
| if (filter !== 'all') { |
| filtered = filtered.filter(todo => todo.status === filter); |
| } |
|
|
| |
| filtered.sort((a, b) => { |
| |
| if (a.status === 'unfinished' && b.status === 'finished') return -1; |
| if (a.status === 'finished' && b.status === 'unfinished') return 1; |
| |
| |
| return new Date(a.deadline).getTime() - new Date(b.deadline).getTime(); |
| }); |
|
|
| return filtered; |
| }; |
|
|
| |
| const handleSubmit = (e: React.FormEvent) => { |
| e.preventDefault(); |
| addTodo(); |
| }; |
|
|
| return ( |
| <div style={{ |
| maxWidth: '800px', |
| margin: '0 auto', |
| padding: '20px', |
| fontFamily: 'sans-serif' |
| }}> |
| <h1 style={{ |
| textAlign: 'center', |
| color: '#333', |
| marginBottom: '30px', |
| fontSize: '2.5rem' |
| }}>Tasky McTaskface</h1> |
| |
| {/* Add Todo Form */} |
| <form onSubmit={handleSubmit} style={{ |
| display: 'flex', |
| flexDirection: 'column', |
| gap: '10px', |
| marginBottom: '20px', |
| padding: '20px', |
| borderRadius: '8px', |
| background: '#f5f5f5' |
| }}> |
| <input |
| type="text" |
| value={inputText} |
| onChange={(e) => setInputText(e.target.value)} |
| placeholder="What needs to be done?" |
| style={{ |
| padding: '10px', |
| borderRadius: '4px', |
| border: '1px solid #ddd', |
| fontSize: '1rem' |
| }} |
| required |
| /> |
| <div style={{ display: 'flex', gap: '10px' }}> |
| <input |
| type="datetime-local" |
| value={deadline} |
| onChange={(e) => setDeadline(e.target.value)} |
| style={{ |
| padding: '10px', |
| borderRadius: '4px', |
| border: '1px solid #ddd', |
| fontSize: '1rem', |
| flex: 1 |
| }} |
| required |
| /> |
| <button |
| type="submit" |
| style={{ |
| padding: '10px 20px', |
| borderRadius: '4px', |
| border: 'none', |
| background: '#4CAF50', |
| color: 'white', |
| cursor: 'pointer', |
| fontSize: '1rem', |
| fontWeight: 'bold' |
| }} |
| > |
| Add Task |
| </button> |
| </div> |
| </form> |
| |
| {/* Controls */} |
| <div style={{ |
| display: 'flex', |
| justifyContent: 'space-between', |
| marginBottom: '20px', |
| gap: '10px', |
| flexWrap: 'wrap' |
| }}> |
| <input |
| type="text" |
| placeholder="Search tasks..." |
| value={searchTerm} |
| onChange={(e) => setSearchTerm(e.target.value)} |
| style={{ |
| padding: '10px', |
| borderRadius: '4px', |
| border: '1px solid #ddd', |
| fontSize: '1rem', |
| flex: 1, |
| minWidth: '200px' |
| }} |
| /> |
| <select |
| value={filter} |
| onChange={(e) => setFilter(e.target.value as any)} |
| style={{ |
| padding: '10px', |
| borderRadius: '4px', |
| border: '1px solid #ddd', |
| fontSize: '1rem', |
| background: 'white', |
| minWidth: '150px' |
| }} |
| > |
| <option value="all">All Tasks</option> |
| <option value="finished">Completed</option> |
| <option value="unfinished">Pending</option> |
| </select> |
| </div> |
| |
| {/* Todo List */} |
| <div style={{ |
| display: 'flex', |
| flexDirection: 'column', |
| gap: '10px' |
| }}> |
| {getFilteredSortedTodos().length === 0 ? ( |
| <p style={{ |
| textAlign: 'center', |
| padding: '20px', |
| color: '#666' |
| }}> |
| {searchTerm ? 'No matching tasks found' : 'No tasks yet. Add one above!'} |
| </p> |
| ) : ( |
| getFilteredSortedTodos().map(todo => ( |
| <div |
| key={todo.id} |
| style={{ |
| display: 'flex', |
| alignItems: 'center', |
| padding: '15px', |
| borderRadius: '8px', |
| background: todo.status === 'finished' ? '#e8f5e9' : '#fff3e0', |
| borderLeft: `4px solid ${todo.status === 'finished' ? '#4CAF50' : '#FF9800'}`, |
| gap: '15px', |
| transition: 'all 0.3s ease' |
| }} |
| > |
| <input |
| type="checkbox" |
| checked={todo.status === 'finished'} |
| onChange={() => toggleTodo(todo.id)} |
| style={{ |
| width: '20px', |
| height: '20px', |
| cursor: 'pointer' |
| }} |
| /> |
| <div style={{ flex: 1 }}> |
| <p style={{ |
| margin: '0', |
| textDecoration: todo.status === 'finished' ? 'line-through' : 'none', |
| color: todo.status === 'finished' ? '#666' : '#333', |
| fontSize: '1.1rem' |
| }}> |
| {todo.text} |
| </p> |
| <p style={{ |
| margin: '5px 0 0', |
| fontSize: '0.9rem', |
| color: '#666' |
| }}> |
| Due: {new Date(todo.deadline).toLocaleString()} |
| {todo.finishedTime && ( |
| <span style={{ marginLeft: '10px' }}> |
| Completed: {new Date(todo.finishedTime).toLocaleString()} |
| </span> |
| )} |
| </p> |
| </div> |
| <button |
| onClick={() => deleteTodo(todo.id)} |
| style={{ |
| padding: '5px 10px', |
| borderRadius: '4px', |
| border: 'none', |
| background: '#f44336', |
| color: 'white', |
| cursor: 'pointer', |
| fontSize: '0.9rem' |
| }} |
| > |
| Delete |
| </button> |
| </div> |
| )) |
| )} |
| </div> |
| |
| {/* Stats */} |
| <div style={{ |
| marginTop: '20px', |
| padding: '15px', |
| background: '#f5f5f5', |
| borderRadius: '8px', |
| textAlign: 'center' |
| }}> |
| <p style={{ margin: '0' }}> |
| {todos.filter(t => t.status === 'unfinished').length} pending, |
| {todos.filter(t => t.status === 'finished').length} completed • |
| Total: {todos.length} tasks |
| </p> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default ToDoList; |
|
|
|
|
| This ToDoList component provides a complete solution with: |
|
|
| 1. LocalStorage persistence for todos |
| 2. CRUD operations (Create, Read, Update, Delete) |
| 3. Search functionality |
| 4. Filtering by status (all, finished, unfinished) |
| 5. Sorting by deadline with unfinished items prioritized |
| 6. Clean, responsive UI with: |
| - Add task form with text and deadline inputs |
| - Search bar and filter dropdown |
| - Visually distinct todo items with completion status |
| - Stats section showing counts |
| - Smooth transitions and intuitive interactions |
|
|
| The component is fully self-contained with all state management and UI rendering handled internally. The TypeScript interface ensures type safety for all todo operations. |
| </html> |