A complete task manager with filtering, sorting, priorities, due dates, and localStorage persistence.
import Eleva from "eleva";
const app = new Eleva("TaskManager");
// Task Item Component
app.component("TaskItem", {
setup({ props }) {
const { task, onToggle, onDelete, onEdit } = props;
return { task, onToggle, onDelete, onEdit };
},
template: (ctx) => `
<div class="task-item ${ctx.task.completed ? 'completed' : ''} priority-${ctx.task.priority}">
<input
type="checkbox"
${ctx.task.completed ? 'checked' : ''}
@change="onToggle"
/>
<div class="task-content">
<span class="task-title">${ctx.task.title}</span>
${ctx.task.dueDate ? `<span class="due-date">Due: ${ctx.task.dueDate}</span>` : ''}
<span class="priority-badge">${ctx.task.priority}</span>
</div>
<div class="task-actions">
<button class="edit-btn" @click="onEdit">✎</button>
<button class="delete-btn" @click="onDelete">×</button>
</div>
</div>
`
});
// Main Task Manager Component
app.component("TaskManager", {
setup({ signal }) {
// Load from localStorage
const savedTasks = localStorage.getItem("tasks");
const tasks = signal(savedTasks ? JSON.parse(savedTasks) : []);
const filter = signal("all"); // all, active, completed
const sortBy = signal("created"); // created, priority, dueDate
const searchQuery = signal("");
const showForm = signal(false);
const editingTask = signal(null);
// Form state
const newTask = signal({
title: "",
priority: "medium",
dueDate: ""
});
// Auto-save to localStorage
tasks.watch((t) => localStorage.setItem("tasks", JSON.stringify(t)));
// Computed: filtered and sorted tasks
function getFilteredTasks() {
let result = [...tasks.value];
// Search
if (searchQuery.value) {
const q = searchQuery.value.toLowerCase();
result = result.filter(t => t.title.toLowerCase().includes(q));
}
// Filter
if (filter.value === "active") result = result.filter(t => !t.completed);
if (filter.value === "completed") result = result.filter(t => t.completed);
// Sort
result.sort((a, b) => {
if (sortBy.value === "priority") {
const order = { high: 0, medium: 1, low: 2 };
return order[a.priority] - order[b.priority];
}
if (sortBy.value === "dueDate") {
if (!a.dueDate) return 1;
if (!b.dueDate) return -1;
return new Date(a.dueDate) - new Date(b.dueDate);
}
return new Date(b.createdAt) - new Date(a.createdAt);
});
return result;
}
// Stats
function getStats() {
const total = tasks.value.length;
const completed = tasks.value.filter(t => t.completed).length;
const active = total - completed;
return { total, completed, active };
}
// Actions
function addTask() {
if (!newTask.value.title.trim()) return;
const task = {
id: Date.now(),
title: newTask.value.title,
priority: newTask.value.priority,
dueDate: newTask.value.dueDate,
completed: false,
createdAt: new Date().toISOString()
};
tasks.value = [...tasks.value, task];
newTask.value = { title: "", priority: "medium", dueDate: "" };
showForm.value = false;
}
function updateTask() {
if (!editingTask.value || !newTask.value.title.trim()) return;
tasks.value = tasks.value.map(t =>
t.id === editingTask.value.id
? { ...t, ...newTask.value }
: t
);
editingTask.value = null;
newTask.value = { title: "", priority: "medium", dueDate: "" };
showForm.value = false;
}
function startEdit(task) {
editingTask.value = task;
newTask.value = {
title: task.title,
priority: task.priority,
dueDate: task.dueDate || ""
};
showForm.value = true;
}
function toggleTask(id) {
tasks.value = tasks.value.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
}
function deleteTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id);
}
function clearCompleted() {
tasks.value = tasks.value.filter(t => !t.completed);
}
function cancelForm() {
showForm.value = false;
editingTask.value = null;
newTask.value = { title: "", priority: "medium", dueDate: "" };
}
return {
tasks, filter, sortBy, searchQuery, showForm, newTask, editingTask,
getFilteredTasks, getStats,
addTask, updateTask, startEdit, toggleTask, deleteTask, clearCompleted, cancelForm
};
},
template: (ctx) => {
const stats = ctx.getStats();
const filtered = ctx.getFilteredTasks();
return `
<div class="task-manager">
<header class="tm-header">
<h1>Task Manager</h1>
<div class="stats">
<span>${stats.active} active</span>
<span>${stats.completed} completed</span>
</div>
</header>
<div class="controls">
<input
type="text"
class="search-input"
placeholder="Search tasks..."
value="${ctx.searchQuery.value}"
@input="(e) => searchQuery.value = e.target.value"
/>
<div class="filter-sort">
<select @change="(e) => filter.value = e.target.value">
<option value="all" ${ctx.filter.value === "all" ? "selected" : ""}>All</option>
<option value="active" ${ctx.filter.value === "active" ? "selected" : ""}>Active</option>
<option value="completed" ${ctx.filter.value === "completed" ? "selected" : ""}>Completed</option>
</select>
<select @change="(e) => sortBy.value = e.target.value">
<option value="created" ${ctx.sortBy.value === "created" ? "selected" : ""}>Newest First</option>
<option value="priority" ${ctx.sortBy.value === "priority" ? "selected" : ""}>Priority</option>
<option value="dueDate" ${ctx.sortBy.value === "dueDate" ? "selected" : ""}>Due Date</option>
</select>
</div>
<button class="add-btn" @click="() => showForm.value = true">+ Add Task</button>
</div>
${ctx.showForm.value ? `
<div class="task-form">
<h3>${ctx.editingTask.value ? 'Edit Task' : 'New Task'}</h3>
<input
type="text"
placeholder="Task title..."
value="${ctx.newTask.value.title}"
@input="(e) => newTask.value = { ...newTask.value, title: e.target.value }"
/>
<div class="form-row">
<select @change="(e) => newTask.value = { ...newTask.value, priority: e.target.value }">
<option value="low" ${ctx.newTask.value.priority === "low" ? "selected" : ""}>Low</option>
<option value="medium" ${ctx.newTask.value.priority === "medium" ? "selected" : ""}>Medium</option>
<option value="high" ${ctx.newTask.value.priority === "high" ? "selected" : ""}>High</option>
</select>
<input
type="date"
value="${ctx.newTask.value.dueDate}"
@change="(e) => newTask.value = { ...newTask.value, dueDate: e.target.value }"
/>
</div>
<div class="form-actions">
<button @click="${ctx.editingTask.value ? 'updateTask' : 'addTask'}">
${ctx.editingTask.value ? 'Update' : 'Add'}
</button>
<button class="cancel" @click="cancelForm">Cancel</button>
</div>
</div>
` : ''}
<div class="task-list">
${filtered.length === 0 ? `
<p class="no-tasks">
${ctx.searchQuery.value ? 'No tasks match your search' : 'No tasks yet. Add one!'}
</p>
` : filtered.map(task => `
<div key="${task.id}" class="task-item-container"
:task="task"
:onToggle="() => toggleTask(task.id)"
:onDelete="() => deleteTask(task.id)"
:onEdit="() => startEdit(task)">
</div>
`).join("")}
</div>
${stats.completed > 0 ? `
<button class="clear-completed" @click="clearCompleted">
Clear completed (${stats.completed})
</button>
` : ''}
</div>
`;
},
style: `
.task-manager { max-width: 600px; margin: 0 auto; padding: 20px; }
.tm-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.tm-header h1 { margin: 0; }
.stats span { margin-left: 15px; color: #666; }
.controls { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
.search-input { flex: 1; min-width: 200px; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; }
.filter-sort { display: flex; gap: 8px; }
.filter-sort select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.add-btn { padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
.task-form { background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
.task-form h3 { margin: 0 0 15px 0; }
.task-form input[type="text"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 10px; }
.form-row { display: flex; gap: 10px; margin-bottom: 15px; }
.form-row select, .form-row input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.form-actions { display: flex; gap: 10px; }
.form-actions button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
.form-actions button:first-child { background: #28a745; color: white; }
.form-actions button.cancel { background: #6c757d; color: white; }
.task-item { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1px solid #eee; border-radius: 4px; margin-bottom: 8px; }
.task-item.completed .task-title { text-decoration: line-through; color: #999; }
.task-item.priority-high { border-left: 4px solid #dc3545; }
.task-item.priority-medium { border-left: 4px solid #ffc107; }
.task-item.priority-low { border-left: 4px solid #28a745; }
.task-content { flex: 1; }
.task-title { display: block; font-weight: 500; }
.due-date { font-size: 12px; color: #666; }
.priority-badge { font-size: 11px; padding: 2px 6px; border-radius: 3px; background: #eee; margin-left: 8px; }
.task-actions button { padding: 4px 8px; border: none; background: transparent; cursor: pointer; font-size: 16px; }
.delete-btn:hover { color: #dc3545; }
.edit-btn:hover { color: #007bff; }
.no-tasks { text-align: center; color: #999; padding: 40px; }
.clear-completed { width: 100%; padding: 10px; background: none; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; color: #666; }
.clear-completed:hover { background: #f8f9fa; }
`,
children: {
".task-item-container": "TaskItem"
}
});
// Mount the application
app.mount(document.getElementById("app"), "TaskManager");
| ← Back to Apps | Next: Weather Dashboard → |