iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0
自我挑戰組

給愛追劇的你:30天互動網站挑戰系列 第 10

Day 10:收藏按鈕互動+收藏清單頁

  • 分享至 

  • xImage
  •  

昨天我們完成了搜尋與類型篩選,今天要把「喜歡」的作品記錄下來。雖然只是幾行 LocalStorage 操作,但它會讓網站從「展示」變成「可用」。


1. 更新首頁腳本 js/app.js:加入收藏邏輯

// js/app.js — Day 8 + Day 9 + Day 10(收藏)
(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: "劇情" };

  // 將資料曝露給收藏頁使用
  window.DRAMA_SHOWS = SHOWS;
  window.DRAMA_LABELS = LABELS;

  // ====== LocalStorage(收藏)======
  const FAV_KEY = 'dramaweb:favIds';
  function loadFavSet(){
    try{ return new Set(JSON.parse(localStorage.getItem(FAV_KEY) || '[]')); }
    catch(e){ return new Set(); }
  }
  function saveFavSet(set){
    localStorage.setItem(FAV_KEY, JSON.stringify([...set]));
  }
  let favSet = loadFavSet();
  function isFav(id){ return favSet.has(id); }
  function toggleFav(id){
    if (favSet.has(id)) favSet.delete(id); else favSet.add(id);
    saveFavSet(favSet);
  }

  // ====== 狀態(Day 9 搜尋)======
  let keyword = "";

  // ====== DOM 快取(可能在不同頁,故做存在檢查)======
  const $cards   = $("#cards");         // 首頁清單容器(收藏頁沒有)
  const $buttons = $(".filter-btn");
  const $chips   = $("#selectedTags");
  const $clear   = $("#btnClear");
  const $search       = $("#searchInput");
  const $searchClear  = $("#btnSearchClear");
  const $searchForm   = $("#searchForm");

  const onHome = $cards.length > 0;     // 僅首頁需要渲染與篩選

  // ====== 渲染 ======
  function cardTemplate(s) {
    const tags = s.genres.map(g => LABELS[g] || g).join(" / ");
    const active = isFav(s.id);
    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 ${active ? 'is-active' : ''}" type="button" aria-pressed="${active}">
            ${active ? '★ 已收藏' : '☆ 收藏'}
          </button>
        </div>
      </article>
    `;
  }

  function render(list) {
    if (!onHome) return; // 收藏頁不在這裡渲染
    if (!list.length) {
      $cards.html(`<div class="empty">沒有符合條件的劇集</div>`);
      return;
    }
    $cards.html(list.map(cardTemplate).join(""));
  }

  // ====== 取得狀態 & 過濾(Day 8/9)======
  function getActiveGenres() {
    return $(".filter-btn.active").map((_, el) => $(el).data("genre")).get();
  }
  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() {
    if (!$clear.length) return;
    const any = $(".filter-btn.active").length > 0;
    $clear.attr("aria-disabled", any ? "false" : "true");
  }
  function addChip(genre) {
    if (!$chips.length) return;
    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) {
    if (!$chips.length) return;
    $chips.find(`[data-genre="${genre}"]`).remove();
  }

  // ====== 綁定互動(首頁用)======
  function bindUI() {
    if (onHome) {
      // 類型切換
      $(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)
      $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());
      });

      // 收藏切換(Day 10)
      $(document).on("click", ".fav", function(){
        const $card = $(this).closest(".card");
        const id = $card.data("id");
        toggleFav(id);
        // 重新渲染分頁中的卡片以同步按鈕狀態
        render(applyFilters());
      });
    }
  }

  // ====== 啟動(僅首頁渲染)======
  $(function () {
    bindUI();
    updateClearState();
    if (onHome) render(SHOWS);
  });
})();

2. 在 style.css 最後加上收藏按鈕的「已收藏」狀態

/* Day 10:收藏按鈕的已收藏樣式 */
.fav.is-active{
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}

3. 建立收藏清單頁 favorite.html

新增一個頁面,讀取 LocalStorage 的收藏 id,對照節目資料,渲染到頁面。也支援在此頁「取消收藏」。

直接建立 favorite.html,內容如下(記得修改導覽連結與檔名符合你的專案結構):

<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>DramaWeb|我的收藏</title>
  <link rel="stylesheet" href="style.css"/>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
  <header>
    <button class="menu" aria-label="開啟選單"><span>&#9776;</span></button>
    <h1>DramaWeb</h1>
    <nav>
      <ul>
        <li><a href="front.html">首頁</a></li>
        <li><a href="type.html">類型篩選:</a></li>
        <li><a href="favorite.html" aria-current="page">收藏清單:</a></li>
        <li><a href="review.html">心得牆:</a></li>
      </ul>
    </nav>
  </header>

  <main id="main">
    <section class="news">
      <h2>我的收藏</h2>
      <p>在這裡集中查看你按下「已收藏」的劇集,想取消也可以直接點。</p>
    </section>

    <section id="favCards" class="cards">
      <!-- 由 favorites.js 動態渲染 -->
    </section>

    <section class="newsletter">
      <button id="btnFavClear" class="btn" type="button">清空全部收藏</button>
    </section>
  </main>

  <footer>
    <p>DramaWeb © 2025 All Rights Reserved.</p>
  </footer>

  <!-- 先載入 app.js(提供 DRAMA_SHOWS 資料) -->
  <script src="js/app.js"></script>
  <!-- 再載入收藏頁邏輯 -->
  <script src="js/favorites.js"></script>
</body>
</html>

4. 新增 js/favorites.js:渲染收藏列表+取消收藏

建立新檔 js/favorites.js,貼上以下內容:

// js/favorites.js — Day 10:收藏清單頁
(function(){
  const FAV_KEY = 'dramaweb:favIds';

  function loadFavSet(){
    try{ return new Set(JSON.parse(localStorage.getItem(FAV_KEY) || '[]')); }
    catch(e){ return new Set(); }
  }
  function saveFavSet(set){
    localStorage.setItem(FAV_KEY, JSON.stringify([...set]));
  }

  const SHOWS = window.DRAMA_SHOWS || [];
  const LABELS = window.DRAMA_LABELS || {};
  const $list = $('#favCards');
  const $btnClear = $('#btnFavClear');

  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 is-active" type="button" aria-pressed="true">★ 已收藏(點我取消)</button>
        </div>
      </article>
    `;
  }

  function render(){
    const favSet = loadFavSet();
    if (!favSet.size){
      $list.html(`<div class="empty">目前沒有收藏的劇集</div>`);
      return;
    }
    // 依收藏順序渲染(以存放順序為主)
    const order = [...favSet];
    const map = new Map(SHOWS.map(s => [s.id, s]));
    const items = order.map(id => map.get(id)).filter(Boolean);
    if (!items.length){
      $list.html(`<div class="empty">找不到對應的劇集資料(可能資料已變更)</div>`);
      return;
    }
    $list.html(items.map(cardTemplate).join(''));
  }

  // 取消收藏
  $(document).on('click', '#favCards .fav', function(){
    const id = $(this).closest('.card').data('id');
    const set = loadFavSet();
    set.delete(id);
    saveFavSet(set);
    render();
  });

  // 清空全部
  $btnClear.on('click', function(){
    const set = loadFavSet();
    if (!set.size) return;
    if (!confirm('確定要清空全部收藏嗎?')) return;
    localStorage.setItem(FAV_KEY, '[]');
    render();
  });

  // 初始化
  $(function(){ render(); });
})();

上一篇
Day 09:關鍵字搜尋
系列文
給愛追劇的你:30天互動網站挑戰10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言