This is the third post of Building a Minimalist Task Manager series
Welcome to Post 3 of our minimalist task manager series! We’ve built a solid foundation and added the three-task focus rule with smooth animations. But there’s one glaring issue, it doesn’t even survive the F5 button.
Picture this: You’ve just spent 10 minutes carefully selecting your 3 most important tasks for the day. You’re feeling focused, motivated, ready to conquer the world… and then you accidentally refresh the page.
Poof.
All gone. Back to square one. Cue the internal screaming.
Well, not anymore! Today we’re adding localStorage to our task manager so your digital discipline survives browser refreshes, accidental closes, and even the occasional computer restart. Plus, we’re throwing in some drag-and-drop reordering because priorities change, and your task list should adapt with you.
What We’re Building Today
- localStorage integration: Tasks persist between browser sessions
- Automatic save/load: No manual saving required - it just works
- Task reordering: Drag and drop to prioritize your tasks
- Data recovery: Your tasks survive crashes, refreshes, and browser restarts
Time to make our task manager truly reliable!
Enhanced HTML for Drag-and-Drop
We need to add draggable attributes to our task items. Update the task creation functions in your JavaScript (we’ll get to that), but first, let’s add a subtle visual indicator in our CSS.
New CSS for Dragging and Visual Feedback
Add these styles to your existing styles.css:
/* Add these drag-and-drop styles to your existing CSS */
.task-item {
/* Add to existing .task-item selector */
transition: transform 0.2s, box-shadow 0.2s;
cursor: grab;
}
.task-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.task-item.dragging {
opacity: 0.5;
transform: rotate(5deg);
cursor: grabbing;
}
.task-list {
/* Add to existing .task-list selector */
min-height: 50px;
}
.drag-over {
border: 2px dashed #4299e1;
background-color: #ebf8ff;
}
.storage-indicator {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #48bb78;
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.storage-indicator.show {
opacity: 1;
}
The localStorage-Powered JavaScript
Now for the main event! Here’s our enhanced script.js with persistence and drag-and-drop:
// Get references to our DOM elements (same as before)
const taskInput = document.getElementById('taskInput');
const addTaskBtn = document.getElementById('addTaskBtn');
const taskList = document.getElementById('taskList');
const completedTaskList = document.getElementById('completedTaskList');
const completedSection = document.getElementById('completedSection');
const taskCounter = document.getElementById('taskCounter');
const limitMessage = document.getElementById('limitMessage');
// Constants
const MAX_ACTIVE_TASKS = 3;
const STORAGE_KEY = 'focusTasksData';
// Arrays to store our tasks
let activeTasks = [];
let completedTasks = [];
// Drag and drop state
let draggedElement = null;
// localStorage functions
function saveToStorage() {
const data = {
activeTasks: activeTasks,
completedTasks: completedTasks,
lastSaved: new Date().toISOString()
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
showStorageIndicator('Saved!');
}
function loadFromStorage() {
try {
const data = localStorage.getItem(STORAGE_KEY);
if (data) {
const parsed = JSON.parse(data);
activeTasks = parsed.activeTasks || [];
completedTasks = parsed.completedTasks || [];
// Rebuild the DOM from saved data
rebuildTaskLists();
showStorageIndicator('Tasks loaded!');
console.log('Data loaded from localStorage:', parsed);
}
} catch (error) {
console.error('Error loading from localStorage:', error);
showStorageIndicator('Error loading tasks');
}
}
function showStorageIndicator(message) {
// Create indicator if it doesn't exist
let indicator = document.querySelector('.storage-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.className = 'storage-indicator';
document.body.appendChild(indicator);
}
indicator.textContent = message;
indicator.classList.add('show');
setTimeout(() => {
indicator.classList.remove('show');
}, 2000);
}
function rebuildTaskLists() {
// Clear existing DOM elements
taskList.innerHTML = '';
completedTaskList.innerHTML = '';
// Rebuild active tasks
activeTasks.forEach(task => {
const taskElement = createActiveTaskElement(task.text, task.id);
taskList.appendChild(taskElement);
});
// Rebuild completed tasks
completedTasks.forEach(task => {
const taskElement = createCompletedTaskElement(task.text, task.id);
completedTaskList.appendChild(taskElement);
});
updateUI();
}
// Enhanced task element creation with drag-and-drop
function createActiveTaskElement(taskText, taskId) {
const li = document.createElement('li');
li.className = 'task-item';
li.draggable = true;
li.dataset.taskId = taskId;
li.innerHTML = `
<span class="task-text">${taskText}</span>
<div>
<button class="complete-btn" onclick="completeTask(${taskId})">Done</button>
<button class="delete-btn" onclick="deleteTask(${taskId})">Delete</button>
</div>
`;
// Add drag event listeners
li.addEventListener('dragstart', handleDragStart);
li.addEventListener('dragend', handleDragEnd);
return li;
}
function createCompletedTaskElement(taskText, taskId) {
const li = document.createElement('li');
li.className = 'task-item';
li.innerHTML = `
<span class="task-text">${taskText}</span>
<button class="delete-btn" onclick="deleteCompletedTask(${taskId})">Remove</button>
`;
return li;
}
// Drag and drop handlers
function handleDragStart(e) {
draggedElement = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.outerHTML);
}
function handleDragEnd(e) {
this.classList.remove('dragging');
draggedElement = null;
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
return false;
}
function handleDragEnter(e) {
this.classList.add('drag-over');
}
function handleDragLeave(e) {
this.classList.remove('drag-over');
}
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
this.classList.remove('drag-over');
if (draggedElement !== this) {
const draggedTaskId = parseInt(draggedElement.dataset.taskId);
const targetTaskId = parseInt(this.dataset.taskId);
reorderTasks(draggedTaskId, targetTaskId);
}
return false;
}
function reorderTasks(draggedId, targetId) {
const draggedIndex = activeTasks.findIndex(task => task.id === draggedId);
const targetIndex = activeTasks.findIndex(task => task.id === targetId);
if (draggedIndex !== -1 && targetIndex !== -1) {
// Remove dragged task and insert at new position
const [draggedTask] = activeTasks.splice(draggedIndex, 1);
activeTasks.splice(targetIndex, 0, draggedTask);
// Rebuild the DOM to reflect new order
rebuildTaskLists();
saveToStorage();
}
}
// Updated task management functions with storage
function addTask() {
const taskText = taskInput.value.trim();
if (taskText === '') {
alert('Please enter a task!');
return;
}
if (activeTasks.length >= MAX_ACTIVE_TASKS) {
alert(`Focus mode! You can only have ${MAX_ACTIVE_TASKS} active tasks. Complete one first.`);
return;
}
const task = {
id: Date.now(),
text: taskText,
createdAt: new Date().toISOString()
};
activeTasks.push(task);
const taskElement = createActiveTaskElement(task.text, task.id);
taskList.appendChild(taskElement);
taskInput.value = '';
updateUI();
saveToStorage(); // Save after adding
}
function completeTask(taskId) {
const taskIndex = activeTasks.findIndex(task => task.id === taskId);
if (taskIndex === -1) return;
const task = activeTasks[taskIndex];
task.completedAt = new Date().toISOString();
completedTasks.push(task);
activeTasks.splice(taskIndex, 1);
removeTaskElementById(taskList, taskId);
const completedElement = createCompletedTaskElement(task.text, task.id);
completedTaskList.appendChild(completedElement);
updateUI();
saveToStorage(); // Save after completing
}
function deleteTask(taskId) {
activeTasks = activeTasks.filter(task => task.id !== taskId);
removeTaskElementById(taskList, taskId);
updateUI();
saveToStorage(); // Save after deleting
}
function deleteCompletedTask(taskId) {
completedTasks = completedTasks.filter(task => task.id !== taskId);
removeTaskElementById(completedTaskList, taskId);
updateUI();
saveToStorage(); // Save after deleting
}
// Add drag and drop to task list container
function initializeDragAndDrop() {
taskList.addEventListener('dragover', handleDragOver);
taskList.addEventListener('dragenter', handleDragEnter);
taskList.addEventListener('dragleave', handleDragLeave);
taskList.addEventListener('drop', handleDrop);
}
// Initialize everything on page load
function initialize() {
loadFromStorage(); // Load saved tasks first
updateUI(); // Then update UI
initializeDragAndDrop(); // Initialize drag and drop
}
// Event listeners (same as before)
addTaskBtn.addEventListener('click', addTask);
taskInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addTask();
}
});
// Initialize when page loads
document.addEventListener('DOMContentLoaded', initialize);
// Save data before page unload (backup safety)
window.addEventListener('beforeunload', saveToStorage);
The localStorage Magic Explained
Here’s what’s happening under the hood:
Automatic Saving: Every time you add, complete, or delete a task,
saveToStorage()runs automatically. No manual saving needed!Smart Loading: When the page loads,
loadFromStorage()retrieves your data and rebuilds the task lists exactly as you left them.Data Structure: We save both active and completed tasks along with timestamps, so you have a complete history.
Error Handling: If localStorage fails (rare, but possible), the app gracefully falls back without crashing.
Visual Feedback: That little green indicator in the bottom-right shows when data is being saved or loaded.
Drag-and-Drop Priority Management
The drag-and-drop feature is surprisingly powerful:
- Visual Feedback: Tasks lift slightly on hover and become semi-transparent when dragging
- Priority Reordering: Drag tasks up or down to change their priority
- Automatic Saving: New order is saved immediately
- Smooth Experience: CSS transitions make everything feel polished
Test the Persistence
Here’s how to test your enhanced task manager:
- Add some tasks - notice the “Saved!” indicator
- Refresh the page - your tasks should reappear with “Tasks loaded!”
- Close the browser entirely - reopen and your tasks are still there
- Drag tasks around - the order persists between sessions
- Complete some tasks - completed tasks stay completed
Why This Matters
Before localStorage, web apps were forgetful. Every refresh meant starting over. Now your task manager has a memory that spans browser sessions, computer restarts, and even system crashes.
This is the difference between a toy project and a tool you’d actually use daily. Reliability builds trust, and trust builds habits.
Performance Considerations
localStorage is synchronous and blocks the main thread, but for our small dataset, it’s perfect. We’re storing maybe a few KB of data at most - localStorage can handle up to 5-10MB depending on the browser.
For larger applications, you’d want to consider IndexedDB, but for a focused task manager with a 3-task limit? localStorage is exactly the right tool.
What’s Next
In our next post, we’ll add some final polish - keyboard shortcuts, task editing, maybe some satisfying animations when you complete tasks. We’re building something that’s not just functional, but genuinely enjoyable to use.
Your task manager now has persistence and priority management. That’s the foundation of any serious productivity tool. Next time, we’ll make it feel premium!
Keep coding, and remember - your tasks will be there when you come back!