Miről szól?
Az előző To-Do példa továbbgondolása: egy letisztult, interaktív Task modul, amelyben feladatokat vehetsz fel, szerkeszthetsz, státuszt válthatsz (Teendő / Folyamatban / Kész), kereshetsz, és minden automatikusan mentődik a böngészőbe (localStorage). Nincs build, nincs framework – csak HTML, CSS és vanilla JS, CDN-ekkel.
Mit fogsz építeni?
- Gyors feladatfelvétel (cím, határidő, prioritás)
- Állapotváltás egy kattintással vagy legördülőből
- Inline cím-szerkesztés (Enter letiltva, fókuszvesztésre ment)
- Keresőmező valós idejű szűréssel
- Számlálók (összes/teendő/folyamatban/kész)
- Automatikus mentés és visszatöltés
Előfeltételek
- Bármely modern böngésző
- Egy mappa és néhány fájl — semmi telepítés
Fáljstruktúra
Ezt a struktúrát készítjük el:
index.html– az oldal váza, CDN-ek, markupcss/styles.css– a célzott stílusokjs/app.js– az interakciók és a mentési logikaassets/task-hero.svg– hero illusztráció a cikkhez
1) HTML alap (oldalváz, űrlap, lista)
<!doctype html>
<html lang="hu">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ultraegyszerű Task modul</title>
<!-- Bootstrap CSS (CDN) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<!-- Bootstrap Icons (CDN) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
<link rel="stylesheet" href="css/styles.css" />
</head>
<body class="bg-light">
<header class="py-4">
<div class="container">
<h1 class="h3 fw-bold mb-3">Ultraegyszerű Task modul</h1>
<img src="assets/task-hero.svg" alt="Task Module Hero" class="img-fluid rounded shadow-sm" />
</div>
</header>
<main class="pb-5">
<div class="container">
<div class="card shadow-sm">
<div class="card-body">
<!-- Új feladat űrlap -->
<form id="taskForm" class="row g-2 align-items-end mb-3">
<div class="col-12 col-md-6">
<label for="taskTitle" class="form-label">Feladat</label>
<input id="taskTitle" type="text" class="form-control" placeholder="Pl.: Landing oldal CTA javítása" required />
</div>
<div class="col-6 col-md-3">
<label for="taskDate" class="form-label">Határidő</label>
<input id="taskDate" type="date" class="form-control" />
</div>
<div class="col-6 col-md-2">
<label for="taskPriority" class="form-label">Prioritás</label>
<select id="taskPriority" class="form-select">
<option value="alacsony">Alacsony</option>
<option value="közepes" selected>Közepes</option>
<option value="magas">Magas</option>
</select>
</div>
<div class="col-12 col-md-1 d-grid">
<button class="btn btn-primary" type="submit">
<i class="bi bi-plus-lg"></i> Hozzáad
</button>
</div>
</form>
<!-- Keresés + státusz szűrő -->
<div class="d-flex flex-column flex-md-row gap-2 align-items-md-center justify-content-between mb-3">
<input id="searchInput" type="search" class="form-control" placeholder="Keresés cím alapján..." />
<ul id="statusTabs" class="nav nav-pills small mt-2 mt-md-0">
<li class="nav-item"><button class="nav-link active" data-filter="all">Összes</button></li>
<li class="nav-item"><button class="nav-link" data-filter="todo">Teendő</button></li>
<li class="nav-item"><button class="nav-link" data-filter="doing">Folyamatban</button></li>
<li class="nav-item"><button class="nav-link" data-filter="done">Kész</button></li>
</ul>
</div>
<!-- Lista -->
<ul id="taskList" class="list-group"></ul>
</div>
<div class="card-footer small text-secondary d-flex flex-wrap gap-3">
<span>Összes: <b id="countAll">0</b></span>
<span>Teendő: <b id="countTodo">0</b></span>
<span>Folyamatban: <b id="countDoing">0</b></span>
<span>Kész: <b id="countDone">0</b></span>
</div>
</div>
</div>
</main>
<script src="js/app.js"></script>
</body>
</html>Tartalmazza:
<head>: Bootstrap + Bootstrap Icons CDN és a saját CSS hivatkozása<header>: cím + hero kép<main>: kártyában az űrlap (cím, határidő, prioritás, Hozzáad gomb), a kereső + státusz fülek, majd a lista (ul)- Lap alján a saját JS fájl betöltése
2) Stílusok (Bootstrapre építve)
/* Rövid, célzott stílusok – Bootstrapra építve */
.list-group-item { user-select: none; }
.task-title {
outline: none;
transition: background-color .2s ease;
border-radius: .25rem;
padding: .1rem .25rem;
}
.task-title:focus { background-color: rgba(13, 110, 253, .08); }
.task-meta i { opacity: .7; }
@media (max-width: 576px) {
.list-group-item .form-select { max-width: 140px; }
}Rövid, célzott szabályok:
- Kijelölés tiltása listaelemeknél
- Szerkeszthető cím fókuszstílusa
- Metaikonok halványítása
- Mobilon a státusz legördülő max szélessége
3) Viselkedés (vanilla JS + localStorage)
// Ultraegyszerű Task modul – vanilla JS + localStorage
(() => {
const lsKey = 'taskModuleV1';
let tasks = loadTasks();
let currentFilter = 'all';
// Elemtárolók
const taskForm = document.getElementById('taskForm');
const taskTitle = document.getElementById('taskTitle');
const taskDate = document.getElementById('taskDate');
const taskPriority = document.getElementById('taskPriority');
const searchInput = document.getElementById('searchInput');
const statusTabs = document.getElementById('statusTabs');
const taskList = document.getElementById('taskList');
// Számlálók
const countAll = document.getElementById('countAll');
const countTodo = document.getElementById('countTodo');
const countDoing = document.getElementById('countDoing');
const countDone = document.getElementById('countDone');
// Init
bindEvents();
renderTasks();
function bindEvents() {
taskForm.addEventListener('submit', handleAddTask);
statusTabs.addEventListener('click', handleFilterClick);
searchInput.addEventListener('input', renderTasks);
taskList.addEventListener('click', handleListClick);
taskList.addEventListener('change', handleListChange);
taskList.addEventListener('focusout', handleTitleEdit);
taskList.addEventListener('keydown', e => {
if (e.target.classList?.contains('task-title') && e.key === 'Enter') {
e.preventDefault();
e.target.blur();
}
});
}
function handleAddTask(e) {
e.preventDefault();
const title = taskTitle.value.trim();
if (!title) return;
const newTask = {
id: uid(),
title,
due: taskDate.value || null,
priority: taskPriority.value,
status: 'todo',
createdAt: Date.now()
};
tasks.push(newTask);
saveTasks();
taskForm.reset();
renderTasks();
taskTitle.focus();
}
function handleFilterClick(e) {
const btn = e.target.closest('[data-filter]');
if (!btn) return;
currentFilter = btn.dataset.filter;
statusTabs.querySelectorAll('.nav-link').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderTasks();
}
function handleListClick(e) {
const li = e.target.closest('li[data-id]');
if (!li) return;
const id = li.dataset.id;
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (action === 'delete') {
tasks = tasks.filter(t => t.id !== id);
saveTasks();
renderTasks();
} else if (action === 'toggleDone') {
const t = findTask(id);
t.status = t.status === 'done' ? 'todo' : 'done';
saveTasks();
renderTasks();
}
}
function handleListChange(e) {
const li = e.target.closest('li[data-id]');
if (!li) return;
const id = li.dataset.id;
if (e.target.matches('[data-action="changeStatus"]')) {
const t = findTask(id);
t.status = e.target.value;
saveTasks();
renderTasks();
}
}
function handleTitleEdit(e) {
if (!e.target.classList?.contains('task-title')) return;
const li = e.target.closest('li[data-id]');
if (!li) return;
const id = li.dataset.id;
const newTitle = e.target.textContent.trim();
if (!newTitle) { renderTasks(); return; }
const t = findTask(id);
t.title = newTitle;
saveTasks();
renderTasks();
}
function renderTasks() {
const q = searchInput.value.toLowerCase();
const visible = tasks.filter(t =>
(currentFilter === 'all' || t.status === currentFilter) &&
t.title.toLowerCase().includes(q)
);
taskList.innerHTML = visible.map(t => taskItemTemplate(t)).join('');
updateCounters();
}
function taskItemTemplate(t) {
const done = t.status === 'done';
const titleClass = `task-title ${done ? 'text-decoration-line-through text-muted' : ''}`;
return `
<li class="list-group-item d-flex align-items-center gap-2" data-id="${t.id}">
<button class="btn btn-sm ${done ? 'btn-success' : 'btn-outline-secondary'}" data-action="toggleDone" title="Készre jelöl">
<i class="bi ${done ? 'bi-check2' : 'bi-square'}"></i>
</button>
<div class="flex-grow-1">
<span class="${titleClass}" contenteditable="true">${escapeHtml(t.title)}</span>
<div class="task-meta small text-secondary mt-1">
<i class="bi bi-flag-fill"></i> ${escapeHtml(t.priority)}
${t.due ? ` • <i class="bi bi-calendar-event"></i> ${escapeHtml(formatDate(t.due))}` : ''}
</div>
</div>
<select class="form-select form-select-sm w-auto" data-action="changeStatus" aria-label="Státusz">
<option value="todo" ${t.status === 'todo' ? 'selected' : ''}>Teendő</option>
<option value="doing" ${t.status === 'doing' ? 'selected' : ''}>Folyamatban</option>
<option value="done" ${t.status === 'done' ? 'selected' : ''}>Kész</option>
</select>
<button class="btn btn-sm btn-outline-danger" data-action="delete" title="Törlés">
<i class="bi bi-trash"></i>
</button>
</li>`;
}
function updateCounters() {
countAll.textContent = tasks.length;
countTodo.textContent = tasks.filter(t => t.status === 'todo').length;
countDoing.textContent = tasks.filter(t => t.status === 'doing').length;
countDone.textContent = tasks.filter(t => t.status === 'done').length;
}
function uid() { return Math.random().toString(36).slice(2, 9); }
function loadTasks() { try { return JSON.parse(localStorage.getItem(lsKey)) || []; } catch { return []; } }
function saveTasks() { localStorage.setItem(lsKey, JSON.stringify(tasks)); }
function findTask(id) { return tasks.find(t => t.id === id); }
function escapeHtml(str) {
return String(str).replaceAll('&','&').replaceAll('<','<')
.replaceAll('>','>').replaceAll('"','"').replaceAll("'",''');
}
function formatDate(yyyyMmDd) { return yyyyMmDd.replaceAll('-', '.'); }
})();Főbb részek:
- Állapot:
taskstömb,currentFilter,lsKey - Események:
- űrlap submit → új feladat
- státusz pill kattintás → szűrés
- kereső input → azonnali szűrés
- lista eseménydelegálás: törlés, készre jelölés, státuszváltás
- cím inline szerkesztése fókuszvesztésre ment
- Renderelés: lista sablonnal, számlálók frissítése
- Mentés:
localStorage-ba írás/olvasás, hibatűrés - Kis segédek:
uid(),escapeHtml(), dátumformázás
4) Hero kép (SVG)

Letisztult, világos illusztráció három oszloppal (Teendő, Folyamatban, Kész). Bármilyen 1200×400-as képpel is helyettesíthető.
Használat
- Töltsd le és csomagold ki a projektet.
- Nyisd meg az
index.htmlfájlt a böngészőben. - Adj hozzá néhány feladatot, próbáld ki a keresést és a státuszváltást.
- Zárd be és nyisd meg újra az oldalt — a feladataid visszatöltődnek.
Bővítési ötletek
- JSON export/import gombpár
- Drag & drop sorrendezés oszlopokon belül
- Címkék (tags) és több mező (leírás, felelős)
- Határidő szerinti szűrés és kiemelés (lejárt/mához közeli)
- A11y & billentyűzet: jobb fókuszkezelés, ARIA attribútumok