← Kursa Dön
📄 Text · 30 min

DOM Projesi: Todo App

Giriş — Öğrenilenleri Birleştirmek

Bu bölümde DOM, element seçme/değiştirme ve event handling konularını öğrendik. Şimdi tüm bu bilgileri bir araya getirerek gerçek bir uygulama inşa edeceğiz: klasik Todo App. Basit gibi görünse de, bir Todo uygulaması CRUD operasyonlarının (Create, Read, Update, Delete) tamamını, localStorage ile veri kalıcılığını, filtreleme mantığını ve event delegation gibi ileri teknikleri kapsar.

Bu proje, sadece "öğrendiğini uygula" değil — profesyonel JavaScript geliştirmede kullanılan tasarım kalıplarını da gösterecek: veriyi DOM'dan ayırmak, tek kaynak doğruluğu (single source of truth), render döngüsü ve modüler yapı.

Analoji: Bir Todo App inşa etmek, araba kullanmayı öğrenen birinin ilk kez trafiğe çıkmasına benzer. Direksiyon, gaz, fren, ayna — hepsini ayrı ayrı biliyorsundur. Ama gerçek trafikte hepsini aynı anda koordine etmek başka bir beceridir. Bu proje, o koordinasyonu sağlayacak.


Proje Mimarisi

Başlamadan önce, uygulamamızın yapısını planlayalım. Profesyonel yaklaşım: veri modeli ve görünüm (DOM) birbirinden ayrılır.

┌──────────────────────────────────────────────────┐
│                    Veri Katmanı                    │
│  todoListesi = [{ id, metin, tamamlandi, tarih }] │
│  localStorage ↔ JSON                              │
└────────────────────────┬─────────────────────────┘
                         │ render()
                         ▼
┌──────────────────────────────────────────────────┐
│                   Görünüm (DOM)                    │
│  <ul> → <li> kartları, butonlar, sayaçlar         │
└────────────────────────┬─────────────────────────┘
                         │ event listener'lar
                         ▼
┌──────────────────────────────────────────────────┐
│                  Olay Yönetimi                     │
│  Ekle, Sil, Toggle, Düzenle, Filtrele             │
│  → veri modelini güncelle → render() çağır         │
└──────────────────────────────────────────────────┘

Altın kural: Event handler'lar veriyi değiştirir, sonra render() fonksiyonu veriyi okuyarak DOM'u günceller. DOM'dan doğrudan veri okumayız — veri her zaman JavaScript'teki dizide yaşar.


HTML Yapısı

Önce uygulamamızın iskeletini oluşturalım:

<!DOCTYPE html>
<html lang="tr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Todo Uygulaması</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      background: #f0f2f5;
      color: #333;
      min-height: 100vh;
      display: flex;
      justify-content: center;
      padding-top: 40px;
    }
    .app { width: 100%; max-width: 520px; padding: 0 16px; }
    h1 { text-align: center; margin-bottom: 24px; font-size: 28px; color: #1a1a2e; }

    /* Form */
    .todo-form {
      display: flex; gap: 8px; margin-bottom: 20px;
    }
    .todo-form input {
      flex: 1; padding: 12px 16px; border: 2px solid #ddd;
      border-radius: 8px; font-size: 16px; outline: none;
      transition: border-color 0.2s;
    }
    .todo-form input:focus { border-color: #4a90d9; }
    .todo-form button {
      padding: 12px 20px; background: #4a90d9; color: white;
      border: none; border-radius: 8px; font-size: 16px;
      cursor: pointer; transition: background 0.2s;
    }
    .todo-form button:hover { background: #357abd; }

    /* Filtreler */
    .filtreler {
      display: flex; gap: 4px; margin-bottom: 16px;
      background: white; padding: 4px; border-radius: 8px;
    }
    .filtre-btn {
      flex: 1; padding: 8px; border: none; background: transparent;
      border-radius: 6px; cursor: pointer; font-size: 14px;
      transition: all 0.2s;
    }
    .filtre-btn.aktif { background: #4a90d9; color: white; }

    /* Bilgi çubuğu */
    .bilgi-cubugu {
      display: flex; justify-content: space-between; align-items: center;
      margin-bottom: 12px; font-size: 14px; color: #666;
    }
    .temizle-btn {
      background: none; border: none; color: #e74c3c;
      cursor: pointer; font-size: 14px;
    }
    .temizle-btn:hover { text-decoration: underline; }

    /* Todo listesi */
    .todo-listesi { list-style: none; }
    .todo-item {
      display: flex; align-items: center; gap: 12px;
      background: white; padding: 14px 16px; margin-bottom: 8px;
      border-radius: 8px; transition: all 0.2s;
      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    }
    .todo-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.12); }
    .todo-item.tamamlandi { opacity: 0.6; }
    .todo-item.tamamlandi .todo-metin {
      text-decoration: line-through; color: #999;
    }

    .todo-checkbox {
      width: 22px; height: 22px; cursor: pointer; accent-color: #4a90d9;
    }
    .todo-metin {
      flex: 1; font-size: 16px; word-break: break-word;
    }
    .todo-metin-input {
      flex: 1; font-size: 16px; padding: 4px 8px;
      border: 2px solid #4a90d9; border-radius: 4px; outline: none;
    }
    .todo-tarih { font-size: 12px; color: #aaa; }
    .todo-sil-btn {
      background: none; border: none; font-size: 18px;
      cursor: pointer; opacity: 0.4; transition: opacity 0.2s;
    }
    .todo-sil-btn:hover { opacity: 1; }

    .bos-mesaj {
      text-align: center; padding: 40px; color: #aaa; font-size: 16px;
    }
  </style>
</head>
<body>
  <div class="app">
    <h1>📝 Yapılacaklar</h1>

    <form class="todo-form" id="todoForm">
      <input type="text" id="todoInput" placeholder="Yeni görev ekle..."
             autocomplete="off" maxlength="200">
      <button type="submit">Ekle</button>
    </form>

    <div class="filtreler" id="filtreler">
      <button class="filtre-btn aktif" data-filtre="hepsi">Hepsi</button>
      <button class="filtre-btn" data-filtre="aktif">Aktif</button>
      <button class="filtre-btn" data-filtre="tamamlandi">Tamamlanan</button>
    </div>

    <div class="bilgi-cubugu">
      <span id="sayac">0 görev</span>
      <button class="temizle-btn" id="temizleBtn">Tamamlananları temizle</button>
    </div>

    <ul class="todo-listesi" id="todoListesi"></ul>
  </div>

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

Adım 1: Veri Modeli ve localStorage

Her şey veriyle başlar. Todo'larımızı bir dizide tutacağız ve localStorage ile kalıcı hale getireceğiz.

// ===== VERİ KATMANI =====

// localStorage'dan verileri yükle veya boş dizi başlat
function todoYukle() {
  try {
    const veri = localStorage.getItem("todolar");
    return veri ? JSON.parse(veri) : [];
  } catch (hata) {
    console.error("localStorage okuma hatası:", hata);
    return [];
  }
}

// localStorage'a kaydet
function todoKaydet(todolar) {
  try {
    localStorage.setItem("todolar", JSON.stringify(todolar));
  } catch (hata) {
    console.error("localStorage yazma hatası:", hata);
    // localStorage dolu olabilir (genellikle 5MB limit)
  }
}

// Uygulama durumu (state)
let todolar = todoYukle();
let aktifFiltre = "hepsi";

Her todo şu yapıda olacak:

// Tek bir todo'nun veri yapısı
// {
//   id: 1709283600000,          // Benzersiz ID (timestamp)
//   metin: "JavaScript öğren",  // Görev metni
//   tamamlandi: false,          // Tamamlandı mı?
//   olusturma: "2025-03-01T10:00:00.000Z"  // Oluşturma tarihi
// }

💡 İpucu: ID olarak Date.now() kullanıyoruz — milisaniye cinsinden zaman damgası benzersiz bir tanımlayıcı olarak yeterli. Profesyonel uygulamalarda UUID veya sunucu tarafında oluşturulan ID'ler kullanılır.


Adım 2: CRUD Operasyonları

Veri katmanındaki temel işlemler:

// ===== CRUD İŞLEMLERİ =====

// CREATE — Yeni todo ekle
function todoEkle(metin) {
  const temizMetin = metin.trim();
  if (!temizMetin) return null; // Boş görev ekleme

  const yeniTodo = {
    id: Date.now(),
    metin: temizMetin,
    tamamlandi: false,
    olusturma: new Date().toISOString()
  };

  todolar.unshift(yeniTodo); // Başa ekle (en yeni en üstte)
  todoKaydet(todolar);
  return yeniTodo;
}

// READ — Filtreye göre todo'ları getir
function todoFiltrele(filtre) {
  switch (filtre) {
    case "aktif":
      return todolar.filter(t => !t.tamamlandi);
    case "tamamlandi":
      return todolar.filter(t => t.tamamlandi);
    default:
      return todolar;
  }
}

// UPDATE — Todo durumunu toggle et
function todoToggle(id) {
  const todo = todolar.find(t => t.id === id);
  if (todo) {
    todo.tamamlandi = !todo.tamamlandi;
    todoKaydet(todolar);
  }
}

// UPDATE — Todo metnini düzenle
function todoDuzenle(id, yeniMetin) {
  const temizMetin = yeniMetin.trim();
  if (!temizMetin) return false;

  const todo = todolar.find(t => t.id === id);
  if (todo) {
    todo.metin = temizMetin;
    todoKaydet(todolar);
    return true;
  }
  return false;
}

// DELETE — Tek todo sil
function todoSil(id) {
  const index = todolar.findIndex(t => t.id === id);
  if (index !== -1) {
    todolar.splice(index, 1);
    todoKaydet(todolar);
  }
}

// DELETE — Tamamlanmış todo'ları toplu sil
function tamamlananlariTemizle() {
  todolar = todolar.filter(t => !t.tamamlandi);
  todoKaydet(todolar);
}

// İstatistikler
function todoIstatistik() {
  const toplam = todolar.length;
  const tamamlanan = todolar.filter(t => t.tamamlandi).length;
  const aktif = toplam - tamamlanan;
  return { toplam, tamamlanan, aktif };
}

Bu fonksiyonlar saf veri fonksiyonlarıdır — DOM ile hiçbir ilgileri yok. Bu ayrım çok önemlidir: veriyi yöneten kod ve DOM'u güncelleyen kod birbirinden bağımsız olmalıdır.


Adım 3: Render Fonksiyonu — Veriyi DOM'a Yansıtma

Render fonksiyonu, veri modelini okur ve DOM'u buna göre günceller. Bu, uygulamamızın tek güncelleme noktasıdır.

// ===== GÖRÜNÜM (RENDER) KATMANI =====

// DOM referansları — bir kere seç, tekrar tekrar kullan
const todoForm = document.querySelector("#todoForm");
const todoInput = document.querySelector("#todoInput");
const todoListesi = document.querySelector("#todoListesi");
const sayacSpan = document.querySelector("#sayac");
const temizleBtn = document.querySelector("#temizleBtn");
const filtreBtnleri = document.querySelectorAll(".filtre-btn");

// Tarih formatlama yardımcısı
function tarihFormatla(isoString) {
  const tarih = new Date(isoString);
  const simdi = new Date();
  const farkMs = simdi - tarih;
  const farkDakika = Math.floor(farkMs / 60000);
  const farkSaat = Math.floor(farkMs / 3600000);

  if (farkDakika < 1) return "Az önce";
  if (farkDakika < 60) return `${farkDakika} dk önce`;
  if (farkSaat < 24) return `${farkSaat} saat önce`;

  return tarih.toLocaleDateString("tr-TR", {
    day: "numeric",
    month: "short"
  });
}

// Tek bir todo'nun HTML'ini oluştur
function todoHTMLOlustur(todo) {
  const li = document.createElement("li");
  li.className = `todo-item${todo.tamamlandi ? " tamamlandi" : ""}`;
  li.dataset.id = todo.id;

  li.innerHTML = `
    <input type="checkbox" class="todo-checkbox"
           ${todo.tamamlandi ? "checked" : ""}
           aria-label="Görevi tamamla">
    <span class="todo-metin">${escapeHTML(todo.metin)}</span>
    <span class="todo-tarih">${tarihFormatla(todo.olusturma)}</span>
    <button class="todo-sil-btn" aria-label="Görevi sil">✕</button>
  `;

  return li;
}

// XSS koruması — kullanıcı girdisini HTML'e güvenle ekleme
function escapeHTML(str) {
  const div = document.createElement("div");
  div.textContent = str;
  return div.innerHTML;
}

// Ana render fonksiyonu
function render() {
  // 1. Filtrelenmiş listeyi al
  const filtrelenmis = todoFiltrele(aktifFiltre);

  // 2. Listeyi güncelle — DocumentFragment ile tek reflow
  const fragment = document.createDocumentFragment();

  if (filtrelenmis.length === 0) {
    const bos = document.createElement("li");
    bos.className = "bos-mesaj";
    bos.textContent = aktifFiltre === "hepsi"
      ? "Henüz görev eklenmedi. Yukarıdan yeni bir görev ekleyin!"
      : aktifFiltre === "aktif"
        ? "Tüm görevler tamamlanmış! 🎉"
        : "Tamamlanmış görev bulunmuyor.";
    fragment.appendChild(bos);
  } else {
    filtrelenmis.forEach(todo => {
      fragment.appendChild(todoHTMLOlustur(todo));
    });
  }

  todoListesi.replaceChildren(fragment);

  // 3. Sayaç güncelle
  const istatistik = todoIstatistik();
  sayacSpan.textContent = `${istatistik.aktif} aktif görev`;

  // 4. "Temizle" butonunu göster/gizle
  temizleBtn.style.display = istatistik.tamamlanan > 0 ? "inline" : "none";

  // 5. Aktif filtre butonunu güncelle
  filtreBtnleri.forEach(btn => {
    btn.classList.toggle("aktif", btn.dataset.filtre === aktifFiltre);
  });
}

⚠️ Dikkat: escapeHTML fonksiyonu, kullanıcının girdiği metni HTML olarak yorumlanmadan DOM'a eklememizi sağlar. Örneğin kullanıcı <script>alert(1)</script> yazarsa, bu metin olarak görünür, script olarak çalışmaz. Her zaman kullanıcı girdisini sanitize edin!


Adım 4: Event Handling — Kullanıcı Etkileşimleri

Şimdi tüm kullanıcı etkileşimlerini bağlayalım:

// ===== OLAY YÖNETİMİ =====

// 1. Form submit — Yeni todo ekleme
todoForm.addEventListener("submit", (e) => {
  e.preventDefault();

  const metin = todoInput.value;
  const yeniTodo = todoEkle(metin);

  if (yeniTodo) {
    todoInput.value = "";     // Input'u temizle
    todoInput.focus();        // Focus'u geri ver

    // Eğer "tamamlandi" filtresindeyse, "hepsi"ne geç
    if (aktifFiltre === "tamamlandi") {
      aktifFiltre = "hepsi";
    }

    render();
  }
});

// 2. Todo listesi etkileşimleri — EVENT DELEGATION
todoListesi.addEventListener("click", (e) => {
  // Tıklanan öğenin en yakın todo-item'ını bul
  const todoItem = e.target.closest(".todo-item");
  if (!todoItem) return;

  const todoId = parseInt(todoItem.dataset.id, 10);

  // Checkbox tıklandıysa → toggle
  if (e.target.classList.contains("todo-checkbox")) {
    todoToggle(todoId);
    render();
    return;
  }

  // Sil butonu tıklandıysa → sil
  if (e.target.classList.contains("todo-sil-btn")) {
    // Kayma animasyonu
    todoItem.style.transform = "translateX(100%)";
    todoItem.style.opacity = "0";
    todoItem.style.transition = "all 0.3s ease";

    // Animasyon bitince sil ve render et
    setTimeout(() => {
      todoSil(todoId);
      render();
    }, 300);
    return;
  }
});

// 3. Çift tıklama ile düzenleme
todoListesi.addEventListener("dblclick", (e) => {
  const todoMetin = e.target.closest(".todo-metin");
  if (!todoMetin) return;

  const todoItem = todoMetin.closest(".todo-item");
  const todoId = parseInt(todoItem.dataset.id, 10);
  const mevcutMetin = todolar.find(t => t.id === todoId)?.metin;
  if (!mevcutMetin) return;

  // Metin span'ını input'a dönüştür
  const input = document.createElement("input");
  input.type = "text";
  input.className = "todo-metin-input";
  input.value = mevcutMetin;
  input.maxLength = 200;

  todoMetin.replaceWith(input);
  input.focus();
  input.select(); // Metni seç

  // Düzenlemeyi kaydet
  function kaydet() {
    const yeniMetin = input.value.trim();
    if (yeniMetin && yeniMetin !== mevcutMetin) {
      todoDuzenle(todoId, yeniMetin);
    }
    render(); // Her durumda render et (eski haline dön)
  }

  // Enter ile kaydet
  input.addEventListener("keydown", (e) => {
    if (e.key === "Enter") {
      e.preventDefault();
      kaydet();
    }
    if (e.key === "Escape") {
      render(); // İptal — eski hale dön
    }
  });

  // Focus kaybedince kaydet
  input.addEventListener("blur", kaydet);
});

// 4. Filtre butonları — EVENT DELEGATION
document.querySelector("#filtreler").addEventListener("click", (e) => {
  const btn = e.target.closest(".filtre-btn");
  if (!btn) return;

  aktifFiltre = btn.dataset.filtre;
  render();
});

// 5. Tamamlananları temizle
temizleBtn.addEventListener("click", () => {
  const tamamlananSayisi = todolar.filter(t => t.tamamlandi).length;

  if (confirm(`${tamamlananSayisi} tamamlanmış görev silinecek. Emin misiniz?`)) {
    tamamlananlariTemizle();
    render();
  }
});

// 6. Klavye kısayolları
document.addEventListener("keydown", (e) => {
  // Ctrl+/ veya Cmd+/ ile input'a odaklan
  if ((e.ctrlKey || e.metaKey) && e.key === "/") {
    e.preventDefault();
    todoInput.focus();
  }
});

Adım 5: İleri Özellikler

Sürükle-Bırak ile Sıralama (Basit Versiyon)

// ===== SÜRÜKLE-BIRAK =====
let suruklenenItem = null;

todoListesi.addEventListener("dragstart", (e) => {
  const todoItem = e.target.closest(".todo-item");
  if (!todoItem) return;

  suruklenenItem = todoItem;
  todoItem.style.opacity = "0.4";
  e.dataTransfer.effectAllowed = "move";
});

todoListesi.addEventListener("dragend", (e) => {
  const todoItem = e.target.closest(".todo-item");
  if (todoItem) todoItem.style.opacity = "1";
  suruklenenItem = null;

  // Yeni sırayı kaydet
  const yeniSira = [...todoListesi.querySelectorAll(".todo-item")]
    .map(item => parseInt(item.dataset.id, 10));

  todolar.sort((a, b) => yeniSira.indexOf(a.id) - yeniSira.indexOf(b.id));
  todoKaydet(todolar);
});

todoListesi.addEventListener("dragover", (e) => {
  e.preventDefault();
  const todoItem = e.target.closest(".todo-item");
  if (!todoItem || todoItem === suruklenenItem) return;

  const rect = todoItem.getBoundingClientRect();
  const ortaNokta = rect.top + rect.height / 2;

  if (e.clientY < ortaNokta) {
    todoItem.before(suruklenenItem);
  } else {
    todoItem.after(suruklenenItem);
  }
});

// Todo kartlarına draggable attribute eklemeyi unutmayın:
// render fonksiyonunda li.draggable = true; ekleyin

localStorage Değişiklik Senkronizasyonu

Birden fazla sekme açıksa, localStorage değişikliklerini senkronize edin:

// ===== SEKMELER ARASI SENKRONİZASYON =====

// Başka bir sekmede localStorage değiştiğinde
window.addEventListener("storage", (e) => {
  if (e.key === "todolar") {
    // Başka sekmede güncelleme yapılmış
    todolar = e.newValue ? JSON.parse(e.newValue) : [];
    render();
    console.log("Başka sekmeden güncelleme alındı");
  }
});

Arama/Filtreleme

// ===== ARAMA =====
const aramaInput = document.createElement("input");
aramaInput.type = "search";
aramaInput.placeholder = "Görev ara...";
aramaInput.className = "arama-input";
// Bu elementi formun altına ekleyebilirsiniz

let aramaSorgusu = "";

aramaInput.addEventListener("input", debounce((e) => {
  aramaSorgusu = e.target.value.trim().toLowerCase();
  render();
}, 200));

// render() fonksiyonundaki filtrelemeyi güncelle:
function todoFiltreleVeAra(filtre) {
  let sonuc = todoFiltrele(filtre);

  if (aramaSorgusu) {
    sonuc = sonuc.filter(t =>
      t.metin.toLowerCase().includes(aramaSorgusu)
    );
  }

  return sonuc;
}

function debounce(fn, bekleme) {
  let zamanlayici;
  return function(...args) {
    clearTimeout(zamanlayici);
    zamanlayici = setTimeout(() => fn.apply(this, args), bekleme);
  };
}

Tüm Kodun Birleştirilmiş Hali

İşte uygulamamızın tamamlanmış app.js dosyası:

// ╔══════════════════════════════════════════════════════════╗
// ║              Todo Uygulaması — app.js                    ║
// ╚══════════════════════════════════════════════════════════╝

// ===== YARDIMCI FONKSİYONLAR =====
function escapeHTML(str) {
  const div = document.createElement("div");
  div.textContent = str;
  return div.innerHTML;
}

function debounce(fn, bekleme) {
  let zamanlayici;
  return function(...args) {
    clearTimeout(zamanlayici);
    zamanlayici = setTimeout(() => fn.apply(this, args), bekleme);
  };
}

function tarihFormatla(isoString) {
  const tarih = new Date(isoString);
  const simdi = new Date();
  const farkMs = simdi - tarih;
  const farkDakika = Math.floor(farkMs / 60000);
  const farkSaat = Math.floor(farkMs / 3600000);
  const farkGun = Math.floor(farkMs / 86400000);

  if (farkDakika < 1) return "Az önce";
  if (farkDakika < 60) return `${farkDakika} dk önce`;
  if (farkSaat < 24) return `${farkSaat} saat önce`;
  if (farkGun < 7) return `${farkGun} gün önce`;

  return tarih.toLocaleDateString("tr-TR", {
    day: "numeric",
    month: "short"
  });
}

// ===== VERİ KATMANI =====
function todoYukle() {
  try {
    const veri = localStorage.getItem("todolar");
    return veri ? JSON.parse(veri) : [];
  } catch {
    return [];
  }
}

function todoKaydet(todolar) {
  try {
    localStorage.setItem("todolar", JSON.stringify(todolar));
  } catch (hata) {
    console.error("Kaydetme hatası:", hata.message);
  }
}

// Uygulama durumu
let todolar = todoYukle();
let aktifFiltre = "hepsi";

// CRUD
function todoEkle(metin) {
  const temiz = metin.trim();
  if (!temiz) return null;

  const todo = {
    id: Date.now(),
    metin: temiz,
    tamamlandi: false,
    olusturma: new Date().toISOString()
  };
  todolar.unshift(todo);
  todoKaydet(todolar);
  return todo;
}

function todoToggle(id) {
  const todo = todolar.find(t => t.id === id);
  if (todo) {
    todo.tamamlandi = !todo.tamamlandi;
    todoKaydet(todolar);
  }
}

function todoDuzenle(id, yeniMetin) {
  const temiz = yeniMetin.trim();
  if (!temiz) return false;

  const todo = todolar.find(t => t.id === id);
  if (todo) {
    todo.metin = temiz;
    todoKaydet(todolar);
    return true;
  }
  return false;
}

function todoSil(id) {
  todolar = todolar.filter(t => t.id !== id);
  todoKaydet(todolar);
}

function tamamlananlariTemizle() {
  todolar = todolar.filter(t => !t.tamamlandi);
  todoKaydet(todolar);
}

function todoFiltrele(filtre) {
  switch (filtre) {
    case "aktif": return todolar.filter(t => !t.tamamlandi);
    case "tamamlandi": return todolar.filter(t => t.tamamlandi);
    default: return [...todolar];
  }
}

function todoIstatistik() {
  const toplam = todolar.length;
  const tamamlanan = todolar.filter(t => t.tamamlandi).length;
  return { toplam, tamamlanan, aktif: toplam - tamamlanan };
}

// ===== GÖRÜNÜM KATMANI =====
const todoForm = document.querySelector("#todoForm");
const todoInput = document.querySelector("#todoInput");
const todoListesi = document.querySelector("#todoListesi");
const sayacSpan = document.querySelector("#sayac");
const temizleBtn = document.querySelector("#temizleBtn");
const filtreBtnleri = document.querySelectorAll(".filtre-btn");

function todoHTMLOlustur(todo) {
  const li = document.createElement("li");
  li.className = `todo-item${todo.tamamlandi ? " tamamlandi" : ""}`;
  li.dataset.id = todo.id;
  li.draggable = true;

  li.innerHTML = `
    <input type="checkbox" class="todo-checkbox"
           ${todo.tamamlandi ? "checked" : ""}
           aria-label="Görevi tamamla">
    <span class="todo-metin">${escapeHTML(todo.metin)}</span>
    <span class="todo-tarih">${tarihFormatla(todo.olusturma)}</span>
    <button class="todo-sil-btn" aria-label="Görevi sil">✕</button>
  `;

  return li;
}

function render() {
  const filtrelenmis = todoFiltrele(aktifFiltre);
  const fragment = document.createDocumentFragment();

  if (filtrelenmis.length === 0) {
    const bos = document.createElement("li");
    bos.className = "bos-mesaj";
    const mesajlar = {
      hepsi: "Henüz görev eklenmedi. Yukarıdan bir görev ekleyin!",
      aktif: "Tüm görevler tamamlanmış! 🎉",
      tamamlandi: "Tamamlanmış görev bulunmuyor."
    };
    bos.textContent = mesajlar[aktifFiltre];
    fragment.appendChild(bos);
  } else {
    filtrelenmis.forEach(todo => {
      fragment.appendChild(todoHTMLOlustur(todo));
    });
  }

  todoListesi.replaceChildren(fragment);

  const ist = todoIstatistik();
  sayacSpan.textContent = `${ist.aktif} aktif görev`;
  temizleBtn.style.display = ist.tamamlanan > 0 ? "inline" : "none";

  filtreBtnleri.forEach(btn => {
    btn.classList.toggle("aktif", btn.dataset.filtre === aktifFiltre);
  });
}

// ===== OLAY YÖNETİMİ =====

// Form submit
todoForm.addEventListener("submit", (e) => {
  e.preventDefault();
  if (todoEkle(todoInput.value)) {
    todoInput.value = "";
    todoInput.focus();
    if (aktifFiltre === "tamamlandi") aktifFiltre = "hepsi";
    render();
  }
});

// Todo listesi etkileşimleri — Event Delegation
todoListesi.addEventListener("click", (e) => {
  const todoItem = e.target.closest(".todo-item");
  if (!todoItem) return;
  const id = parseInt(todoItem.dataset.id, 10);

  if (e.target.classList.contains("todo-checkbox")) {
    todoToggle(id);
    render();
  } else if (e.target.classList.contains("todo-sil-btn")) {
    todoItem.style.transform = "translateX(100%)";
    todoItem.style.opacity = "0";
    todoItem.style.transition = "all 0.3s ease";
    setTimeout(() => { todoSil(id); render(); }, 300);
  }
});

// Çift tıklama ile düzenleme
todoListesi.addEventListener("dblclick", (e) => {
  const metinSpan = e.target.closest(".todo-metin");
  if (!metinSpan) return;

  const todoItem = metinSpan.closest(".todo-item");
  const id = parseInt(todoItem.dataset.id, 10);
  const todo = todolar.find(t => t.id === id);
  if (!todo) return;

  const input = document.createElement("input");
  input.type = "text";
  input.className = "todo-metin-input";
  input.value = todo.metin;
  input.maxLength = 200;

  metinSpan.replaceWith(input);
  input.focus();
  input.select();

  function kaydet() {
    todoDuzenle(id, input.value);
    render();
  }

  input.addEventListener("keydown", (e) => {
    if (e.key === "Enter") { e.preventDefault(); kaydet(); }
    if (e.key === "Escape") render();
  });
  input.addEventListener("blur", kaydet);
});

// Filtre butonları
document.querySelector("#filtreler").addEventListener("click", (e) => {
  const btn = e.target.closest(".filtre-btn");
  if (!btn) return;
  aktifFiltre = btn.dataset.filtre;
  render();
});

// Tamamlananları temizle
temizleBtn.addEventListener("click", () => {
  const sayi = todolar.filter(t => t.tamamlandi).length;
  if (confirm(`${sayi} görev silinecek. Emin misiniz?`)) {
    tamamlananlariTemizle();
    render();
  }
});

// Sekmeler arası senkronizasyon
window.addEventListener("storage", (e) => {
  if (e.key === "todolar") {
    todolar = e.newValue ? JSON.parse(e.newValue) : [];
    render();
  }
});

// ===== BAŞLANGIÇ =====
render();
todoInput.focus();

Projeden Çıkarılacak Dersler

1. Veri ve DOM Ayrımı (Separation of Concerns)

// ❌ Kötü pratik — DOM'dan veri okumak
function todoSayisiniAl() {
  return document.querySelectorAll(".todo-item").length;
}

// ✅ İyi pratik — veri modelinden okumak
function todoSayisiniAl() {
  return todolar.length;
}

Veri her zaman JavaScript'teki dizide yaşar. DOM sadece bu verinin görsel yansımasıdır. Veri değişir → render çağrılır → DOM güncellenir. Asla tersi yapılmaz.

2. Event Delegation

Bu projede todoListesi üzerinde tek bir click listener ile tüm checkbox ve silme butonlarını yönetiyoruz. 100 tane todo olsa bile tek listener yeterli — ve sonradan eklenen todo'lar da otomatik çalışır.

3. XSS Koruması

escapeHTML fonksiyonu, kullanıcı girdisini güvenle DOM'a eklememizi sağlar. innerHTML kullanırken bu koruma şarttır.

4. localStorage ile Kalıcılık

// Hata yönetimi ile localStorage kullanımı
// - JSON.parse/stringify ile veri dönüşümü
// - try/catch ile hata koruması
// - storage event ile sekmeler arası senkronizasyon

⚠️ Dikkat: localStorage sınırlamaları:

  • ~5MB limit (tarayıcıya göre değişir)

  • Sadece string saklar — JSON dönüşümü gerekir

  • Senkron API — büyük verilerde ana thread'i bloklar

  • Aynı origin'e özel — başka siteler erişemez

  • Private/incognito modda kalıcı değildir

5. render() Döngüsü

Tüm state değişikliklerinden sonra render() çağırmak, React ve Vue gibi modern framework'lerin temelini oluşturur. Bu projede bu kalıbı vanilya JavaScript ile öğreniyoruz.

💡 İpucu: Bu Todo App kalıbı, React'ın useState + JSX, Vue'nun reactive + template mantığının saf JavaScript karşılığıdır. Framework öğrenmeye başladığınızda, aslında burada yaptığınız şeyin daha otomatize edilmiş halini gördüğünüzü fark edeceksiniz.


Özet

  • 🔹 Veri ve DOM ayrımı en temel mimari prensiptir. Veri JavaScript dizisinde yaşar, DOM sadece render() fonksiyonu ile güncellenir.

  • 🔹 CRUD operasyonları (Create, Read, Update, Delete) veri katmanında yapılır, ardından render() çağrılarak DOM güncellenir.

  • 🔹 Event delegation ile tek bir üst elementte (liste) dinleyip, closest() ile hedef elementleri ayırt ediyoruz — performans ve dinamik içerik için zorunlu teknik.

  • 🔹 localStorage ile veriler kalıcı hale gelir. JSON.stringify/parse ile dönüşüm, storage event'i ile sekmeler arası senkronizasyon sağlanır.

  • 🔹 XSS koruması: Kullanıcı girdisini innerHTML'e yazmadan önce escapeHTML ile sanitize edin — güvenlik her şeyden önce gelir.

  • 🔹 Bu proje, modern framework'lerin (React, Vue) temel prensiplerinin vanilya JavaScript karşılığıdır: state → render → event → state döngüsü.