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)

TEMI PC weboldal készítés | Ultraegyszerű Task modul (HTML + CSS + JS, localStorage)

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