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:
tasks
tö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.html
fá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