const NOTION_TOKEN = "ntn_" // create one in Notion's Developer tools > Connections const PAGE_ID = "" // your Lesson Log page ID const CONFIG = { lastSessionField: "Last session", sessionCountField: "Session count", streakCountField: "Streak count", dateFormat: "YYYY-MM-DD", // options: YYYY-MM-DD, DD/MM/YYYY, MM/DD/YYYY, DD.MM.YYYY, DD MMM YYYY, MMM DD, YYYY gradientDone: { from: "#4CAF72", to: "#82C26E" }, gradientPending: { from: "#F05454", to: "#FBAD52" }, gradientLost: { from: "#3B1F8C", to: "#F0A876" }, } // ================================ // async function fetchBlocks(blockId) { const req = new Request(`https://api.notion.com/v1/blocks/${blockId}/children?page_size=100`) req.headers = { "Authorization": `Bearer ${NOTION_TOKEN}`, "Notion-Version": "2022-06-28" } const res = await req.loadJSON() if (res.object !== "list") { console.log("❌ Unexpected response:") console.log(JSON.stringify(res)) return null } return res.results } function normalizeText(text) { return text .toLowerCase() .replace(/[-:]/g, "") .replace(/\s+/g, " ") .trim() } function extractValue(text, fieldName) { const normalizedText = normalizeText(text) const normalizedField = normalizeText(fieldName) if (!normalizedText.startsWith(normalizedField)) return null return text .slice(text.toLowerCase().indexOf(fieldName.toLowerCase()) + fieldName.length) .replace(/^[\s:|-]+/, "") .trim() } // Extract plain text from a Notion rich_text array function richTextToPlain(richTextArray) { return (richTextArray || []).map(t => t.plain_text).join("").trim() } // Parse a streak value like "1 day 🔥" or "3 days" → just the number function parseStreakValue(val) { const match = val.match(/\d+/) return match ? parseInt(match[0]) : null } async function parseBlocks(blocks) { let lastSession = null let sessionCount = null let streakCount = null for (const block of blocks) { const type = block.type // ── Table blocks ────────────────────────────────────────────── if (type === "table") { console.log(`📊 Found table block, fetching rows...`) const rows = await fetchBlocks(block.id) if (!rows) continue for (const row of rows) { if (row.type !== "table_row") continue const cells = row.table_row?.cells || [] if (cells.length < 2) continue const key = richTextToPlain(cells[0]) const val = richTextToPlain(cells[1]) if (!key || !val) continue console.log(` Row: "${key}" → "${val}"`) if (normalizeText(key) === normalizeText(CONFIG.lastSessionField)) { lastSession = val console.log(`📅 Last session: ${lastSession}`) } else if (normalizeText(key) === normalizeText(CONFIG.sessionCountField)) { sessionCount = parseInt(val) console.log(`🔢 Session count: ${sessionCount}`) } else if (normalizeText(key) === normalizeText(CONFIG.streakCountField)) { streakCount = parseStreakValue(val) console.log(`🔥 Streak count: ${streakCount}`) } } continue } // ── Text-based blocks (paragraph, heading, bulleted_list_item, etc.) ── const richText = block[type]?.rich_text || [] const text = richText.map(t => t.plain_text).join("") const lastSessionValue = extractValue(text, CONFIG.lastSessionField) if (lastSessionValue) { lastSession = lastSessionValue console.log(`📅 Last session: ${lastSession}`) } const sessionCountValue = extractValue(text, CONFIG.sessionCountField) if (sessionCountValue) { sessionCount = parseInt(sessionCountValue) console.log(`🔢 Session count: ${sessionCount}`) } const streakCountValue = extractValue(text, CONFIG.streakCountField) if (streakCountValue) { streakCount = parseStreakValue(streakCountValue) console.log(`🔥 Streak count: ${streakCount}`) } } if (!lastSession) console.log("⚠️ Could not find Last session") if (sessionCount == null) console.log("⚠️ Could not find Session count") if (streakCount == null) console.log("⚠️ Could not find Streak count") return { lastSession, sessionCount, streakCount } } function parseDate(dateStr) { const MONTHS = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"] const fmt = CONFIG.dateFormat const s = dateStr.trim() if (fmt === "YYYY-MM-DD") { const [y, m, d] = s.split("-").map(Number) return { y, m, d } } if (fmt === "DD/MM/YYYY") { const [d, m, y] = s.split("/").map(Number) return { y, m, d } } if (fmt === "MM/DD/YYYY") { const [m, d, y] = s.split("/").map(Number) return { y, m, d } } if (fmt === "DD.MM.YYYY") { const [d, m, y] = s.split(".").map(Number) return { y, m, d } } if (fmt === "DD MMM YYYY") { const parts = s.split(" ") const d = Number(parts[0]) const m = MONTHS.indexOf(parts[1].toLowerCase()) + 1 const y = Number(parts[2]) return { y, m, d } } if (fmt === "MMM DD, YYYY") { const parts = s.replace(",", "").split(" ") const m = MONTHS.indexOf(parts[0].toLowerCase()) + 1 const d = Number(parts[1]) const y = Number(parts[2]) return { y, m, d } } console.log(`⚠️ Unknown date format: ${fmt}`) return null } function isDoneToday(lastSession) { const today = new Date() const y = today.getFullYear() const m = today.getMonth() + 1 const d = today.getDate() console.log(`📆 Today is: ${y}-${String(m).padStart(2,"0")}-${String(d).padStart(2,"0")}`) const parsed = parseDate(lastSession) if (!parsed) { console.log("⚠️ Could not parse last session date") return false } const done = parsed.y === y && parsed.m === m && parsed.d === d console.log(done ? "✅ Session done today!" : "⏳ No session yet today") return done } function isStreakLost(lastSession) { const today = new Date() today.setHours(0, 0, 0, 0) const parsed = parseDate(lastSession) if (!parsed) return false const last = new Date(parsed.y, parsed.m - 1, parsed.d) const diffDays = Math.floor((today - last) / (1000 * 60 * 60 * 24)) const lost = diffDays > 1 console.log(lost ? `💔 Streak lost — last session was ${diffDays} days ago` : "✅ Streak still alive") return lost } function buildWidget(done, lastSession, sessionCount, streakCount, streakLost) { const colors = done ? CONFIG.gradientDone : streakLost ? CONFIG.gradientLost : CONFIG.gradientPending const widget = new ListWidget() const gradient = new LinearGradient() gradient.locations = [0, 1] gradient.colors = [new Color(colors.from), new Color(colors.to)] widget.backgroundGradient = gradient widget.setPadding(16, 16, 16, 16) // Top left status const statusLabel = done ? "✅ Done today!" : streakLost ? "🌱 New streak" : "⚠️ Not done yet" const statusText = widget.addText(statusLabel) statusText.font = Font.boldSystemFont(13) statusText.textColor = new Color("#FFFFFF", 0.9) statusText.leftAlignText() widget.addSpacer(6) // Big streak number with fire emoji const streakDigits = String(streakCount ?? "–").length const streakFontSize = streakDigits < 2 ? 40 : streakDigits < 3 ? 36 : 32 const streakText = widget.addText(streakLost ? `🌱 0` : `🔥 ${streakCount ?? "–"}`) streakText.font = Font.boldSystemFont(streakFontSize) streakText.textColor = new Color("#FFFFFF") streakText.leftAlignText() widget.addSpacer(4) // Total sessions const countText = widget.addText(`${sessionCount ?? "–"} total sessions`) countText.font = Font.systemFont(13) countText.textColor = new Color("#FFFFFF", 0.8) countText.leftAlignText() widget.addSpacer() // Last session date bottom const dateText = widget.addText(`Last: ${lastSession ?? "unknown"}`) dateText.font = Font.systemFont(11) dateText.textColor = new Color("#FFFFFF", 0.6) dateText.leftAlignText() return widget } async function run() { const blocks = await fetchBlocks(PAGE_ID) if (!blocks) return console.log(`✅ Got page content! Found ${blocks.length} blocks`) const { lastSession, sessionCount, streakCount } = await parseBlocks(blocks) const done = isDoneToday(lastSession) const streakLost = !done && isStreakLost(lastSession) const widget = buildWidget(done, lastSession, sessionCount, streakCount, streakLost) if (config.runsInWidget) { Script.setWidget(widget) } else { await widget.presentMedium() } Script.complete() } await run()