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
33 changes: 33 additions & 0 deletions scripts/verify-task-board.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail

required_files=(
"task-board/index.html"
"task-board/styles.css"
"task-board/app.js"
"task-board/README.md"
)

for file in "${required_files[@]}"; do
if [[ ! -f "$file" ]]; then
echo "FAIL: Missing required file: $file"
exit 1
fi
done

required_tokens=(
"addTask"
"toggleTaskComplete"
"deleteTask"
"setFilter"
"localStorage"
)

for token in "${required_tokens[@]}"; do
if ! rg -q "$token" task-board/app.js; then
echo "FAIL: task-board/app.js missing token: $token"
exit 1
fi
done

echo "PASS: task-board verification succeeded"
25 changes: 25 additions & 0 deletions task-board/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Task Board

A small vanilla JavaScript task board app.

## Features

- Add tasks with a single input and Add button
- Render tasks in a list
- Toggle task complete/incomplete
- Delete tasks
- Filter tasks by All, Active, and Completed
- Summary counts for total, active, and completed tasks
- Persist tasks in `localStorage` under the key `taskBoardItems`

## Run locally

1. Open `task-board/index.html` directly in a browser.
2. Or serve the directory with any static server, for example:

```bash
cd task-board
python3 -m http.server 8080
```

Then open `http://localhost:8080`.
179 changes: 179 additions & 0 deletions task-board/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
const STORAGE_KEY = "taskBoardItems";

const state = {
tasks: [],
filter: "all",
};

const elements = {
form: document.getElementById("task-form"),
input: document.getElementById("task-input"),
list: document.getElementById("task-list"),
summary: document.getElementById("summary"),
filters: document.getElementById("filters"),
};

function loadTasks() {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return [];
}

try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}

function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.tasks));
}

function addTask(title) {
const cleanTitle = title.trim();
if (!cleanTitle) {
return;
}

state.tasks.push({
id: crypto.randomUUID(),
title: cleanTitle,
completed: false,
});

saveTasks();
render();
}

function toggleTaskComplete(taskId) {
const task = state.tasks.find((item) => item.id === taskId);
if (!task) {
return;
}

task.completed = !task.completed;
saveTasks();
render();
}

function deleteTask(taskId) {
state.tasks = state.tasks.filter((task) => task.id !== taskId);
saveTasks();
render();
}

function setFilter(filterName) {
state.filter = filterName;

Array.from(elements.filters.querySelectorAll("button")).forEach((button) => {
const isActive = button.dataset.filter === filterName;
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
});

render();
}

function getFilteredTasks() {
if (state.filter === "active") {
return state.tasks.filter((task) => !task.completed);
}

if (state.filter === "completed") {
return state.tasks.filter((task) => task.completed);
}

return state.tasks;
}

function renderSummary() {
const total = state.tasks.length;
const completed = state.tasks.filter((task) => task.completed).length;
const active = total - completed;

elements.summary.textContent = `Total: ${total} | Active: ${active} | Completed: ${completed}`;
}

function createTaskListItem(task) {
const item = document.createElement("li");
item.className = "task-item";

const main = document.createElement("div");
main.className = "task-main";

const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = task.completed;
checkbox.setAttribute("aria-label", `Toggle completion for ${task.title}`);
checkbox.addEventListener("change", () => toggleTaskComplete(task.id));

const title = document.createElement("span");
title.className = "task-title";
if (task.completed) {
title.classList.add("is-completed");
}
title.textContent = task.title;

const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "delete-btn";
deleteButton.textContent = "Delete";
deleteButton.addEventListener("click", () => deleteTask(task.id));

main.appendChild(checkbox);
main.appendChild(title);
item.appendChild(main);
item.appendChild(deleteButton);

return item;
}

function renderTaskList() {
const visibleTasks = getFilteredTasks();
elements.list.innerHTML = "";

if (visibleTasks.length === 0) {
const empty = document.createElement("li");
empty.className = "empty-state";
empty.textContent = "No tasks to show for this filter.";
elements.list.appendChild(empty);
return;
}

visibleTasks.forEach((task) => {
elements.list.appendChild(createTaskListItem(task));
});
}

function render() {
renderTaskList();
renderSummary();
}

function attachEventHandlers() {
elements.form.addEventListener("submit", (event) => {
event.preventDefault();
addTask(elements.input.value);
elements.input.value = "";
elements.input.focus();
});

elements.filters.addEventListener("click", (event) => {
const button = event.target.closest("button[data-filter]");
if (!button) {
return;
}

setFilter(button.dataset.filter);
});
}

function initializeApp() {
state.tasks = loadTasks();
attachEventHandlers();
render();
}

initializeApp();
50 changes: 50 additions & 0 deletions task-board/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Task Board</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="app" aria-labelledby="app-title">
<header class="app__header">
<h1 id="app-title">Task Board</h1>
<p class="subtitle">Track tasks with filters and completion status.</p>
</header>

<section aria-label="Add a task" class="panel">
<form id="task-form" class="task-form">
<label for="task-input" class="sr-only">Task description</label>
<input
id="task-input"
name="task"
type="text"
placeholder="What do you need to do?"
autocomplete="off"
required
/>
<button type="submit">Add</button>
</form>
</section>

<section aria-label="Task filters" class="panel">
<div id="filters" class="filters" role="tablist" aria-label="Filter tasks">
<button type="button" data-filter="all" class="is-active" aria-pressed="true">All</button>
<button type="button" data-filter="active" aria-pressed="false">Active</button>
<button type="button" data-filter="completed" aria-pressed="false">Completed</button>
</div>
</section>

<section aria-label="Task summary" class="panel">
<p id="summary">Total: 0 | Active: 0 | Completed: 0</p>
</section>

<section aria-label="Task list" class="panel">
<ul id="task-list" class="task-list"></ul>
</section>
</main>

<script src="app.js"></script>
</body>
</html>
Loading