iT邦幫忙

2025 iThome 鐵人賽

0
自我挑戰組

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

Day 12:心得牆永久保存

  • 分享至 

  • xImage
  •  

1.在 style.css 末尾補上兩段(可選)

讓「清空全部」與空狀態更清楚(若 Day 11 已有類似就可略過):

/* Day 12:清空全部按鈕(危險動作) */
.btn-warn{
  border: 1px solid #ef4444; color: #fff; background: #ef4444;
  border-radius: 10px; padding: 8px 12px; cursor: pointer;
}
.btn-warn:hover{ background: #dc2626; border-color:#dc2626; }

/* 空狀態(若未定義) */
.empty{
  text-align: center; color: var(--muted);
  padding: 24px 8px; border: 1px dashed var(--border);
  border-radius: var(--radius); background: var(--surface);
}

你也可以在 review.html 的「最新心得」區塊上方,額外加一個「清空全部」按鈕(若想要):

<div class="review-actions" style="margin-bottom:10px;">
  <button id="btnClearAll" class="btn-warn" type="button">清空全部心得</button>
</div>

2.用 LocalStorage 重寫 js/reviews.js

直接覆蓋 你現有的 js/reviews.js。此版包含:載入、保存、刪除、清空、字數、驗證、Ctrl/⌘+Enter 快速送出。

// js/reviews.js — Day 12:心得牆永久保存(LocalStorage)
(function(){
  // 來自 app.js
  const SHOWS  = window.DRAMA_SHOWS || [];
  const LABELS = window.DRAMA_LABELS || {};

  // 儲存鍵
  const RV_KEY = 'dramaweb:reviews:v1';

  // DOM
  const $form    = $('#reviewForm');
  const $title   = $('#titleInput');
  const $rating  = $('#ratingInput');
  const $name    = $('#nameInput');
  const $content = $('#contentInput');
  const $count   = $('#contentCount');
  const $msg     = $('#formMsg');
  const $list    = $('#reviewList');
  const $reset   = $('#btnResetReview');
  const $clearAll= $('#btnClearAll'); // 可能不存在,容錯處理

  // 工具
  function esc(str){
    return String(str)
      .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
      .replace(/"/g,'&quot;').replace(/'/g,'&#39;');
  }
  function nowISO(){ return new Date().toISOString(); }

  // LocalStorage:load/save
  function loadReviews(){
    try{
      const raw = localStorage.getItem(RV_KEY);
      const arr = raw ? JSON.parse(raw) : [];
      // 兼容:確保必要欄位存在
      return Array.isArray(arr) ? arr.filter(Boolean) : [];
    }catch(e){ return []; }
  }
  function saveReviews(list){
    localStorage.setItem(RV_KEY, JSON.stringify(list));
  }

  // 狀態:所有心得(持久化)
  let REVIEWS = loadReviews(); // [{id,title,rating,name,content,genres,createdAt}...]

  // datalist
  function buildDatalist(){
    const $dl = $('#showList').empty();
    SHOWS.forEach(s => $dl.append(`<option value="${s.title}"></option>`));
  }

  // render
  function render(){
    if (!REVIEWS.length){
      $list.html(`<div class="empty">目前還沒有心得,快成為第一個分享的人吧!</div>`);
      return;
    }
    // 以建立時間排序(新到舊)
    const sorted = REVIEWS.slice().sort((a,b) => (b.createdAt || '').localeCompare(a.createdAt || ''));
    const html = sorted.map(item => {
      const tags = (item.genres || []).map(g => LABELS[g] || g).join(' / ');
      const metaRight = tags ? ` · ${tags}` : '';
      // 顯示日期(YYYY-MM-DD)
      const dateStr = (item.createdAt || '').slice(0,10);
      return `
        <article class="review-item" data-id="${item.id}">
          <div class="review-head">
            <h3 class="review-title">${esc(item.title)}</h3>
            <span class="review-meta">
              ⭐ ${item.rating} · ${esc(item.name || '匿名')}${metaRight}${dateStr ? ` · ${dateStr}` : ''}
            </span>
          </div>
          <p class="review-content">${esc(item.content)}</p>
          <div class="review-actions">
            <button class="btn-del" type="button">刪除</button>
          </div>
        </article>
      `;
    }).join('');
    $list.html(html);
  }

  // 即時字數
  $content.on('input', function(){ $count.text($(this).val().length); });

  // 驗證
  function setError($el, $errEl, cond, msg){
    if (cond){
      $el.attr('aria-invalid','true'); if (msg) $errEl.text(msg); $errEl.prop('hidden', false);
    } else {
      $el.attr('aria-invalid','false'); $errEl.prop('hidden', true);
    }
    return !cond;
  }
  function validate(){
    const titleVal = $title.val().trim();
    const ratingVal = parseFloat($rating.val());
    const contentVal = $content.val().trim();

    const okTitle   = setError($title,   $('#errTitle'),   !(titleVal.length >= 2 && titleVal.length <= 40));
    const okRating  = setError($rating,  $('#errRating'),  !(Number.isFinite(ratingVal) && ratingVal >= 1 && ratingVal <= 10));
    const okContent = setError($content, $('#errContent'), !(contentVal.length >= 30 && contentVal.length <= 1000));

    if (!okTitle)   { $title.focus();   return false; }
    if (!okRating)  { $rating.focus();  return false; }
    if (!okContent) { $content.focus(); return false; }
    return true;
  }

  function findShowByTitle(title){
    return SHOWS.find(s => s.title === title);
  }

  // 送出
  $form.on('submit', function(e){
    e.preventDefault();
    if (!validate()) return;

    const titleVal   = $title.val().trim();
    const ratingVal  = parseFloat($rating.val());
    const nameVal    = $name.val().trim() || '匿名';
    const contentVal = $content.val().trim();
    const match      = findShowByTitle(titleVal);

    const review = {
      id: 'rv_' + Date.now(),
      title: titleVal,
      rating: ratingVal.toFixed(1),
      name: nameVal,
      content: contentVal,
      genres: match ? match.genres : [],
      createdAt: nowISO()
    };

    REVIEWS.push(review);
    saveReviews(REVIEWS);

    // reset & 提示
    $form[0].reset();
    $count.text('0');
    $msg.text('已送出並保存!').prop('hidden', false);
    setTimeout(() => $msg.prop('hidden', true), 1500);

    render();
  });

  // Ctrl/⌘ + Enter 快速送出
  $(document).on('keydown', function(e){
    if ((e.ctrlKey || e.metaKey) && e.key === 'Enter'){
      $('#btnSubmitReview').click();
    }
  });

  // 清空表單
  $reset.on('click', function(){
    $form[0].reset();
    $count.text('0');
    ['#titleInput','#ratingInput','#contentInput'].forEach(sel => $(sel).attr('aria-invalid','false'));
    $('.error').prop('hidden', true);
  });

  // 刪除單筆(帶確認)
  $(document).on('click', '.review-item .btn-del', function(){
    const id = $(this).closest('.review-item').data('id');
    const idx = REVIEWS.findIndex(r => r.id === id);
    if (idx < 0) return;
    if (!confirm('確定要刪除此則心得嗎?')) return;
    REVIEWS.splice(idx, 1);
    saveReviews(REVIEWS);
    render();
  });

  // 清空全部(若頁面有此按鈕)
  if ($clearAll && $clearAll.length){
    $clearAll.on('click', function(){
      if (!REVIEWS.length) return;
      if (!confirm('確定要清空全部心得嗎?此動作無法復原。')) return;
      REVIEWS = [];
      saveReviews(REVIEWS);
      render();
    });
  }

  // 初始化
  $(function(){
    buildDatalist();
    render(); // 從 LocalStorage 載入並顯示
  });
})();

3.測試清單

  • 重新整理 review.html → 先前送出的心得還在。
  • 新增心得 → 出現在列表;再次整理後仍存在。
  • 刪除單筆 → 跳出確認;同意後刪除並保存。
    -(若有加)清空全部 → 跳出確認;同意後清空並保存。
  • Console 無錯誤。

上一篇
Day 11:心得表單(新增/驗證)
下一篇
Day 13:心得牆美化 +「載入更多」
系列文
給愛追劇的你:30天互動網站挑戰30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言