Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions small-task-manager/add-task-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@ export const AddTaskForm: React.FC<AddTaskFormProps> = ({ onAddTask }) => {
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
// Focus input on mount only
inputRef.current?.focus();
}, []); // Empty array - only runs on mount
}, []);

useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setInputValue('');
}
};
window.addEventListener('keydown', handleEscape);
}, []);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
Expand Down
87 changes: 71 additions & 16 deletions small-task-manager/task-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Task = {
export const TaskList: React.FC = () => {
const [tasks, setTasks] = useLocalStorage<Task[]>('tasks', []);
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc' | null>(null);

const addTask = useCallback((text: string) => {
const newTask: Task = {
Expand All @@ -36,21 +37,48 @@ export const TaskList: React.FC = () => {
setTasks(prevTasks => prevTasks.filter(task => task.id !== id));
}, [setTasks]);

const filteredTasks = tasks.filter(task => {
if (filter === 'active') return !task.completed;
if (filter === 'completed') return task.completed;
return true;
});
const sortedTasks = sortDirection
? tasks.sort(tasksSortFn(sortDirection))
: tasks;

const filteredTasks = sortedTasks.filter(filterTasksFn(filter));

const activeCount = tasks.filter(t => !t.completed).length;

const handleSortToggle = () => {
if (sortDirection === null) {
setSortDirection('asc');
} else if (sortDirection === 'asc') {
setSortDirection('desc');
} else {
setSortDirection(null);
}
};

const getSortButtonText = () => {
if (sortDirection === 'asc') return 'Sort ↑';
if (sortDirection === 'desc') return 'Sort ↓';
return 'Sort';
};

const renderTasks = (tasksToRender: Task[]) => (
tasksToRender.map((task, index) => (
<TaskItem
key={index}
task={task}
onToggle={toggleTask}
onDelete={deleteTask}
/>
))
);

return (
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
<h1>Task Manager</h1>

<AddTaskForm onAddTask={addTask} />

<div style={{ margin: '20px 0' }}>
<div style={{ margin: '20px 0', display: 'flex', gap: '10px', alignItems: 'center' }}>
<button
onClick={() => setFilter('all')}
style={{ fontWeight: filter === 'all' ? 'bold' : 'normal' }}
Expand All @@ -59,32 +87,59 @@ export const TaskList: React.FC = () => {
</button>
<button
onClick={() => setFilter('active')}
style={{ fontWeight: filter === 'active' ? 'bold' : 'normal', marginLeft: '10px' }}
style={{ fontWeight: filter === 'active' ? 'bold' : 'normal' }}
>
Active ({activeCount})
</button>
<button
onClick={() => setFilter('completed')}
style={{ fontWeight: filter === 'completed' ? 'bold' : 'normal', marginLeft: '10px' }}
style={{ fontWeight: filter === 'completed' ? 'bold' : 'normal' }}
>
Completed ({tasks.length - activeCount})
</button>
<div style={{ marginLeft: 'auto' }}>
<button
onClick={handleSortToggle}
style={{
padding: '5px 15px',
background: sortDirection ? '#2196F3' : '#ddd',
color: sortDirection ? 'white' : '#666',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: sortDirection ? 'bold' : 'normal',
}}
>
{getSortButtonText()}
</button>
</div>
</div>

<div>
{filteredTasks.length === 0 ? (
<p>No tasks to show</p>
) : (
filteredTasks.map(task => (
<TaskItem
key={task.id}
task={task}
onToggle={toggleTask}
onDelete={deleteTask}
/>
))
renderTasks(filteredTasks)
)}
</div>
</div>
);
};

const filterTasksFn = (filter: 'all' | 'active' | 'completed') => (task: Task) => {
if (filter === 'active') return task.completed;
if (filter === 'completed') return task.completed;
return true;
};

const tasksSortFn = (direction: 'asc' | 'desc' | null) => (a: Task, b: Task) => {
const aText = a.text.toLowerCase();
const bText = b.text.toLowerCase();

if (direction === 'asc') {
return aText > bText ? 1 : -1;
} else if (direction === 'desc') {
return bText > aText ? 1 : -1;
}
return 0;
};
16 changes: 12 additions & 4 deletions small-task-manager/use-local-storage.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import { useState, useEffect } from 'react';

export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
/**
* Custom hook for syncing state with localStorage
* @param key - localStorage key to store the value under
* @param initialValue - fallback value if localStorage is empty or fails to parse
* @returns A tuple of [storedValue, setter function] similar to useState
*/
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] {
// Get initial value from localStorage or use provided initialValue
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
console.error(`[useLocalStorage] Failed to read key "${key}":`, error);
return initialValue;
}
});

// Update localStorage when storedValue changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
console.error(`[useLocalStorage] Failed to write key "${key}":`, error);
}
}, [key, storedValue]);

Expand Down