# ปฏิบัติการที่ 9: สร้าง App ง่ายๆ ด้วย Async/Await

## วัตถุประสงค์

สร้าง **Todo App ที่มี Timer** ให้แต่ละ task

### Features

- เพิ่ม task ใหม่
- แต่ละ task มี timer (เริ่มต้น 30 นาที)
- กด "เริ่ม" ให้ timer เรียบเรียง
- เมื่อจบ → บอก "เสร็จ!" และสร้อง alarm
- ลบ task ได้

---

## Project Structure

```
lab09/todo/
├── index.html      (HTML & CSS)
├── app.js          (JavaScript logic)
└── style.css       (Styling - optional)
```

---

## Step 1: สร้าง HTML

สร้างไฟล์ `index.html`:

```html
<!DOCTYPE html>
<html lang="th">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Task Timer App</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div class="container">
      <h1>Task Timer</h1>

      <div class="input-group">
        <input
          type="text"
          id="taskInput"
          placeholder="เพิ่ม task ใหม่..."
          autocomplete="off"
        />
        <button id="addBtn">เพิ่ม</button>
      </div>

      <div id="taskList" class="task-list">
        <div class="empty-message">ยังไม่มี task 😴</div>
      </div>
    </div>

    <script src="app.js"></script>
  </body>
</html>
```

---

## Step 2: สร้าง CSS

สร้างไฟล์ `styles.html`:

```css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
  background: linear-gradient(135deg, #eee 0%, #eee 100%);
  min-height: 100vh;
  padding: 20px;
}

.container {
  max-width: 600px;
  margin: 0 auto;
  background: white;
  border-radius: 15px;
  padding: 30px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}

h1 {
  text-align: center;
  color: #333;
  margin-bottom: 30px;
  font-size: 28px;
}

.input-group {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

#taskInput {
  flex: 1;
  padding: 12px;
  border: 2px solid #ddd;
  border-radius: 8px;
  font-size: 16px;
  transition: border-color 0.3s;
}

#taskInput:focus {
  outline: none;
  border-color: #667eea;
}

#addBtn {
  padding: 12px 30px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 16px;
  font-weight: bold;
  transition: background 0.3s;
}

#addBtn:hover {
  background: #5568d3;
}

#addBtn:active {
  transform: scale(0.98);
}

.task-list {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.task-item {
  background: #f8f9fa;
  border-left: 5px solid #667eea;
  padding: 15px;
  border-radius: 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  transition: all 0.3s;
}

.task-item:hover {
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.task-item.completed {
  opacity: 0.6;
  border-left-color: #28a745;
}

.task-item.completed .task-name {
  text-decoration: line-through;
  color: #999;
}

.task-info {
  flex: 1;
}

.task-name {
  font-size: 18px;
  color: #333;
  font-weight: 500;
  margin-bottom: 5px;
}

.task-timer {
  font-size: 24px;
  color: #667eea;
  font-weight: bold;
  font-family: "Courier New", monospace;
}

.task-timer.warning {
  color: #ff9800;
}

.task-timer.danger {
  color: #f44336;
}

.task-controls {
  display: flex;
  gap: 10px;
  margin-left: 15px;
}

button {
  padding: 8px 15px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 600;
  transition: all 0.3s;
}

.btn-start {
  background: #28a745;
  color: white;
}

.btn-start:hover {
  background: #218838;
}

.btn-start:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.btn-pause {
  background: #ff9800;
  color: white;
}

.btn-pause:hover {
  background: #e68900;
}

.btn-delete {
  background: #f44336;
  color: white;
}

.btn-delete:hover {
  background: #da190b;
}

.empty-message {
  text-align: center;
  color: #999;
  padding: 40px;
  font-size: 18px;
}

.notification {
  position: fixed;
  top: 20px;
  right: 20px;
  background: #28a745;
  color: white;
  padding: 15px 25px;
  border-radius: 8px;
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
  animation: slideIn 0.3s ease;
  z-index: 1000;
}

@keyframes slideIn {
  from {
    transform: translateX(400px);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

.alarm {
  animation: blink 0.5s infinite;
}

@keyframes blink {
  0%,
  100% {
    background: #f44336;
  }
  50% {
    background: #d32f2f;
  }
}
```

---

## Step 3: สร้าง JavaScript Logic

สร้างไฟล์ `app.js`:

```javascript
// ================================
// Task Timer App
// ================================

// Data structure
let tasks = [];
let taskId = 0;

// DOM Elements
const taskInput = document.getElementById("taskInput");
const addBtn = document.getElementById("addBtn");
const taskList = document.getElementById("taskList");

// ================================
// Helper Functions
// ================================

// ฟังก์ชันแปลงเวลา (วินาที → นาที:วินาที)
function formatTime(seconds) {
  const minutes = Math.floor(seconds / 60);
  const secs = seconds % 60;
  return `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
}

// แสดง notification
function showNotification(message) {
  const notification = document.createElement("div");
  notification.className = "notification";
  notification.textContent = message;
  document.body.appendChild(notification);

  setTimeout(() => {
    notification.remove();
  }, 3000);
}

// เล่นเสียง alarm
function playAlarm() {
  // ใช้ Web Audio API เพื่อสร้างเสียง
  const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  const oscillator = audioContext.createOscillator();
  const gainNode = audioContext.createGain();

  oscillator.connect(gainNode);
  gainNode.connect(audioContext.destination);

  oscillator.frequency.value = 800; // ความถี่ 800 Hz
  oscillator.type = "sine";

  gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
  gainNode.gain.exponentialRampToValueAtTime(
    0.01,
    audioContext.currentTime + 0.5,
  );

  oscillator.start(audioContext.currentTime);
  oscillator.stop(audioContext.currentTime + 0.5);
}

// ================================
// Task Functions
// ================================

// เพิ่ม task ใหม่
function addTask() {
  const taskName = taskInput.value.trim();

  if (!taskName) {
    alert("กรุณากรอก task");
    return;
  }

  const task = {
    id: taskId++,
    name: taskName,
    duration: 30 * 60, // 30 นาที (เป็นวินาที)
    timeLeft: 30 * 60,
    isRunning: false,
    intervalId: null,
  };

  tasks.push(task);
  taskInput.value = "";
  renderTasks();
  showNotification(`เพิ่ม task: ${taskName}`);
}

// ลบ task
function deleteTask(id) {
  tasks = tasks.filter((task) => task.id !== id);
  renderTasks();
  showNotification("ลบ task แล้ว");
}

// เริ่ม/หยุด timer
function toggleTimer(id) {
  const task = tasks.find((t) => t.id === id);
  if (!task) return;

  if (task.isRunning) {
    // หยุด timer
    clearInterval(task.intervalId);
    task.isRunning = false;
  } else {
    // เริ่ม timer
    task.isRunning = true;

    task.intervalId = setInterval(() => {
      task.timeLeft--;

      // ถ้าหมดเวลา
      if (task.timeLeft <= 0) {
        clearInterval(task.intervalId);
        task.isRunning = false;
        task.timeLeft = 0;

        playAlarm();
        showNotification(`🎉 เสร็จ! "${task.name}"`);
      }

      renderTasks();
    }, 1000); // อัปเดตทุกๆ 1 วินาที
  }

  renderTasks();
}

// ================================
// Render UI
// ================================

function renderTasks() {
  if (tasks.length === 0) {
    taskList.innerHTML = '<div class="empty-message">ยังไม่มี task 😴</div>';
    return;
  }

  taskList.innerHTML = tasks
    .map((task) => {
      const isCompleted = task.timeLeft === 0;
      const isWarning = task.timeLeft < 60 && task.timeLeft > 0;
      const isDanger = task.timeLeft < 30 && task.timeLeft > 0;

      let timerClass = "";
      if (isDanger) timerClass = "danger";
      else if (isWarning) timerClass = "warning";

      return `
        <div class="task-item ${isCompleted ? "completed" : ""}">
          <div class="task-info">
            <div class="task-name">${task.name}</div>
            <div class="task-timer ${timerClass}">
              ${formatTime(task.timeLeft)}
            </div>
          </div>
          <div class="task-controls">
            <button
              class="btn-start ${task.isRunning ? "btn-pause" : ""}"
              onclick="toggleTimer(${task.id})"
              ${isCompleted ? "disabled" : ""}
            >
              ${task.isRunning ? "⏸️ หยุด" : "▶️ เริ่ม"}
            </button>
            <button class="btn-delete" onclick="deleteTask(${task.id})">
              ลบ
            </button>
          </div>
        </div>
      `;
    })
    .join("");
}

// ================================
// Event Listeners
// ================================

addBtn.addEventListener("click", addTask);

taskInput.addEventListener("keypress", (e) => {
  if (e.key === "Enter") {
    addTask();
  }
});

// เริ่มต้น
renderTasks();
```

---

## Run App

### 1 เปิด index.html ด้วย Live Server

ใน VS Code: `Alt + L` → `Alt + O`

### 2 ทดสอบ

- กรอก "เขียนรายงาน" → คลิก "เพิ่ม"
- คลิก "▶️ เริ่ม" → timer เริ่มนับลง
- เมื่อจบ → เสียงดัง + notification
- คลิก "ลบ" → ลบ task

---

## Code Explanation

### ส่วน async ที่สำคัญ

**1 setInterval (อัปเดต timer ทุกๆ 1 วินาที)**

```javascript
task.intervalId = setInterval(() => {
  task.timeLeft--; // ลดเวลา 1 วินาที
  renderTasks(); // อัปเดต UI
}, 1000); // ทุกๆ 1000 millisecond
```

**2 playAlarm (เล่นเสียง)**

```javascript
function playAlarm() {
  const audioContext = new AudioContext();
  const oscillator = audioContext.createOscillator();
  // ... สร้างเสียง 800 Hz เป็นเวลา 0.5 วินาที
}
```

**3 showNotification (แสดง popup)**

```javascript
function showNotification(message) {
  const notification = document.createElement("div");
  notification.textContent = message;
  document.body.appendChild(notification);

  setTimeout(() => {
    notification.remove(); // ลบหลังจาก 3 วินาที
  }, 3000);
}
```

---

## Enhancement Ideas (ถ้าอยากเพิ่มเติม)

- [ ] **Save to LocalStorage** - เก็บ tasks ไว้ใน browser
- [ ] **Custom Timer** - ให้ผู้ใช้กำหนดเวลา
- [ ] **Categories** - จัดหมวดหมู่ task
- [ ] **Progress Stats** - แสดง task ที่เสร็จแล้วกี่ %
- [ ] **Sound Selection** - เลือกเสียง alarm
- [ ] **Dark Mode** - โหมดกลางคืน

---

## Concepts ที่ใช้

| Concept              | ใช้ที่ไหน                        |
| -------------------- | -------------------------------- |
| **setInterval**      | อัปเดต timer ทุกๆ 1 วินาที       |
| **clearInterval**    | หยุด timer                       |
| **setTimeout**       | ลบ notification หลังจาก 3 วินาที |
| **addEventListener** | เช็คการคลิกปุ่ม                  |
| **DOM Methods**      | เพิ่ม/ลบ/แก้ไข element           |
| **String Template**  | สร้าง HTML ด้วย backticks        |
| **Array Methods**    | find(), filter(), map()          |

---

## Testing Checklist

- [ ] เพิ่ม task ได้
- [ ] Timer เรียบเรียงลงได้
- [ ] Color เปลี่ยนเมื่อรอเหลือน้อย (warning/danger)
- [ ] Alarm ดังเมื่อหมดเวลา
- [ ] Notification แสดงขึ้นมา
- [ ] ลบ task ได้
- [ ] UI ปรับปรุงเมื่อ timer เปลี่ยน

---

## Resources

- [MDN: setInterval](https://developer.mozilla.org/en-US/docs/Web/API/setInterval)
- [MDN: Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
- [MDN: Document.createElement](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement)

---

## 📋 คำถามทดสอบความเข้าใจ (Assessment Questions)

### ส่วนที่ 1: การทำความเข้าใจโค้ด

**คำถาม 1.1**
เหตุใดจึงต้องแยก `duration` และ `timeLeft` ออกจากกัน อธิบายความแตกต่าง

**คำถาม 1.2**
`formatTime()` ฟังก์ชันนี้ทำความสำคัญอะไร? ถ้าไม่มีฟังก์ชันนี้ app จะเป็นยังไง

**คำถาม 1.3**
อธิบายข้อแตกต่างระหว่าง `setInterval()` และ `setTimeout()`

---

### ส่วนที่ 2: ฟังก์ชันการทำงาน

**คำถาม 2.1**
เมื่อผู้ใช้คลิกปุ่ม "▶️ เริ่ม" ครั้งแรก เกิดอะไรขึ้นในลำดับใด (ระบุขั้นตอนอย่างน้อย 4 ขั้น)

**คำถาม 2.2**
ในฟังก์ชัน `toggleTimer()` ทำไมจึงต้องมี `clearInterval()` ก่อนแล้วจึงตั้ง `task.isRunning = false`

**คำถาม 2.3**
ถ้า timer หมดเวลา จะเกิดความผิดพลาดใดหากไม่มี `clearInterval()`

---

### ส่วนที่ 3: DOM และ UI

**คำถาม 3.1**
ฟังก์ชัน `renderTasks()` สามารถแยกออกเป็นฟังก์ชันย่อยได้กี่ฟังก์ชัน จงเสนอการแยกที่เหมาะสม

**คำถาม 3.2**
ใช้วิธีใดในการอัปเดต UI ทุกครั้งที่ timer เปลี่ยนแปลง มีวิธีการอื่นนอกเหนือจำนวนปัจจุบันหรือไม่

**คำถาม 3.3**
"keypress" event listener ทำความสำคัญอะไร หากลบออกไป app จะเป็นยังไง

---

### ส่วนที่ 4: Web Audio API

**คำถาม 4.1**
อธิบายหน้าที่ของการตั้งค่า:

- `oscillator.frequency.value = 800`
- `oscillator.type = "sine"`
- `gainNode.gain.setValueAtTime(0.3, audioContext.currentTime)`

**คำถาม 4.2**
ทำไมเสียง alarm จึงใช้เวลา 0.5 วินาที แล้วหยุด ถ้าเพิ่มเป็น 2 วินาทีจะส่งผลต่อ UX อย่างไร

---

### ส่วนที่ 5: Array Methods

**คำถาม 5.1**
ในฟังก์ชัน `deleteTask(id)` ใช้ `filter()` เพื่ออะไร เขียนวิธีอื่นแทน `filter()` ได้หรือไม่

**คำถาม 5.2**
ในฟังก์ชัน `renderTasks()` ใช้ `map()` เพื่อทำอะไร ถ้าใช้ `forEach()` แทนจะดีหรือไม่

---

### ส่วนที่ 6: ข้อบกพร่องและการแก้ไข

**คำถาม 6.1**
หากผู้ใช้เพิ่ม task 2 อัน แล้วคลิก "เริ่ม" บน task ทั้ง 2 อันพร้อมๆ กัน จะเกิดอะไรขึ้น

**คำถาม 6.2**
ถ้า timer กำลังเดินอยู่ และผู้ใช้รีเฟรชหน้า (F5) task จะหายไป ทำไม วิธีแก้ปัญหาคือ

**คำถาม 6.3**
ในการทดสอบ app ที่ timer 30 นาที จะใช้เวลานานไปหรือไม่ เสนอวิธีทดสอบที่เร็วขึ้น

---

### ส่วนที่ 7: Enhancement

**คำถาม 7.1**
หากต้องการเพิ่มฟังก์ชัน "รีเซ็ต timer" คืนไปเป็น 30 นาที โค้ดจะเป็นยังไง

**คำถาม 7.2**
วิธีใดในการเก็บ tasks ไว้ใน LocalStorage เพื่อไม่ให้หายเมื่อรีเฟรชหน้า

**คำถาม 7.3**
ถ้าต้องการให้ผู้ใช้กำหนดเวลา timer เอง จะต้องแก้ไขฟังก์ชันไหนบ้าง

---

### ส่วนที่ 8: Concepts เชิงลึก

**คำถาม 8.1**
ทำไม `renderTasks()` จึงต้องเรียกหลายครั้ง มีวิธี DRY ที่ดีกว่าหรือไม่

**คำถาม 8.2**
ถ้าเปลี่ยนจาก `setInterval` เป็น `requestAnimationFrame` จะส่งผลดีหรือไม่

**คำถาม 8.3**
ในโค้ดที่ใช้ `setInterval()` มี Memory Leak ที่อาจเกิดขึ้นได้หรือไม่

---

## เกณฑ์การประเมิน

| ระดับคะแนน    | เกณฑ์การประเมิน                                |
| ------------- | ---------------------------------------------- |
| **ตัวอย่าง**  | ตอบคำถาม 1-3 ข้อได้ถูกต้อง +ทำให้ app ทำงานได้ |
| **ดี**        | ตอบคำถาม 4-6 ข้อ +เข้าใจ async patterns        |
| **ดีมาก**     | ตอบคำถามส่วนใหญ่ +enhance ฟังก์ชันเพิ่มได้     |
| **ยอดเยี่ยม** | ตอบคำถามทั้งหมด +Memory Leak awareness         |
