Skip to content

Commit 3dff159

Browse files
committed
feat: create simple task manager
1 parent 4ea48ed commit 3dff159

File tree

5 files changed

+318
-0
lines changed

5 files changed

+318
-0
lines changed

small-task-manager/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Task Manager
2+
3+
A simple and efficient task management application built with React and TypeScript. Keep track of your tasks, mark them as complete, and organize them with filtering and sorting capabilities.
4+
5+
## Features
6+
7+
- **Add Tasks**: Quickly add new tasks with a simple input form
8+
- **Mark Complete**: Toggle tasks between active and completed states
9+
- **Filter Tasks**: View all tasks, only active tasks, or only completed tasks
10+
- **Sort Tasks**: Alphabetically sort your tasks in ascending or descending order
11+
- **Persistent Storage**: Tasks are automatically saved to localStorage and persist between sessions
12+
- **Keyboard Shortcuts**: Press Escape to clear the input field
13+
- **Task Counter**: See how many tasks you've added in the current session
14+
15+
## Usage
16+
17+
### Adding Tasks
18+
19+
1. Type your task in the input field
20+
2. Press Enter or click the "Add Task" button
21+
3. Your task will appear in the list below
22+
23+
### Managing Tasks
24+
25+
- **Complete a task**: Click the checkbox next to a task to mark it as complete
26+
- **Delete a task**: Click the "Delete" button to remove a task
27+
- **Filter tasks**: Use the "All", "Active", or "Completed" buttons to filter your view
28+
- **Sort tasks**: Click the "Sort" button to toggle between ascending, descending, and unsorted views
29+
30+
### Keyboard Shortcuts
31+
32+
- **Escape**: Clear the input field (useful for quickly starting over)
33+
34+
## Technical Details
35+
36+
### Built With
37+
38+
- React 18
39+
- TypeScript
40+
- Local Storage API for persistence
41+
42+
### Components
43+
44+
- **TaskList**: Main container component managing state and task operations
45+
- **TaskItem**: Individual task display with checkbox and delete button
46+
- **AddTaskForm**: Input form for creating new tasks
47+
- **useLocalStorage**: Custom hook for localStorage persistence
48+
49+
### Data Persistence
50+
51+
All tasks are automatically saved to your browser's localStorage. Your tasks will be available even after closing and reopening the browser, as long as you're using the same browser on the same device.
52+
53+
## Development
54+
55+
This is a TypeScript React application. The components follow React best practices with proper state management, memoization, and effect cleanup.
56+
57+
### File Structure
58+
59+
```
60+
├── task-list.tsx # Main component with task management logic
61+
├── task-item.tsx # Individual task display component
62+
├── add-task-form.tsx # Form for adding new tasks
63+
└── use-local-storage.ts # Custom hook for localStorage integration
64+
```
65+
66+
## Browser Support
67+
68+
Works in all modern browsers that support:
69+
- ES6+ JavaScript
70+
- localStorage API
71+
- React 18
72+
73+
## Notes
74+
75+
- Tasks are stored per-browser, not synced across devices
76+
- Maximum storage is limited by browser localStorage limits (typically ~5-10MB)
77+
- Task IDs are generated using timestamps for uniqueness
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React, { useState, useRef, useEffect } from 'react';
2+
3+
type AddTaskFormProps = {
4+
onAddTask: (text: string) => void;
5+
};
6+
7+
export const AddTaskForm: React.FC<AddTaskFormProps> = ({ onAddTask }) => {
8+
const [inputValue, setInputValue] = useState('');
9+
const [submissionCount, setSubmissionCount] = useState(0);
10+
const inputRef = useRef<HTMLInputElement>(null);
11+
12+
useEffect(() => {
13+
// Focus input on mount only
14+
inputRef.current?.focus();
15+
}, []); // Empty array - only runs on mount
16+
17+
const handleSubmit = (e: React.FormEvent) => {
18+
e.preventDefault();
19+
const trimmedValue = inputValue.trim();
20+
21+
if (trimmedValue) {
22+
onAddTask(trimmedValue);
23+
setSubmissionCount(prev => prev + 1); // Use state for values that need to trigger re-renders
24+
setInputValue('');
25+
inputRef.current?.focus();
26+
}
27+
};
28+
29+
return (
30+
<div>
31+
<form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
32+
<input
33+
ref={inputRef}
34+
type="text"
35+
value={inputValue}
36+
onChange={(e) => setInputValue(e.target.value)}
37+
placeholder="What needs to be done?"
38+
aria-label="New task input"
39+
style={{
40+
padding: '10px',
41+
width: '70%',
42+
fontSize: '16px',
43+
border: '1px solid #ddd',
44+
borderRadius: '4px',
45+
}}
46+
/>
47+
<button
48+
type="submit"
49+
disabled={!inputValue.trim()}
50+
style={{
51+
padding: '10px 20px',
52+
marginLeft: '10px',
53+
fontSize: '16px',
54+
background: '#4CAF50',
55+
color: 'white',
56+
border: 'none',
57+
borderRadius: '4px',
58+
cursor: inputValue.trim() ? 'pointer' : 'not-allowed',
59+
opacity: inputValue.trim() ? 1 : 0.6,
60+
}}
61+
>
62+
Add Task
63+
</button>
64+
</form>
65+
<small style={{ color: '#666', fontSize: '12px' }}>
66+
Tasks added this session: {submissionCount}
67+
</small>
68+
</div>
69+
);
70+
};

small-task-manager/task-item.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { Task } from './task-list';
3+
4+
type TaskItemProps = {
5+
task: Task;
6+
onToggle: (id: string) => void;
7+
onDelete: (id: string) => void;
8+
};
9+
10+
export const TaskItem: React.FC<TaskItemProps> = React.memo(({ task, onToggle, onDelete }) => {
11+
return (
12+
<div
13+
style={{
14+
display: 'flex',
15+
alignItems: 'center',
16+
padding: '10px',
17+
border: '1px solid #ddd',
18+
marginBottom: '8px',
19+
borderRadius: '4px',
20+
}}
21+
>
22+
<input
23+
type="checkbox"
24+
checked={task.completed}
25+
onChange={() => onToggle(task.id)}
26+
aria-label={`Mark "${task.text}" as ${task.completed ? 'incomplete' : 'complete'}`}
27+
style={{ marginRight: '10px' }}
28+
/>
29+
<span
30+
style={{
31+
flex: 1,
32+
textDecoration: task.completed ? 'line-through' : 'none',
33+
color: task.completed ? '#888' : '#000',
34+
}}
35+
>
36+
{task.text}
37+
</span>
38+
<button
39+
onClick={() => onDelete(task.id)}
40+
aria-label={`Delete task "${task.text}"`}
41+
style={{
42+
padding: '4px 8px',
43+
background: '#ff4444',
44+
color: 'white',
45+
border: 'none',
46+
borderRadius: '4px',
47+
cursor: 'pointer',
48+
}}
49+
>
50+
Delete
51+
</button>
52+
</div>
53+
);
54+
});
55+
56+
TaskItem.displayName = 'TaskItem';

small-task-manager/task-list.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React, { useState, useCallback } from 'react';
2+
import { TaskItem } from './task-item';
3+
import { AddTaskForm } from './add-task-form';
4+
import { useLocalStorage } from './use-local-storage';
5+
6+
export type Task = {
7+
id: string;
8+
text: string;
9+
completed: boolean;
10+
createdAt: number;
11+
};
12+
13+
export const TaskList: React.FC = () => {
14+
const [tasks, setTasks] = useLocalStorage<Task[]>('tasks', []);
15+
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
16+
17+
const addTask = useCallback((text: string) => {
18+
const newTask: Task = {
19+
id: `task-${Date.now()}`,
20+
text,
21+
completed: false,
22+
createdAt: Date.now(),
23+
};
24+
setTasks(prevTasks => [...prevTasks, newTask]);
25+
}, [setTasks]);
26+
27+
const toggleTask = useCallback((id: string) => {
28+
setTasks(prevTasks =>
29+
prevTasks.map(task =>
30+
task.id === id ? { ...task, completed: !task.completed } : task
31+
)
32+
);
33+
}, [setTasks]);
34+
35+
const deleteTask = useCallback((id: string) => {
36+
setTasks(prevTasks => prevTasks.filter(task => task.id !== id));
37+
}, [setTasks]);
38+
39+
const filteredTasks = tasks.filter(task => {
40+
if (filter === 'active') return !task.completed;
41+
if (filter === 'completed') return task.completed;
42+
return true;
43+
});
44+
45+
const activeCount = tasks.filter(t => !t.completed).length;
46+
47+
return (
48+
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
49+
<h1>Task Manager</h1>
50+
51+
<AddTaskForm onAddTask={addTask} />
52+
53+
<div style={{ margin: '20px 0' }}>
54+
<button
55+
onClick={() => setFilter('all')}
56+
style={{ fontWeight: filter === 'all' ? 'bold' : 'normal' }}
57+
>
58+
All ({tasks.length})
59+
</button>
60+
<button
61+
onClick={() => setFilter('active')}
62+
style={{ fontWeight: filter === 'active' ? 'bold' : 'normal', marginLeft: '10px' }}
63+
>
64+
Active ({activeCount})
65+
</button>
66+
<button
67+
onClick={() => setFilter('completed')}
68+
style={{ fontWeight: filter === 'completed' ? 'bold' : 'normal', marginLeft: '10px' }}
69+
>
70+
Completed ({tasks.length - activeCount})
71+
</button>
72+
</div>
73+
74+
<div>
75+
{filteredTasks.length === 0 ? (
76+
<p>No tasks to show</p>
77+
) : (
78+
filteredTasks.map(task => (
79+
<TaskItem
80+
key={task.id}
81+
task={task}
82+
onToggle={toggleTask}
83+
onDelete={deleteTask}
84+
/>
85+
))
86+
)}
87+
</div>
88+
</div>
89+
);
90+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useState, useEffect } from 'react';
2+
3+
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
4+
// Get initial value from localStorage or use provided initialValue
5+
const [storedValue, setStoredValue] = useState<T>(() => {
6+
try {
7+
const item = window.localStorage.getItem(key);
8+
return item ? JSON.parse(item) : initialValue;
9+
} catch (error) {
10+
console.error(`Error reading localStorage key "${key}":`, error);
11+
return initialValue;
12+
}
13+
});
14+
15+
// Update localStorage when storedValue changes
16+
useEffect(() => {
17+
try {
18+
window.localStorage.setItem(key, JSON.stringify(storedValue));
19+
} catch (error) {
20+
console.error(`Error setting localStorage key "${key}":`, error);
21+
}
22+
}, [key, storedValue]);
23+
24+
return [storedValue, setStoredValue];
25+
}

0 commit comments

Comments
 (0)