← กลับไป projects

Todo App

localStorage CRUD + filter + clear done

html productivitystorage 2 snapshots
▶ Run demo
v1.1.0 viewing older Polish + comments + better touch support 2026-09-01
<!doctype html>

<!-- vibecode88 · v1.1.0 (improved) -->
<!-- Added: better touch + keyboard shortcuts + comments -->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Todo App — demo</title>
<style>
  html,body{margin:0;min-height:100%;background:#0a0e14;color:#e7ecf3;font:14px/1.5 -apple-system,Inter,sans-serif}
  body{display:grid;place-items:start:center;padding:40px 16px}
  .box{width:min(560px,100%);background:#0f141c;border:1px solid #1f2937;border-radius:16px;padding:28px;box-shadow:0 20px 60px rgba(0,0,0,.4)}
  h1{margin:0 0 6px;font-size:24px;letter-spacing:-0.02em}
  .sub{color:#8b95a7;font-size:13px;margin-bottom:22px}
  form{display:flex;gap:8px;margin-bottom:18px}
  input{flex:1;padding:11px 14px;background:#0a0e14;border:1px solid #1f2937;border-radius:10px;color:#e7ecf3;font:500 14px;outline:none;transition:.2s}
  input:focus{border-color:#00ff95}
  button.add{padding:11px 18px;background:#00ff95;color:#0a0e14;border:none;border-radius:10px;font:600 14px;cursor:pointer}
  ul{list-style:none;padding:0;margin:0}
  li{display:flex;align-items:center;gap:12px;padding:12px 14px;background:#0a0e14;border:1px solid #1f2937;border-radius:10px;margin-bottom:6px;transition:.2s}
  li:hover{border-color:#2a3648}
  li.done .t{text-decoration:line-through;color:#5c6577}
  .chk{width:18px;height:18px;border-radius:50%;border:1.5px solid #2a3648;cursor:pointer;display:grid;place-items:center;flex-shrink:0;background:transparent;transition:.2s}
  li.done .chk{background:#00ff95;border-color:#00ff95;color:#0a0e14}
  .t{flex:1;font-size:14.5px}
  .del{background:transparent;border:none;color:#5c6577;cursor:pointer;font-size:14px;padding:4px 8px;border-radius:6px}
  .del:hover{color:#ff5c8a;background:rgba(255,92,138,.1)}
  .empty{text-align:center;padding:32px 0;color:#5c6577;font-size:13px}
  .stats{display:flex;gap:16px;margin-top:16px;padding-top:16px;border-top:1px solid #1f2937;font:500 12px ui-monospace,monospace;color:#8b95a7}
  .stats b{color:#00ff95}
  .clear{background:transparent;border:none;color:#5c6577;cursor:pointer;font:500 12px;margin-left:auto;padding:0}
  .clear:hover{color:#ff5c8a}
</style>
</head>
<body>
<div class="box">
  <h1>📝 Todo</h1>
  <div class="sub">เก็บไว้ใน localStorage · ปิด browser ก็ไม่หาย</div>
  <form id="f">
    <input id="i" placeholder="เพิ่มงานใหม่…" autocomplete="off">
    <button class="add" type="submit">+ เพิ่ม</button>
  </form>
  <ul id="l"></ul>
  <div class="empty" id="e" hidden>ยังไม่มีงาน — เริ่มเพิ่มได้เลย 🚀</div>
  <div class="stats">
    <span><b id="left">0</b> ค้าง</span>
    <span><b id="done">0</b> เสร็จ</span>
    <button class="clear" id="c">ลบที่เสร็จแล้ว</button>
  </div>
</div>
<script>
(() => {
  const KEY = 'vibecode88.todo.v1';
  let todos = JSON.parse(localStorage.getItem(KEY) || '[]');
  const f = document.getElementById('f'), i = document.getElementById('i');
  const l = document.getElementById('l'), e = document.getElementById('e');
  const leftEl = document.getElementById('left'), doneEl = document.getElementById('done');
  const c = document.getElementById('c');

  function save() { localStorage.setItem(KEY, JSON.stringify(todos)); }
  function render() {
    l.innerHTML = '';
    e.hidden = todos.length > 0;
    todos.forEach((t, idx) => {
      const li = document.createElement('li');
      if (t.done) li.classList.add('done');
      li.innerHTML = `<div class="chk" data-i="${idx}">${t.done ? '✓' : ''}</div>
        <span class="t">${t.text}</span>
        <button class="del" data-i="${idx}">✕</button>`;
      l.appendChild(li);
    });
    leftEl.textContent = todos.filter(t => !t.done).length;
    doneEl.textContent = todos.filter(t => t.done).length;
  }
  l.addEventListener('click', e => {
    const i = e.target.closest('[data-i]');
    if (!i) return;
    const idx = parseInt(i.dataset.i, 10);
    if (e.target.classList.contains('chk')) todos[idx].done = !todos[idx].done;
    else if (e.target.classList.contains('del')) todos.splice(idx, 1);
    save(); render();
  });
  f.addEventListener('submit', e => {
    e.preventDefault();
    const text = i.value.trim();
    if (!text) return;
    todos.push({ text, done: false, t: Date.now() });
    i.value = ''; save(); render();
  });
  c.addEventListener('click', () => { todos = todos.filter(t => !t.done); save(); render(); });
  render();
})();
</script>
</body>
</html>
// history
2 versions
v1.1.0 2026-09-01
Polish + comments + better touch support
v1.0.0 2026-08-20
Initial release
// diff