Ultraegyszerű Task modul (HTML + CSS + JS, localStorage)

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, markup
  • css/styles.css – a célzott stílusok
  • js/app.js – az interakciók és a mentési logika
  • assets/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

  1. Töltsd le és csomagold ki a projektet.
  2. Nyisd meg az index.html fájlt a böngészőben.
  3. Adj hozzá néhány feladatot, próbáld ki a keresést és a státuszváltást.
  4. 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

Letöltés