每次讀不懂就回來翻這份。先看整體架構,再看細節。
整個 App 只有一個 .html 檔案,裡面同時包含三個部分:
| 部分 | 寫在哪裡 | 負責什麼 |
|---|---|---|
| HTML | <body> 裡面 | 畫出頁面結構(表單、按鈕、分頁) |
| CSS | <style> 標籤 | 控制顏色、字型、間距、排版 |
| JavaScript | <script> 標籤 | 處理邏輯、讀寫資料、更新畫面 |
資料存在 Supabase(雲端資料庫),JS 透過 API 讀寫,不管哪個裝置開,資料都一樣。
// 引入 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' // 公開金鑰(可以讀寫,但有權限控制)
);
從 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,函式本身就要加 asyncawait 只能用在 async 函式裡Promise.all([])
// 同時發出兩個請求,等兩個都回來
const [entries, habits] = await Promise.all([
getEntries('2026-06-01', '2026-06-30'),
getHabits()
]);
// 比分開 await 快,因為兩個同時進行
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;
}
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% 的資料處理都能搞定。
遍歷陣列,每個元素套一個函式,回傳新陣列。
const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2);
// n => n * 2 叫做「箭頭函式」,意思是「每個 n,回傳 n × 2」
在反思 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('') 把陣列合成一個字串
遍歷陣列,條件回傳 true 的才保留,回傳新陣列(長度 ≤ 原陣列)。
const scores = [45, 82, 60, 91, 33];
const passed = scores.filter(s => s >= 60);
// s >= 60 是條件,true 才留下
在反思 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);
// 算出完成率百分比
和 .map() 很像,但不回傳新陣列,通常用來做「副作用」(例如更新畫面、打 log)。
const fruits = ['蘋果', '香蕉', '芒果'];
fruits.forEach(f => {
console.log(f); // 印出每個水果
});
在反思 App 裡的實際用法:
// 把五個問題的答案存到 fields 物件
const fields = {};
QS.forEach(q => {
fields[q.field] = document.getElementById(q.id).value.trim();
// 取出 textarea 的內容,去掉頭尾空白,存進 fields
});
// 結果:fields = { gratitude: '...', achievement: '...', ... }
.map().forEach()
| 方法 | 記憶口訣 | 回傳 | 常用場景 |
|---|---|---|---|
.map() |
每個元素變形 | 新陣列(同長度) | 資料轉 HTML、計算新值 |
.filter() |
篩選留下合格的 | 新陣列(較短或同長) | 找完成的習慣、找特定日期 |
.forEach() |
跑過去做事,不回傳 | undefined | 更新欄位、打 log |
這個語法在整個 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} 歲`);
這是這次改動的關鍵段,把反思月曆和習慣打點合在一起。
// 取得三份資料(同時發出,速度最快)
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>`;
});
條件 ? 成立時的值 : 不成立時的值hlog[h.id] ? `background:${hc}` : ''// 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);
}
使用者填寫反思
↓
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