iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0

昨天我們讓清單用 JS 動起來,今天加上「即時搜尋」。雖然只是幾行 JS,但使用者體驗會大幅提升,也為之後的排序、收藏打好基礎。


1. 在篩選區加入「搜尋欄位」

把你目前的 <section class="filters">…</section> 改成下面這版(多了右側搜尋列與清除搜尋「×」):

<!-- 篩選按鈕群組 + 搜尋(Day 9) -->
<section class="filters" aria-label="類型篩選與搜尋">
  <div class="filter-group" role="group" aria-label="類型">
    <button class="filter-btn" data-genre="Romance" aria-pressed="false">愛情</button>
    <button class="filter-btn" data-genre="Comedy"  aria-pressed="false">喜劇</button>
    <button class="filter-btn" data-genre="Mystery" aria-pressed="false">懸疑</button>
    <button class="filter-btn" data-genre="Action"  aria-pressed="false">動作</button>
  </div>

  <div class="filter-actions">
    <!-- 搜尋列 -->
    <form id="searchForm" class="searchbar" role="search" autocomplete="off">
      <label class="visually-hidden" for="searchInput">搜尋片名</label>
      <input id="searchInput" type="search" placeholder="搜尋片名…" inputmode="search">
      <button id="btnSearchClear" class="btn btn-clear" type="button" aria-label="清除搜尋" aria-hidden="true">×</button>
    </form>

    <!-- 清除類型 -->
    <button class="btn btn-clear" id="btnClear" type="button" aria-disabled="true">清除篩選</button>
  </div>
</section>

<!-- 已選標籤列(保留 Day 6) -->
<section class="selected-bar" aria-live="polite" aria-atomic="true">
  <span class="label">已選:</span>
  <div id="selectedTags" class="chips"></div>
</section>

2. 在 style.css 末尾加入搜尋列樣式

(沿用 Day 5 的設計變數)

/* Day 9:搜尋列 */
.searchbar{
  display: inline-flex; align-items: center; gap: 8px;
  border: 1px solid var(--border); background: var(--surface);
  padding: 6px 10px; border-radius: 999px; margin-right: 10px;
}
#searchInput{
  border: none; outline: none; min-width: 180px; font-size: 1rem;
  background: transparent; color: var(--text);
}
#btnSearchClear{ padding: 4px 8px; }
#btnSearchClear[aria-hidden="true"]{ display: none; }

/* 手機排版微調 */
@media (max-width: 720px){
  .filter-actions{ width: 100%; display: flex; gap: 8px; margin-top: 8px; }
  .searchbar{ flex: 1; margin-right: 0; }
  #searchInput{ min-width: 0; width: 100%; }
}

3. 更新 js/app.js(合併:渲染 + 篩選 + chips + 搜尋)

把下方整段覆蓋你的 js/app.js(內含 Day 8 的功能並加入搜尋)。如果你前面有自訂資料,保留 SHOWS 陣列即可。

// js/app.js — Day 8 + Day 9:動態渲染 + 類型過濾 + 關鍵字搜尋
(function () {
  // ====== 假資料(可自行擴充)======
  const SHOWS = [
    { id: "tv_0001", title: "我們的戀愛日記", genres: ["Romance","Comedy"], rating: 8.2, cover: "https://picsum.photos/seed/drama1/640/360" },
    { id: "tv_0002", title: "霧中真相",       genres: ["Mystery","Drama"],  rating: 8.8, cover: "https://picsum.photos/seed/drama2/640/360" },
    { id: "tv_0003", title: "熱血行動",       genres: ["Action"],           rating: 7.9, cover: "https://picsum.photos/seed/drama3/640/360" },
    { id: "tv_0004", title: "笑到最後一刻",   genres: ["Comedy"],           rating: 7.6, cover: "https://picsum.photos/seed/drama4/640/360" },
    { id: "tv_0005", title: "青春課間",       genres: ["Drama"],            rating: 7.4, cover: "https://picsum.photos/seed/drama5/640/360" },
    { id: "tv_0006", title: "答案在遠方",     genres: ["Romance","Drama"],  rating: 8.5, cover: "https://picsum.photos/seed/drama6/640/360" },
    { id: "tv_0007", title: "夜行者",         genres: ["Mystery","Action"], rating: 8.1, cover: "https://picsum.photos/seed/drama7/640/360" },
    { id: "tv_0008", title: "辦公室日常",     genres: ["Comedy","Drama"],   rating: 7.8, cover: "https://picsum.photos/seed/drama8/640/360" },
    { id: "tv_0009", title: "追風少年",       genres: ["Action","Drama"],   rating: 7.7, cover: "https://picsum.photos/seed/drama9/640/360" },
  ];

  // 類型中文顯示
  const LABELS = { Romance: "愛情", Comedy: "喜劇", Mystery: "懸疑", Action: "動作", Drama: "劇情" };

  // ====== 狀態 ======
  let keyword = ""; // Day 9:關鍵字

  // ====== DOM 快取 ======
  const $cards   = $("#cards");
  const $buttons = $(".filter-btn");
  const $chips   = $("#selectedTags");
  const $clear   = $("#btnClear");

  const $search       = $("#searchInput");
  const $searchClear  = $("#btnSearchClear");
  const $searchForm   = $("#searchForm");

  // ====== 渲染 ======
  function cardTemplate(s) {
    const tags = s.genres.map(g => LABELS[g] || g).join(" / ");
    return `
      <article class="card" data-id="${s.id}">
        <img class="cover" loading="lazy" src="${s.cover}" alt="${s.title} 封面">
        <div class="card-body">
          <h3 class="title">${s.title}</h3>
          <p class="meta">${tags} · ⭐ ${s.rating}</p>
          <button class="fav" type="button" aria-pressed="false">★ 收藏</button>
        </div>
      </article>
    `;
  }

  function render(list) {
    if (!list.length) {
      $cards.html(`<div class="empty">沒有符合條件的劇集</div>`);
      return;
    }
    $cards.html(list.map(cardTemplate).join(""));
  }

  // ====== 取得狀態 ======
  function getActiveGenres() {
    return $(".filter-btn.active").map((_, el) => $(el).data("genre")).get();
  }

  // ====== 過濾邏輯: (類型 OR 多選) AND (關鍵字) ======
  function applyFilters() {
    const active = new Set(getActiveGenres());
    const kw = keyword.trim().toLowerCase();

    return SHOWS.filter(s => {
      const byGenre = active.size ? s.genres.some(g => active.has(g)) : true;
      const byKw    = kw ? s.title.toLowerCase().includes(kw) : true;
      return byGenre && byKw;
    });
  }

  // ====== chips 與清除鍵(沿用 Day 6)======
  function updateClearState() {
    const any = $(".filter-btn.active").length > 0;
    $clear.attr("aria-disabled", any ? "false" : "true");
  }

  function addChip(genre) {
    const text = LABELS[genre] || genre;
    if ($chips.find(`[data-genre="${genre}"]`).length) return;
    $chips.append(`
      <span class="chip" data-genre="${genre}">
        ${text}
        <button class="remove" type="button" aria-label="移除 ${text}">
          ×<span class="visually-hidden"> 移除 ${text}</span>
        </button>
      </span>
    `);
  }

  function removeChip(genre) {
    $chips.find(`[data-genre="${genre}"]`).remove();
  }

  // ====== 綁定互動 ======
  function bindUI() {
    // 類型:切換
    $(document).on("click", ".filter-btn", function () {
      const $btn = $(this);
      const genre = $btn.data("genre");
      const active = !$btn.hasClass("active");
      $btn.toggleClass("active", active).attr("aria-pressed", active);
      active ? addChip(genre) : removeChip(genre);
      updateClearState();
      render(applyFilters());
    }).on("keydown", ".filter-btn", function (e) {
      if (e.key === " " || e.key === "Enter") { e.preventDefault(); $(this).click(); }
    });

    // chips:移除單一
    $(document).on("click", ".chip .remove", function () {
      const $chip = $(this).closest(".chip");
      const genre = $chip.data("genre");
      $chip.remove();
      const $btn = $buttons.filter(`[data-genre="${genre}"]`);
      $btn.removeClass("active").attr("aria-pressed", "false");
      updateClearState();
      render(applyFilters());
    });

    // 類型:一鍵清除
    $clear.on("click", function () {
      if ($clear.attr("aria-disabled") === "true") return;
      $buttons.removeClass("active").attr("aria-pressed", "false");
      $chips.empty();
      updateClearState();
      render(applyFilters());
    });

    // ====== Day 9:搜尋 ======
    // 即時輸入 → 過濾;可用 Enter 阻止表單提交
    $search.on("input", function(){
      keyword = $(this).val();
      $searchClear.attr("aria-hidden", keyword.trim() ? "false" : "true");
      render(applyFilters());
    });

    $searchForm.on("submit", function(e){ e.preventDefault(); });

    $searchClear.on("click", function(){
      keyword = "";
      $search.val("");
      $searchClear.attr("aria-hidden", "true");
      render(applyFilters());
    });
  }

  // ====== 啟動 ======
  $(function () {
    render(SHOWS);      // 初次渲染全部
    bindUI();           // 綁定所有互動
    updateClearState(); // 初始化清除鍵狀態
  });
})();

上一篇
Day 08:用 JS 依「類型」過濾卡片
系列文
給愛追劇的你:30天互動網站挑戰9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言