SLOPSHOPPER

activity-calendar-th

ปฏิทินกิจกรรมแบบ GitHub 26 สัปดาห์ข้ามทุก session พร้อม streak และวันที่ขยันที่สุด

newpaneguardcommandprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · activity-calendar-th
│ ┃ 📅 ปฏิทินกิจกรรม ✕ › fix the failing auth test and add an audit log call │ ┃ เม.ย. พ.ค. มิ.ย. ก.ค. ส.ค. │ ┃ ก.ย. ต.ค. ⏺ Read(src/auth.ts) │ ┃ จ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ⎿ Read 6 lines │ ┃ ■ ■ ■ ■ ■ ⏺ Update(src/auth.ts) │ ┃ อ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ⎿ Added 2 lines, removed 1 line │ ┃ ■ ■ ■ ■ ■ ⏺ Bash(bun test) │ ┃ พ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ⎿ 3 pass, 1 fail │ ┃ ■ ■ ■ ■ ■ │ ┃ พฤ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ ■ ■ ■ ■ ◆ │ ┃ ศ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ✻ Worked for 42s · done 4:20 PM │ ┃ ■ ■ ■ ■ ■ │ ┃ ส ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ › /calendar │ ┃ ■ ■ ■ ■ ■ ⎿ activity-calendar-th: 📅 นับprompt · 🔥 streak 1 วัน (ยาวสุด 1) · │ ┃ อา ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ │ ┃ ■ ■ ■ ■ ■ │ ┃ น้อย ■ ■ ■ ■ ■ มาก · นับprompt │ ┃ │ ┃ 🔥 streak 1 วัน · ยาวสุด 1 วัน · ทำงาน 1 วัน │ ┃ 📊 รวม 1 prompt · ขยันสุด 2025-10-09 (1) │ ┃ 🎉 Mod ที่ 100 · ขอบคุณที่ใช้ Claude Code │ ┃ Mods TH 🇹🇭 │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · 📅 ปฏิทินกิจกรรม
เม.ย. พ.ค. มิ.ย. ก.ค. ส.ค. ก.ย. ต.ค. จ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ อ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ พ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ พฤ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ◆ ศ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ส ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ อา ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ น้อย ■ ■ ■ ■ ■ มาก · นับprompt 🔥 streak 1 วัน · ยาวสุด 1 วัน · ทำงาน 1 วัน 📊 รวม 1 prompt · ขยันสุด 2025-10-09 (1) 🎉 Mod ที่ 100 · ขอบคุณที่ใช้ Claude Code Mods TH 🇹🇭
README

Mod 100: Activity Calendar TH

🎉 Mod ตัวที่ 100 · ปฏิทินกิจกรรมแบบ GitHub contribution ข้ามทุก session: นับรายวันว่าส่ง prompt กี่ครั้ง ใช้ tool เท่าไหร่ แก้ไฟล์กี่ครั้ง Claude ทำงานกี่นาที เสียเงินเท่าไหร่ แล้ววาดตาราง 26 สัปดาห์ สีเขียวเข้มตามความขยัน พร้อม streak วันติดกัน และวันที่ขยันที่สุด

คำสั่ง

คำสั่งทำอะไร
/calendarเปิดปฏิทินกิจกรรม
`/calendar prompts\tools\edits\minutes\cost`เลือกสิ่งที่นับ

ติดตั้ง

/plugin install activity-calendar-th --marketplace tksoft11/claude-code-mods-th

ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)

โค้ดสอนอะไร

เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. นับลงวันนี้จากหลาย event
  2. ตารางและสถิติ (streak, ยาวสุด, วันที่ขยันที่สุด)
  3. เก็บตัวเลขจาก prompt.submit, tool.call, turn, session.measure
  4. วาดตาราง 7 แถว × 26 สัปดาห์

หมายเหตุ

เก็บใน $.store ของ mod นับเฉพาะเครื่องนี้

Source 1 files
hooks/register.tsx 248 lines
1/**
2 * ============================================================================
3 * MOD 100: Activity Calendar TH - ปฏิทินกิจกรรมแบบ GitHub ข้ามทุก session (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: mosaic (KOZMOS) และ heatmap ใน awesome-claude-code-mods
6 *
7 * 🎉 Mod ตัวที่ 100 ของ Claude Code Mods TH
8 *
9 * หน้าที่ของ Mod นี้:
10 * 1. นับกิจกรรมรายวันข้ามทุก session: prompt, tool, ไฟล์ที่แก้, นาทีที่ Claude ทำงาน, ค่าใช้จ่าย
11 * 2. /calendar เปิดแผงตารางแบบ GitHub contribution 26 สัปดาห์ สีเขียวเข้มตามความขยัน
12 * 3. สถิติ: streak ปัจจุบัน, streak ยาวสุด, วันที่ขยันที่สุด, รวมทั้งหมด
13 * 4. เปลี่ยนสิ่งที่นับได้: /calendar prompts | tools | edits | minutes | cost
14 */
15
16import type { EngineInterface, Register } from 'claude-code'
17
18const PANE = 'activity-calendar-th'
19const WEEKS = 26
20const DAY_MS = 86_400_000
21
22interface Day {
23  prompts: number
24  tools: number
25  edits: number
26  minutes: number
27  cost: number
28}
29
30type Metric = keyof Day
31
32const METRIC: Record<Metric, string> = { prompts: 'prompt', tools: 'tool', edits: 'การแก้ไฟล์', minutes: 'นาทีทำงาน', cost: 'ดอลลาร์' }
33const SHADES = ['#2d333b', '#0e4429', '#006d32', '#26a641', '#39d353']
34const DOW = ['จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส', 'อา']
35const MONTH = ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.']
36
37let days: Record<string, Day> = {}
38let metric: Metric = 'prompts'
39let turnStart = 0
40let sessionCost = 0
41
42function key(ms: number): string {
43  const d = new Date(ms)
44  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
45}
46
47function empty(): Day {
48  return { prompts: 0, tools: 0, edits: 0, minutes: 0, cost: 0 }
49}
50
51// ==========================================================================
52// บทที่ 1: นับลงวันนี้
53// ==========================================================================
54async function bump($: EngineInterface, field: Metric, by = 1): Promise<void> {
55  const k = key(await $.clock.now())
56  const d = days[k] ?? empty()
57  days[k] = { ...d, [field]: d[field] + by }
58}
59
60async function save($: EngineInterface): Promise<void> {
61  await $.store.set('days', days)
62}
63
64// ==========================================================================
65// บทที่ 2: ตารางและสถิติ
66// ==========================================================================
67function value(k: string): number {
68  return days[k]?.[metric] ?? 0
69}
70
71function level(v: number, max: number): number {
72  if (v <= 0) return 0
73  return Math.min(4, Math.ceil((v / max) * 4))
74}
75
76// คืนวันจันทร์ของสัปดาห์แรกในตาราง
77function gridStart(now: number): number {
78  const d = new Date(new Date(now).toDateString()).getTime()
79  const dow = (new Date(d).getDay() + 6) % 7 // จันทร์ = 0
80  return d - dow * DAY_MS - (WEEKS - 1) * 7 * DAY_MS
81}
82
83function stats(now: number): { total: number; active: number; streak: number; longest: number; best?: [string, number] } {
84  const keys = Object.keys(days).sort()
85  let total = 0
86  let active = 0
87  let best: [string, number] | undefined
88  for (const k of keys) {
89    const v = value(k)
90    total += v
91    if (v > 0) active++
92    if (v > 0 && (!best || v > best[1])) best = [k, v]
93  }
94  // streak: นับวันติดกันที่มีกิจกรรม (ใช้ prompt เป็นเกณฑ์ว่า "ได้ทำงาน")
95  const worked = (ms: number): boolean => (days[key(ms)]?.prompts ?? 0) > 0
96  let streak = 0
97  let cursor = new Date(new Date(now).toDateString()).getTime()
98  if (!worked(cursor)) cursor -= DAY_MS
99  while (worked(cursor)) {
100    streak++
101    cursor -= DAY_MS
102  }
103  let longest = 0
104  let run = 0
105  let prev = 0
106  for (const k of keys.filter(k => (days[k]?.prompts ?? 0) > 0)) {
107    const t = new Date(`${k}T00:00:00`).getTime()
108    run = prev && Math.round((t - prev) / DAY_MS) === 1 ? run + 1 : 1
109    longest = Math.max(longest, run)
110    prev = t
111  }
112  return { total, active, streak, longest, best }
113}
114
115function fmt(v: number): string {
116  return metric === 'cost' ? `$${v.toFixed(2)}` : metric === 'minutes' ? `${Math.round(v)}` : String(v)
117}
118
119export const register: Register = on => {
120  on('session.start', async ($, e, next) => {
121    days = ((await $.store.get('days')) as Record<string, Day> | undefined) ?? {}
122    metric = ((await $.store.get('metric')) as Metric | undefined) ?? 'prompts'
123    sessionCost = 0
124    await $.command.register({ name: 'calendar', description: 'ปฏิทินกิจกรรมแบบ GitHub ข้ามทุก session + streak (/calendar help)', argumentHint: 'prompts|tools|edits|minutes|cost' })
125    return next(e)
126  })
127
128  // ==========================================================================
129  // บทที่ 3: เก็บตัวเลขจาก event
130  // ==========================================================================
131  on('prompt.submit', async ($, e, next) => {
132    if (e.origin.kind !== 'plugin') await bump($, 'prompts')
133    return next(e)
134  })
135
136  on('tool.call', async ($, e, next) => {
137    const r = await next(e)
138    if (r.deny === undefined) {
139      await bump($, 'tools')
140      if (!r.isError && (e.tool === 'Edit' || e.tool === 'Write' || e.tool === 'NotebookEdit')) await bump($, 'edits')
141    }
142    return r
143  })
144
145  on('turn.start', async ($, e, next) => {
146    if (!e.agentId) turnStart = await $.clock.now()
147    return next(e)
148  })
149
150  on('turn.complete', async ($, e, next) => {
151    const r = await next(e)
152    if (!e.agentId && turnStart) {
153      await bump($, 'minutes', ((await $.clock.now()) - turnStart) / 60_000)
154      turnStart = 0
155      await save($)
156    }
157    return r
158  })
159
160  // ค่าใช้จ่ายที่ engine รายงานเป็นยอดรวมของ session: บวกเฉพาะส่วนที่เพิ่มขึ้น
161  on('session.measure', async ($, e, next) => {
162    if (e.cost && e.cost.usd > sessionCost) {
163      await bump($, 'cost', e.cost.usd - sessionCost)
164      sessionCost = e.cost.usd
165    }
166    return next(e)
167  })
168
169  on('session.end', async ($, e, next) => {
170    await save($)
171    return next(e)
172  })
173
174  // ==========================================================================
175  // บทที่ 4: วาดตาราง 7 แถว × 26 สัปดาห์
176  // ==========================================================================
177  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
178    const { Box, Text } = $.ui.resolve(e)
179    const now = await $.clock.now()
180    const start = gridStart(now)
181    const today = key(now)
182    const cells = Array.from({ length: WEEKS * 7 }, (_, i) => key(start + (Math.floor(i / 7) * 7 + (i % 7)) * DAY_MS))
183    const max = Math.max(1, ...cells.map(value))
184    // แถวชื่อเดือน: ใส่ชื่อเดือนที่คอลัมน์ที่ขึ้นเดือนใหม่
185    let monthRow = ''
186    let lastMonth = -1
187    for (let w = 0; w < WEEKS; w++) {
188      const m = new Date(start + w * 7 * DAY_MS).getMonth()
189      if (m !== lastMonth && monthRow.length <= w * 2) {
190        monthRow = monthRow.padEnd(w * 2) + MONTH[m]
191        lastMonth = m
192      }
193    }
194    const s = stats(now)
195    return (
196      <Box flexDirection="column">
197        <Text dimColor>{`   ${monthRow}`}</Text>
198        {DOW.map((label, row) => (
199          <Text>
200            <Text dimColor>{label.padEnd(3)}</Text>
201            {Array.from({ length: WEEKS }, (_, w) => {
202              const k = cells[w * 7 + row]!
203              const future = k > today
204              return <Text color={future ? '#1c2128' : SHADES[level(value(k), max)]}>{k === today ? '◆ ' : '■ '}</Text>
205            })}
206          </Text>
207        ))}
208        <Text>
209          <Text dimColor>{'   น้อย '}</Text>
210          {SHADES.map(c => (
211            <Text color={c}>■ </Text>
212          ))}
213          <Text dimColor>{` มาก · นับ${METRIC[metric]}`}</Text>
214        </Text>
215        <Box flexDirection="column" marginTop={1}>
216          <Text>{`🔥 streak ${s.streak} วัน · ยาวสุด ${s.longest} วัน · ทำงาน ${s.active} วัน`}</Text>
217          <Text>{`📊 รวม ${fmt(s.total)} ${METRIC[metric]}${s.best ? ` · ขยันสุด ${s.best[0]} (${fmt(s.best[1])})` : ''}`}</Text>
218          <Text dimColor>🎉 Mod ที่ 100 · ขอบคุณที่ใช้ Claude Code Mods TH 🇹🇭</Text>
219        </Box>
220      </Box>
221    )
222  })
223
224  on('command.run', { command: 'calendar' }, async ($, e) => {
225    const sub = e.args.trim()
226    if (sub in METRIC) {
227      metric = sub as Metric
228      await $.store.set('metric', metric)
229    } else if (sub) {
230      return { text: '📅 Activity Calendar TH (Mod 100)\n/calendar                เปิดปฏิทินกิจกรรม 26 สัปดาห์\n/calendar prompts|tools|edits|minutes|cost   เลือกสิ่งที่นับ' }
231    }
232    await save($)
233    await $.ui.open({ id: PANE, title: '📅 ปฏิทินกิจกรรม' })
234    $.ui.invalidate('ui.render')
235    const s = stats(await $.clock.now())
236    return { text: `📅 นับ${METRIC[metric]} · 🔥 streak ${s.streak} วัน (ยาวสุด ${s.longest}) · รวม ${fmt(s.total)} · ทำงาน ${s.active} วัน` }
237  })
238}
239
240/**
241 * ========================================================================
242 * การบ้าน / ต่อยอด
243 * ========================================================================
244 * 1. แยกตามโปรเจกต์: เก็บ cwd ด้วย แล้วกรองดูทีละโปรเจกต์
245 * 2. ส่งออกเป็นรูป SVG ไว้แปะ README ของโปรไฟล์ GitHub
246 * 3. เตือนตอนค่ำถ้าวันนี้ยังไม่ได้ทำงาน จะได้ไม่เสีย streak (ต่อกับ Mod 96)
247 */
248