# ปฏิบัติการที่ 9: Weather App (Fetch API)

## วัตถุประสงค์

สร้าง **Weather App** ที่:

- 🌍 ค้นหาสภาพอากาศของเมืองต่างๆ
- 🌡️ แสดง อุณหภูมิ, ความชื้น, ความเร็วลม
- 🔄 Auto-refresh ทุก 10 นาที
- 💾 บันทึกเมืองที่ค้นหาล่าสุด

---

## API ที่ใช้

ปฏิบัติการนี้ใช้ **Open-Meteo API** (ฟรี ไม่ต้อง API Key)

```
https://api.open-meteo.com/v1/forecast
```

---

## Project Structure

```
lab09/weather/
├── 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>Weather App</title>
    <link rel="stylesheet" href="styles.css" />
  <body>
    <div class="container">
      <h1>Weather App</h1>

      <!-- Search Section -->
      <div class="search-section">
        <div class="search-box">
          <input
            type="text"
            id="cityInput"
            placeholder="ค้นหาเมือง (เช่น Bangkok, Tokyo, London)..."
            autocomplete="off"
          />
          <button id="searchBtn">ค้นหา</button>
        </div>

        <div id="recentCities" class="recent-cities"></div>
      </div>

      <!-- Error Message -->
      <div id="errorMsg" class="error"></div>

      <!-- Loading State -->
      <div id="loading" class="loading">
        <div class="spinner"></div>
        <p>กำลังดึงข้อมูล...</p>
      </div>

      <!-- Weather Display -->
      <div id="weatherContainer" class="weather-container">
        <div class="weather-header">
          <div class="city-name" id="cityName">--</div>
          <div class="update-time" id="updateTime">--</div>
        </div>

        <div class="temperature-section">
          <div class="weather-description" id="description">--</div>
          <div class="temperature" id="temperature">--°C</div>
          <div class="feels-like" id="feelsLike">--</div>
        </div>

        <div class="weather-details">
          <div class="detail-card">
            <div class="detail-label">💧 ความชื้น</div>
            <div class="detail-value" id="humidity">--%</div>
          </div>
          <div class="detail-card">
            <div class="detail-label">💨 ความเร็วลม</div>
            <div class="detail-value" id="windSpeed">-- m/s</div>
          </div>
          <div class="detail-card">
            <div class="detail-label">🔽 ความกดอากาศ</div>
            <div class="detail-value" id="pressure">-- hPa</div>
          </div>
          <div class="detail-card">
            <div class="detail-label">👁️ ระยะมองเห็น</div>
            <div class="detail-value" id="visibility">-- km</div>
          </div>
        </div>

        <div class="hourly-forecast" id="hourlySection" style="display: none;">
          <div class="hourly-title">
            📈 อุณหภูมิรายชั่วโมง (24 ชั่วโมงข้างหน้า)
          </div>
          <div class="hourly-list" id="hourlyList"></div>
        </div>
      </div>

      <!-- Empty State -->
      <div id="emptyState" class="empty-state">
        <div class="empty-icon">🌍</div>
        <div class="empty-text">ค้นหาเมืองเพื่อดูสภาพอากาศ</div>
      </div>
    </div>
    <script src="app.js"></script>
  </body>
</html>
```

---

## Step 2: สร้าง CSS

สร้างไฟล์ `styles.css`:

```css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  padding: 20px;
}

.container {
  max-width: 700px;
  margin: 0 auto;
  background: white;
  border-radius: 20px;
  padding: 30px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

h1 {
  text-align: center;
  color: #333;
  margin-bottom: 30px;
  font-size: 32px;
}

/* Search Section */
.search-section {
  margin-bottom: 30px;
}

.search-box {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}

#cityInput {
  flex: 1;
  padding: 12px;
  border: 2px solid #ddd;
  border-radius: 8px;
  font-size: 16px;
  transition: border-color 0.3s;
}

#cityInput:focus {
  outline: none;
  border-color: #667eea;
}

#searchBtn {
  padding: 12px 30px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 16px;
  font-weight: bold;
  transition: all 0.3s;
}

#searchBtn:hover {
  background: #5568d3;
  transform: translateY(-2px);
  box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}

#searchBtn:active {
  transform: scale(0.98);
}

#searchBtn.loading {
  background: #ff9800;
  cursor: not-allowed;
}

/* Recent Cities */
.recent-cities {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.city-tag {
  background: #f0f0f0;
  padding: 8px 15px;
  border-radius: 20px;
  cursor: pointer;
  transition: all 0.3s;
  font-size: 14px;
  border: 2px solid transparent;
}

.city-tag:hover {
  background: #e0e0e0;
  border-color: #667eea;
}

/* Weather Display */
.weather-container {
  display: none;
  animation: fadeIn 0.5s ease;
}

.weather-container.show {
  display: block;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.weather-header {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 30px;
  border-radius: 15px;
  margin-bottom: 20px;
  text-align: center;
}

.city-name {
  font-size: 32px;
  margin-bottom: 10px;
  font-weight: bold;
}

.update-time {
  font-size: 12px;
  opacity: 0.8;
}

/* Temperature Display */
.temperature-section {
  background: #f8f9fa;
  padding: 30px;
  border-radius: 15px;
  text-align: center;
  margin-bottom: 20px;
}

.temperature {
  font-size: 80px;
  font-weight: bold;
  color: #667eea;
  line-height: 1;
  margin: 10px 0;
}

.weather-description {
  font-size: 20px;
  color: #666;
  margin-bottom: 10px;
  text-transform: capitalize;
}

.feels-like {
  font-size: 14px;
  color: #999;
}

/* Details Grid */
.weather-details {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 15px;
  margin-bottom: 20px;
}

.detail-card {
  background: #f8f9fa;
  padding: 20px;
  border-radius: 10px;
  border-left: 4px solid #667eea;
}

.detail-label {
  color: #999;
  font-size: 12px;
  margin-bottom: 8px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.detail-value {
  color: #333;
  font-size: 24px;
  font-weight: bold;
}

/* Status Messages */
.loading {
  text-align: center;
  padding: 30px;
  color: #667eea;
  display: none;
}

.loading.show {
  display: block;
}

.spinner {
  border: 4px solid #f0f0f0;
  border-top: 4px solid #667eea;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  animation: spin 1s linear infinite;
  margin: 0 auto 10px;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.error {
  background: #f8d7da;
  color: #721c24;
  padding: 15px;
  border-radius: 8px;
  margin-bottom: 20px;
  border: 1px solid #f5c6cb;
  display: none;
}

.error.show {
  display: block;
}

/* Hourly Forecast */
.hourly-forecast {
  background: #f8f9fa;
  padding: 20px;
  border-radius: 10px;
  margin-bottom: 20px;
}

.hourly-title {
  font-weight: bold;
  margin-bottom: 15px;
  color: #333;
  font-size: 16px;
}

.hourly-list {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  padding-bottom: 10px;
}

.hourly-item {
  background: white;
  padding: 10px;
  border-radius: 8px;
  border: 1px solid #ddd;
  min-width: 80px;
  text-align: center;
  white-space: nowrap;
}

.hourly-time {
  font-size: 12px;
  color: #999;
  margin-bottom: 5px;
}

.hourly-temp {
  font-size: 18px;
  font-weight: bold;
  color: #667eea;
}

/* Empty State */
.empty-state {
  text-align: center;
  padding: 50px 20px;
  color: #999;
}

.empty-icon {
  font-size: 60px;
  margin-bottom: 15px;
}

.empty-text {
  font-size: 18px;
}

/* Responsive */
@media (max-width: 600px) {
  .container {
    padding: 20px;
  }

  .weather-details {
    grid-template-columns: 1fr;
  }

  .temperature {
    font-size: 60px;
  }

  .city-name {
    font-size: 24px;
  }
}

.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;
  }
}
```

---

## Step 3: สร้าง JavaSCript Logic

สร้างไฟล์ `app.js`:

```javascript
// ================================
//  Weather App with Fetch API
// ================================

// State
let state = {
  currentCity: null,
  latitude: null,
  longitude: null,
  recentCities: JSON.parse(localStorage.getItem("recentCities")) || [],
  autoRefreshInterval: null,
};

// 🎯 DOM Elements
const cityInput = document.getElementById("cityInput");
const searchBtn = document.getElementById("searchBtn");
const weatherContainer = document.getElementById("weatherContainer");
const loading = document.getElementById("loading");
const errorMsg = document.getElementById("errorMsg");
const emptyState = document.getElementById("emptyState");
const recentCitiesDiv = document.getElementById("recentCities");

// ================================
// 🔧 Helper Functions
// ================================

function showNotification(message) {
  const notification = document.createElement("div");
  notification.className = "notification";
  notification.textContent = message;
  document.body.appendChild(notification);

  setTimeout(() => {
    notification.remove();
  }, 3000);
}

function showLoading() {
  loading.classList.add("show");
  errorMsg.classList.remove("show");
}

function hideLoading() {
  loading.classList.remove("show");
}

function showError(message) {
  errorMsg.textContent = "❌ " + message;
  errorMsg.classList.add("show");
  weatherContainer.classList.remove("show");
  emptyState.style.display = "block";
}

function getCurrentTime() {
  const now = new Date();
  return now.toLocaleString("th-TH", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
  });
}

// ================================
// 🌍 Geocoding Functions
// ================================

// ดึง Latitude/Longitude จากชื่อเมือง
async function geocodeCity(cityName) {
  try {
    showLoading();

    const response = await fetch(
      `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(
        cityName,
      )}&count=1&language=en&format=json`,
    );

    if (!response.ok) {
      throw new Error("Network response was not ok");
    }

    const data = await response.json();

    if (!data.results || data.results.length === 0) {
      throw new Error("ไม่พบเมืองนี้");
    }

    const location = data.results[0];
    return {
      name: location.name,
      country: location.country,
      latitude: location.latitude,
      longitude: location.longitude,
    };
  } catch (error) {
    showError(error.message);
    throw error;
  }
}

// ================================
// 🌡️ Weather Functions
// ================================

// ดึงข้อมูลสภาพอากาศ
async function fetchWeather(latitude, longitude, cityInfo) {
  try {
    showLoading();

    const response = await fetch(
      `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,pressure_msl,visibility&timezone=auto&hourly=temperature_2m`,
    );

    if (!response.ok) {
      throw new Error("Failed to fetch weather data");
    }

    const data = await response.json();
    hideLoading();

    // อัปเดต State
    state.currentCity = cityInfo;
    state.latitude = latitude;
    state.longitude = longitude;

    // บันทึก recent cities
    saveRecentCity(cityInfo);

    // แสดงผลข้อมูล
    displayWeather(data, cityInfo);
  } catch (error) {
    showError(error.message);
  }
}

// ================================
// 🎨 Display Functions
// ================================

function getWeatherDescription(code) {
  const weatherCodes = {
    0: "☀️ ท้องฟ้าแจ่มใส",
    1: "🌤️ เมฆเล็กน้อย",
    2: "⛅ เมฆครึ่งหนึ่ง",
    3: "☁️ เมฆมาก",
    45: "🌫️ หมอก",
    48: "🌫️ หมอก",
    51: "🌧️ ฝนเล็กน้อย",
    53: "🌧️ ฝนปานกลาง",
    55: "🌧️ ฝนหนัก",
    61: "🌧️ ฝนเล็กน้อย",
    63: "🌧️ ฝนปานกลาง",
    65: "⛈️ ฝนหนัก",
    71: "❄️ หิมะเล็กน้อย",
    73: "❄️ หิมะปานกลาง",
    75: "❄️ หิมะหนัก",
    80: " ฝนแต่อากาศส่วนใหญ่ปกติ",
    81: "⛈️ ฝนเล็กน้อย",
    82: "⛈️ ฝนหนัก",
    85: "🌨️ หิมะและฝนปนกัน",
    86: "🌨️ หิมะหนัก",
    95: "⛈️ พายุฝนฟ้าคะนอง",
    96: "⛈️ พายุฝนฟ้าคะนอง",
    99: "⛈️ พายุฝนฟ้าคะนองหนัก",
  };
  return weatherCodes[code] || "🌍 สภาพอากาศไม่ชัดเจน";
}

function displayWeather(data, cityInfo) {
  const current = data.current;
  const hourly = data.hourly;

  // Update Header
  document.getElementById("cityName").textContent =
    `${cityInfo.name}, ${cityInfo.country}`;
  document.getElementById("updateTime").textContent =
    `📍 อัปเดตเมื่อ ${getCurrentTime()}`;

  // Update Temperature
  const description = getWeatherDescription(current.weather_code);
  document.getElementById("description").textContent = description;
  document.getElementById("temperature").textContent =
    Math.round(current.temperature_2m) + "°C";
  document.getElementById("feelsLike").textContent =
    `รู้สึก ${Math.round(current.apparent_temperature)}°C`;

  // Update Details
  document.getElementById("humidity").textContent =
    current.relative_humidity_2m + "%";
  document.getElementById("windSpeed").textContent =
    current.wind_speed_10m + " m/s";
  document.getElementById("pressure").textContent =
    current.pressure_msl + " hPa";
  document.getElementById("visibility").textContent =
    (current.visibility / 1000).toFixed(1) + " km";

  // Display Hourly Forecast
  displayHourlyForecast(hourly);

  // Show Weather Container
  weatherContainer.classList.add("show");
  emptyState.style.display = "none";
  errorMsg.classList.remove("show");
}

function displayHourlyForecast(hourly) {
  const hourlyList = document.getElementById("hourlyList");
  const hourlySection = document.getElementById("hourlySection");

  hourlyList.innerHTML = "";

  // ดึงเฉพาะ 24 ชั่วโมงแรก
  for (let i = 0; i < 24; i += 3) {
    const time = hourly.time[i];
    const temp = hourly.temperature_2m[i];

    const hour = new Date(time).getHours();
    const hourlyItem = document.createElement("div");
    hourlyItem.className = "hourly-item";
    hourlyItem.innerHTML = `
          <div class="hourly-time">${hour}:00</div>
          <div class="hourly-temp">${Math.round(temp)}°</div>
        `;

    hourlyList.appendChild(hourlyItem);
  }

  hourlySection.style.display = "block";
}

// ================================
// 💾 LocalStorage Functions
// ================================

function saveRecentCity(cityInfo) {
  // ลบซ้ำ
  state.recentCities = state.recentCities.filter(
    (city) => city.name !== cityInfo.name,
  );

  // เพิ่มหน้า
  state.recentCities.unshift(cityInfo);

  // เก็บแค่ 5 เมืองล่าสุด
  if (state.recentCities.length > 5) {
    state.recentCities.pop();
  }

  localStorage.setItem("recentCities", JSON.stringify(state.recentCities));
  renderRecentCities();
}

function renderRecentCities() {
  recentCitiesDiv.innerHTML = "";

  if (state.recentCities.length === 0) return;

  const label = document.createElement("div");
  label.style.width = "100%";
  label.style.fontSize = "12px";
  label.style.color = "#999";
  label.style.marginBottom = "10px";
  label.style.textTransform = "uppercase";
  label.textContent = "🕐 ค้นหาล่าสุด:";
  recentCitiesDiv.appendChild(label);

  state.recentCities.forEach((city) => {
    const tag = document.createElement("div");
    tag.className = "city-tag";
    tag.textContent = city.name;
    tag.addEventListener("click", async () => {
      await fetchWeather(city.latitude, city.longitude, city);
    });
    recentCitiesDiv.appendChild(tag);
  });
}

// ================================
// 🎬 Event Listeners
// ================================

async function searchCity() {
  const cityName = cityInput.value.trim();

  if (!cityName) {
    showError("กรุณากรอกชื่อเมือง");
    return;
  }

  try {
    const cityInfo = await geocodeCity(cityName);
    await fetchWeather(cityInfo.latitude, cityInfo.longitude, cityInfo);
    cityInput.value = "";
    showNotification(`✅ ค้นหา ${cityInfo.name} สำเร็จ`);
  } catch (error) {
    // Error already shown in geocodeCity
  }
}

searchBtn.addEventListener("click", searchCity);

cityInput.addEventListener("keypress", (e) => {
  if (e.key === "Enter") {
    searchCity();
  }
});

// Initialize
renderRecentCities();
```

---

## Run App

### 1 เปิด index.html ด้วย Live Server

ใน VS Code: `Alt + L` → `Alt + O`

### 2 ทดสอบ

```
[กรอก "Bangkok"]
[คลิก ค้นหา]
⏳ กำลังดึงข้อมูล...
(หลังจาก 1-2 วินาที)
✅ แสดงข้อมูลสภาพอากาศ Bangkok
  - อุณหภูมิ: 28°C
  - ความชื้น: 75%
  - ความเร็วลม: 3.5 m/s
  - อุณหภูมิรายชั่วโมง: 24h forecast
```

### 3 ลองค้นหา

- Bangkok 🇹🇭
- Tokyo 🇯🇵
- London 🇬🇧
- New York 🇺🇸
- Paris 🇫🇷

---

## Code Explanation

### ส่วน Async/Await ที่สำคัญ

**1 Geocoding - แปลงชื่อเมือง → Latitude/Longitude**

```javascript
async function geocodeCity(cityName) {
  try {
    const response = await fetch(
      `https://geocoding-api.open-meteo.com/v1/search?name=${cityName}...`,
    );
    const data = await response.json();
    return {
      name: location.name,
      latitude: location.latitude,
      longitude: location.longitude,
    };
  } catch (error) {
    showError(error.message);
  }
}
```

**2 Fetch Weather - ดึงข้อมูลสภาพอากาศ**

```javascript
async function fetchWeather(latitude, longitude) {
  try {
    const response = await fetch(
      `https://api.open-meteo.com/v1/forecast?latitude=${latitude}...`,
    );
    const data = await response.json();
    displayWeather(data); // แสดงผล
  } catch (error) {
    showError(error.message);
  }
}
```

**3 Search - ค้นหาเมือง**

```javascript
async function searchCity() {
  const cityInfo = await geocodeCity(cityName);      // ขั้น 1
  await fetchWeather(cityInfo.latitude, ...);        // ขั้น 2
  showNotification("✅ ค้นหาสำเร็จ");               // ขั้น 3
}
```

---

## API ที่ใช้

### 1 Geocoding API (หา Lat/Lon)

```
GET https://geocoding-api.open-meteo.com/v1/search?name={city}

Response:
{
  "results": [
    {
      "name": "Bangkok",
      "latitude": 13.7563,
      "longitude": 100.5018,
      "country": "Thailand"
    }
  ]
}
```

### 2 Weather Forecast API

```
GET https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&current=...

Response:
{
  "current": {
    "temperature_2m": 28.5,
    "relative_humidity_2m": 75,
    "wind_speed_10m": 3.5,
    "weather_code": 3
  }
}
```

---

## Features ที่มี

| Feature               | รายละเอียด           |
| --------------------- | -------------------- |
| **Search**            | ค้นหาเมือง           |
| 📍 **Location**       | แสดงเมือง/ประเทศ     |
| 🌡️ **Temperature**    | อุณหภูมิปัจจุบัน     |
| 💧 **Humidity**       | ความชื้นอากาศ        |
| 💨 **Wind Speed**     | ความเร็วลม           |
| 🔽 **Pressure**       | ความกดอากาศ          |
| 👁️ **Visibility**     | ระยะมองเห็น          |
| 📈 **Hourly**         | อุณหภูมิรายชั่วโมง   |
| 💾 **History**        | บันทึก 5 เมืองล่าสุด |
| ⏳ **Loading**        | Spinner animation    |
| ❌ **Error Handling** | แสดง error message   |

---

## Testing Checklist

- [ ] ค้นหาเมือง Bangkok
- [ ] ดึงข้อมูลและแสดงผล
- [ ] Loading spinner ทำงาน
- [ ] โปรแกรมแสดง temp, humidity, wind
- [ ] Hourly forecast แสดง 24 ชั่วโมง
- [ ] ค้นหาเมืองอื่น
- [ ] Recent cities เก็บได้
- [ ] Refresh หน้า recent cities ยังมีอยู่
- [ ] Error handling (ค้นหาเมืองไม่มี)
- [ ] Responsive ใน mobile

---

## Enhancement Ideas

- [ ] **Favorite Cities** - บันทึกเมือง favorite
- [ ] **Dark Mode** - โหมดกลางคืน
- [ ] **Alerts** - เตือนสภาพอากาศเลวร้าย
- [ ] **Graphs** - แสดงกราฟอุณหภูมิ
- [ ] **Multiple Units** - Celsius/Fahrenheit
- [ ] **Maps** - แสดง map ของเมือง
- [ ] **Push Notifications** - แจ้งเตือน
- [ ] **Offline Mode** - ใช้ได้แม้ปลั๊ก

---

## API Resources

- [Open-Meteo API Docs](https://open-meteo.com/en/docs)
- [Geocoding API](https://open-meteo.com/en/docs/geocoding-api)
- [Weather Forecast API](https://open-meteo.com/en/docs/forecast-api)
- [MDN: Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)

---

## คำถามทดสอบความเข้าใจ (Assessment Questions)

### ส่วนที่ 1: Fetch API & HTTP Requests

**คำถาม 1.1**
Fetch API คืออะไร ความแตกต่างระหว่าง Fetch API และ XMLHttpRequest คืออะไร

**คำถาม 1.2**
โค้ดนี้ทำอะไร อธิบายการทำงานแต่ละขั้นตอน

```javascript
fetch(url)
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error(error));
```

**คำถาม 1.3**
ถ้า API response ล้มเหลว (network error) จะถูก handle ที่ไหน `.catch()` จะจับได้หรือไม่

---

### ส่วนที่ 2: Geocoding API

**คำถาม 2.1**
Geocoding API ทำหน้าที่อะไร ทำไมจึงต้องใช้ก่อนเรียก Weather API

```
https://geocoding-api.open-meteo.com/v1/search?name=Bangkok
```

**คำถาม 2.2**
When user searches "Bangkok" ควรส่ง request ไปที่ endpoint ไหนก่อน ได้ข้อมูลอะไรจาก response

**คำถาม 2.3**
ถ้า user ค้นหาเมือง "Nonthaburi" ซึ่งชื่อเดียวกับเมืองอื่น app จะตัดสินใจอย่างไร

---

### ส่วนที่ 3: Weather Data & Parameters

**คำถาม 3.1**
Weather API ต้องการ parameters อะไรบ้าง (latitude, longitude, ...อื่นๆ)

**คำถาม 3.2**

```javascript
const params =
  "?latitude=13.7&longitude=100.5&current=temperature_2m,humidity,weather_code";
```

Parameters นี้ request ข้อมูลอะไร ทำไมต้องใช้ weather_code

**คำถาม 3.3**
ความแตกต่างระหว่าง `current` weather และ `hourly` forecast คืออะไร

---

### ส่วนที่ 4: Async/Await Pattern

**คำถาม 4.1**
ทำไมจึงใช้ `async/await` แทน `.then().catch()` มีข้อดีอะไรบ้าง

```javascript
async function getWeather(city) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
  }
}
```

**คำถาม 4.2**
`await` ทำอะไร ถ้าไม่ใช้ code จะเป็นยังไง

**คำถาม 4.3**
`try-catch` block ใช้เพื่ออะไร ตกลง error ที่ catch ได้อะไร

---

### ส่วนที่ 5: DOM Manipulation

**คำถาม 5.1**
เมื่อได้ข้อมูล weather มา app ต้องแสดงผล element ใดบ้าง

**คำถาม 5.2**
Loading spinner ปรากฏในไหน เมื่อไหร่ควรแสดง/ซ่อน

**คำถาม 5.3**
ถ้าต้องอัปเดต temperature display ทุก 10 นาที ควรใช้ `setInterval()` หรือ `setTimeout()`

---

### ส่วนที่ 6: Data Storage (LocalStorage)

**คำถาม 6.1**
LocalStorage ใช้เพื่ออะไร บันทึก recent cities ยังไง

```javascript
localStorage.setItem("recentCities", JSON.stringify(cities));
```

**คำถาม 6.2**
ทำไมต้องใช้ `JSON.stringify()` ถ้าเก็บ object โดยตรงจะเป็นอย่างไร

**คำถาม 6.3**
เมื่อ refresh หน้า app ควร restore recent cities จากไหน โค้ดเป็นยังไง

---

### ส่วนที่ 7: Error Handling & Edge Cases

**คำถาม 7.1**
ถ้า user ค้นหาเมืองที่ไม่มี (เช่น "XYZ") app จะแสดง error อย่างไร

**คำถาม 7.2**
ถ้า network ไม่มี (offline) fetch จะ throw error หรือ return success

**คำถาม 7.3**
เมื่อ hover หรือ click ที่ recent city button โปรแกรมจะทำอะไร

---

### ส่วนที่ 8: Weather Display

**คำถาม 8.1**
Weather code 0 = Clear sky, 1 = Partly cloudy, 80 = Drizzle... เลือก icon/emoji ยังไง

**คำถาม 8.2**
ความแตกต่างระหว่าง อุณหภูมิ (°C), ความชื้น (%), ความเร็วลม (m/s) คืออะไร

**คำถาม 8.3**
Hourly forecast แสดง 24 ชั่วโมง ต้องเลือก element ไหนจาก response

---

### ส่วนที่ 9: Responsive Design & Mobile

**คำถาม 9.1**
ใน mobile screen weather card ควร มี layout อย่างไร (vertical/horizontal)

**คำถาม 9.2**
เมื่อผู้ใช้ rotate device grid/flex ควรตัดสินใจอย่างไร

**คำถาม 9.3**
Input field และ button บน mobile ต้องมี font size เท่าไร (ควรมากพอให้แตะง่าย)

---

### ส่วนที่ 10: Code Architecture & Enhancement

**คำถาม 10.1**
ถ้าต้องสร้าง Weather class เพื่อจัดการ logic อย่างไร

```javascript
class WeatherApp {
  async searchCity(cityName) {}
  async fetchWeather(lat, lon) {}
  displayWeather(data) {}
}
```

**คำถาม 10.2**
วิธีเพิ่ม Favorite Cities feature ยังไง ต้องแก้ไข HTML/CSS/JS ส่วนไหนบ้าง

**คำถาม 10.3**
วิธีเพิ่ม Dark Mode ด้วย CSS variables ได้อย่างไร

---

## เกณฑ์การประเมิน

| ระดับคะแนน    | เกณฑ์                                 |
| ------------- | ------------------------------------- |
| **ผ่าน**      | ตอบคำถาม 1-5 ข้อ + app ทำงาน          |
| **ดี**        | ตอบ 7-10 ข้อ + เข้าใจ Fetch API       |
| **ดีมาก**     | ตอบ 12-15 ข้อ + async/await clear     |
| **ยอดเยี่ยม** | ตอบ 18+ ข้อ + สร้าง class/enhancement |

---

## Learning Outcomes

เมื่อจบปฏิบัติการนี้ นิสิตควรเข้าใจ:

✅ Fetch API และ HTTP requests  
✅ Async/Await pattern ในการจัดการ asynchronous operations  
✅ Geocoding และ Weather data structure  
✅ LocalStorage สำหรับเก็บข้อมูลเบราว์เซอร์  
✅ Error handling และ edge cases  
✅ DOM manipulation การแสดงข้อมูล  
✅ Responsive design บน mobile  
✅ Code architecture patterns (Classes, Separation of concerns)  
✅ การ debug network requests  
✅ API integration best practices
