學習筆記 / 每日反思 App

程式碼完整解說

每次讀不懂就回來翻這份。先看整體架構,再看細節。

第一章 這個 App 的架構是什麼?

整個 App 只有一個 .html 檔案,裡面同時包含三個部分:

部分寫在哪裡負責什麼
HTML<body> 裡面畫出頁面結構(表單、按鈕、分頁)
CSS<style> 標籤控制顏色、字型、間距、排版
JavaScript<script> 標籤處理邏輯、讀寫資料、更新畫面

資料存在 Supabase(雲端資料庫),JS 透過 API 讀寫,不管哪個裝置開,資料都一樣。

比喻:HTML 是房子的骨架,CSS 是油漆和裝潢,JavaScript 是電路(讓東西會動)。Supabase 是倉庫(存資料)。

第二章 連接 Supabase(雲端資料庫)

// 引入 Supabase 函式庫(從 CDN 載入,不需要自己下載)
// 這行寫在 <head> 裡
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>

// 建立連線
const { createClient } = supabase;

const supa = createClient(
  'https://你的專案.supabase.co',  // 資料庫網址
  '你的 anon key'                    // 公開金鑰(可以讀寫,但有權限控制)
);
supa 這個變數就是「資料庫的遙控器」。之後所有讀寫都透過它。

第三章 async / await — 等待雲端回應

從 Supabase 讀資料需要時間(網路延遲)。JS 用 async / await 處理這件事。

// ❌ 不行這樣寫——資料還沒回來就繼續跑了
const data = getEntry('2026-06-27');
console.log(data); // 會印出 undefined

// ✅ 正確寫法——await 會等資料回來才繼續
async function loadToday() {
  const e = await getEntry('2026-06-27');
  console.log(e); // 這時資料已經回來了
}
規則:
• 函式裡面有 await,函式本身就要加 async
await 只能用在 async 函式裡
• 想同時等兩件事,用 Promise.all([])
// 同時發出兩個請求,等兩個都回來
const [entries, habits] = await Promise.all([
  getEntries('2026-06-01', '2026-06-30'),
  getHabits()
]);
// 比分開 await 快,因為兩個同時進行

第四章 資料層函式(Supabase 讀寫)

讀取單筆資料

async function getEntry(date) {
  const { data } = await supa
    .from('reflection_entries')   // 選資料表
    .select('*')                  // 取所有欄位(* = all)
    .eq('date', date)             // WHERE date = '2026-06-27'
    .maybeSingle();               // 只取一筆(沒資料回 null,不報錯)
  return data;
}
這就是 SQL 的 JS 版:
SELECT * FROM reflection_entries WHERE date = '2026-06-27' LIMIT 1

寫入 / 更新資料

async function upsertEntry(date, fields) {
  const { error } = await supa
    .from('reflection_entries')
    .upsert(                        // upsert = insert OR update
      { date, ...fields },           // 展開 fields 物件的所有欄位
      { onConflict: 'date' }         // 如果 date 重複,就更新(不要重複新增)
    );
  if (error) toast('儲存失敗');    // 有錯誤才顯示提示
}
... (展開運算子):把物件的欄位全部攤開
{ date, ...fields } 等於把 date 和 fields 裡的所有欄位合在一起

第五章 三大陣列方法

.map().filter().forEach() — 學會這三個,80% 的資料處理都能搞定。


.map() — 把每個元素轉換成新的東西

遍歷陣列,每個元素套一個函式,回傳新陣列

const nums = [1, 2, 3];

const doubled = nums.map(n => n * 2);
// n => n * 2  叫做「箭頭函式」,意思是「每個 n,回傳 n × 2」
結果:[2, 4, 6]
原陣列 nums 不變:[1, 2, 3]

在反思 App 裡的實際用法:

// habits 是習慣陣列
const habits = [
  { id: '001', name: '讀書', color: '#3b82f6' },
  { id: '002', name: '運動', color: '#dc2626' },
];

// 把每個習慣轉成一個 HTML 點點字串
const dots = habits.map(h => {
  return `<div class="hdot" style="background:${h.color}"></div>`;
}).join('');
// .join('') 把陣列合成一個字串
結果:'<div class="hdot" style="background:#3b82f6"></div><div class="hdot" style="background:#dc2626"></div>'

.filter() — 只留下符合條件的元素

遍歷陣列,條件回傳 true 的才保留,回傳新陣列(長度 ≤ 原陣列)。

const scores = [45, 82, 60, 91, 33];

const passed = scores.filter(s => s >= 60);
// s >= 60 是條件,true 才留下
結果:[82, 60, 91]

在反思 App 裡的實際用法:

// 從所有習慣裡,找出今天打卡完成的
const log = { '001': true, '002': false }; // 今天的打卡紀錄

const done = habits.filter(h => log[h.id]);
// log[h.id] 是 true 的才留下(有打卡的習慣)

const pct = Math.round(done.length / habits.length * 100);
// 算出完成率百分比
done.length = 1(只有讀書打卡了)
pct = 50(50%)

.forEach() — 跑每個元素,但不回傳

.map() 很像,但不回傳新陣列,通常用來做「副作用」(例如更新畫面、打 log)。

const fruits = ['蘋果', '香蕉', '芒果'];

fruits.forEach(f => {
  console.log(f);  // 印出每個水果
});
蘋果
香蕉
芒果
(回傳值是 undefined)

在反思 App 裡的實際用法:

// 把五個問題的答案存到 fields 物件
const fields = {};

QS.forEach(q => {
  fields[q.field] = document.getElementById(q.id).value.trim();
  // 取出 textarea 的內容,去掉頭尾空白,存進 fields
});

// 結果:fields = { gratitude: '...', achievement: '...', ... }
何時用 .map(),何時用 .forEach()?

• 需要回傳新陣列 → 用 .map()
• 只是要「做某件事」,不需要新陣列 → 用 .forEach()

三個方法快速對照

方法記憶口訣回傳常用場景
.map() 每個元素變形 新陣列(同長度) 資料轉 HTML、計算新值
.filter() 篩選留下合格的 新陣列(較短或同長) 找完成的習慣、找特定日期
.forEach() 跑過去做事,不回傳 undefined 更新欄位、打 log

第六章 Template Literal(反引號字串)

這個語法在整個 App 大量使用,必須要懂。

// 舊寫法(用 + 拼字串,很醜)
const html = '<div class="' + cls + '">' + date + '</div>';

// 新寫法(Template Literal,用反引號 ` ` 包起來)
const html = `<div class="${cls}">${date}</div>`;
// ${ } 裡面可以放任何 JS 表達式
// ${ } 裡面可以放運算
const name = 'Balian';
const age = 41;
console.log(`${name} 今年 ${age} 歲,明年 ${age + 1} 歲`);
Balian 今年 41 歲,明年 42 歲

第七章 月曆格子整合習慣點點(本次核心修改)

這是這次改動的關鍵段,把反思月曆和習慣打點合在一起。

// 取得三份資料(同時發出,速度最快)
const [entries, habits, habitLogs] = await Promise.all([
  getEntries(...),     // 這個月的反思紀錄
  getHabits(),         // 所有習慣
  getLogsForMonth(y, m) // 這個月的打卡紀錄
]);

// 對每一天的格子...
cells.forEach(c => {
  const hlog = habitLogs[c.k] || {};
  // c.k 是日期字串(如 '2026-06-27')
  // || {} 避免那天沒資料時報錯

  const dots = habits.length > 0
    ? `<div class="hcal-dot-row">
        ${habits.slice(0, 6).map(h => {
          const hc = h.color || catColor(h.cat) || '#b0a898';
          return `<div class="hdot" style="${hlog[h.id] ? `background:${hc}` : ''}"></div>`;
        }).join('')}
       </div>`
    : '';
  // habits.length > 0 ? ... : ''  — 三元運算子
  // 有習慣才畫點,沒有習慣就空字串
  // hlog[h.id] 是 true → 上習慣的顏色;false → 不加 style(灰點)

  // 把日期數字和點點組合成 HTML
  calH += `<div class="${cls}" ${oc}>
    <span>${c.date}</span>
    ${dots}
  </div>`;
});
三元運算子條件 ? 成立時的值 : 不成立時的值

等同於 if/else,但可以寫在字串裡面:
hlog[h.id] ? `background:${hc}` : ''
意思:「打卡了就給顏色,沒打卡就給空字串(灰色)」

第八章 Toast 通知(小提示)

// HTML 裡有一個隱藏的 div
<div id="toast" style="display:none; ..."></div>

// JS 函式:讓它出現,2 秒後消失
function toast(msg, ms = 2200) {
  const t = document.getElementById('toast');
  t.textContent = msg;         // 設定文字
  t.style.display = 'block';   // 顯示
  clearTimeout(t._timer);      // 清掉上次的計時器(避免重疊)
  t._timer = setTimeout(() => {
    t.style.display = 'none'; // ms 毫秒後隱藏
  }, ms);
}
ms = 2200:函式參數的預設值。不傳就用 2200,傳了就用傳入的值。
setTimeout:計時器,幾毫秒後執行一次。1000ms = 1 秒。

第九章 整個 App 的資料流

使用者填寫反思
       ↓
  onInput() — 更新進度點、按鈕狀態
       ↓
  autoSave() — 每次離開欄位自動儲存草稿到 Supabase
       ↓
  markDone() — 按「完成今日記錄」
       ↓
  upsertEntry() — 寫入 Supabase(draft: false)
       ↓
  loadToday() — 重新載入畫面,顯示完成狀態
使用者新增習慣
       ↓
  addHabit() — 組成 habit 物件
       ↓
  upsertHabitDB() — 寫入 Supabase habits 資料表
       ↓
  renderHabits() — 重新渲染整個習慣區塊
       ↓
  renderHabitToday() — 今日打卡清單
  renderHabitCalendar() — 月行事曆點點
  renderHabitStats() — 每個習慣的統計

這份筆記對應的程式碼在
/Users/balianwang/Downloads/balian/daily-reflection.html