claude-code-templates 1.20.1 → 1.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,738 @@
1
+ /**
2
+ * ActivityHeatmap - GitHub-style contribution calendar for Claude Code activity
3
+ * Shows daily Claude Code usage over the last year with orange theme
4
+ */
5
+ class ActivityHeatmap {
6
+ constructor(container, dataService) {
7
+ this.container = container;
8
+ this.dataService = dataService;
9
+ this.activityData = null;
10
+ this.tooltip = null;
11
+ this.currentYear = new Date().getFullYear();
12
+ this.currentMetric = 'messages'; // Default metric
13
+
14
+ console.log('🔥 ActivityHeatmap initialized');
15
+ }
16
+
17
+ /**
18
+ * Initialize the heatmap component
19
+ */
20
+ async initialize() {
21
+ try {
22
+ console.log('🔥 Initializing ActivityHeatmap...');
23
+ await this.render();
24
+ await this.loadActivityData();
25
+ console.log('✅ ActivityHeatmap initialized successfully');
26
+ } catch (error) {
27
+ console.error('❌ Failed to initialize ActivityHeatmap:', error);
28
+ this.showErrorState();
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Render the heatmap structure
34
+ */
35
+ async render() {
36
+ this.container.innerHTML = `
37
+ <div class="activity-heatmap-container">
38
+ <div class="heatmap-loading">
39
+ <div class="heatmap-loading-spinner"></div>
40
+ <span>Loading activity...</span>
41
+ </div>
42
+ </div>
43
+ <div class="heatmap-tooltip" id="heatmap-tooltip">
44
+ <div class="heatmap-tooltip-date"></div>
45
+ <div class="heatmap-tooltip-activity"></div>
46
+ </div>
47
+ `;
48
+
49
+ this.tooltip = document.getElementById('heatmap-tooltip');
50
+ }
51
+
52
+ /**
53
+ * Load activity data from the API
54
+ */
55
+ async loadActivityData() {
56
+ try {
57
+ console.log('🔥 Loading activity data...');
58
+
59
+ // Get complete activity data from backend (pre-processed with tools)
60
+ const response = await this.dataService.cachedFetch('/api/activity');
61
+ if (response && response.dailyActivity) {
62
+ console.log(`🔥 Loaded pre-processed activity data: ${response.dailyActivity.length} active days`);
63
+
64
+ // Use pre-processed data from backend instead of processing raw conversations
65
+ const dailyActivityMap = new Map();
66
+ response.dailyActivity.forEach(day => {
67
+ dailyActivityMap.set(day.date, day);
68
+ });
69
+
70
+ this.activityData = this.processPrecomputedActivityData(dailyActivityMap);
71
+ await this.renderHeatmap();
72
+ this.updateTitle();
73
+ } else {
74
+ throw new Error('No activity data available');
75
+ }
76
+ } catch (error) {
77
+ console.error('❌ Error loading activity data:', error);
78
+ this.showErrorState();
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Process pre-computed activity data from backend
84
+ */
85
+ processPrecomputedActivityData(dailyActivityMap) {
86
+ console.log(`🔥 Processing ${dailyActivityMap.size} days of pre-computed data...`);
87
+
88
+ // Calculate thresholds based on current metric
89
+ const metricCounts = Array.from(dailyActivityMap.values())
90
+ .map(activity => activity[this.currentMetric] || 0)
91
+ .filter(count => count > 0)
92
+ .sort((a, b) => a - b);
93
+
94
+ const thresholds = this.calculateDynamicThresholds(metricCounts);
95
+
96
+ // Calculate total activity for current metric
97
+ let totalActivity = 0;
98
+ let totalTools = 0;
99
+ let totalMessages = 0;
100
+ dailyActivityMap.forEach(activity => {
101
+ totalActivity += activity[this.currentMetric] || 0;
102
+ totalTools += activity.tools || 0;
103
+ totalMessages += activity.messages || 0;
104
+ });
105
+
106
+ console.log(`🔥 Pre-computed data stats: ${totalMessages} messages, ${totalTools} tools`);
107
+ console.log(`🔥 Current metric (${this.currentMetric}): ${totalActivity} total`);
108
+ console.log(`🔥 Dynamic thresholds:`, thresholds);
109
+ console.log(`🔥 Sample ${this.currentMetric} counts:`, metricCounts.slice(0, 10), '...', metricCounts.slice(-10));
110
+
111
+ return {
112
+ dailyActivity: dailyActivityMap,
113
+ totalActivity,
114
+ activeDays: dailyActivityMap.size,
115
+ thresholds
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Process conversation data into daily activity counts (legacy method)
121
+ */
122
+ processActivityData(conversations) {
123
+ const dailyActivity = new Map();
124
+ const oneYearAgo = new Date();
125
+ oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
126
+ const today = new Date();
127
+
128
+ console.log(`🔥 Processing ${conversations.length} conversations for activity data...`);
129
+ console.log(`🔥 Date range: ${oneYearAgo.toLocaleDateString()} to ${today.toLocaleDateString()}`);
130
+ console.log(`🔥 Cutoff timestamp: ${oneYearAgo.getTime()}`);
131
+
132
+ let validConversations = 0;
133
+ let latestDate = null;
134
+ let oldestDate = null;
135
+ let beforeOneYearCount = 0;
136
+
137
+ // Sample some conversations to see their date formats and data structure
138
+ console.log(`🔥 Sampling first 5 conversations:`);
139
+ conversations.slice(0, 5).forEach((conv, i) => {
140
+ console.log(` ${i+1}: ${conv.filename} - lastModified: ${conv.lastModified} (${new Date(conv.lastModified).toLocaleDateString()})`);
141
+ console.log(` messages: ${conv.messageCount || 0}, tokens: ${conv.tokens || 0}, toolUsage: ${conv.toolUsage?.totalToolCalls || 0}`);
142
+ });
143
+
144
+ conversations.forEach((conversation, index) => {
145
+ if (!conversation.lastModified) {
146
+ if (index < 5) console.log(`⚠️ Conversation ${index} has no lastModified:`, conversation);
147
+ return;
148
+ }
149
+
150
+ const date = new Date(conversation.lastModified);
151
+
152
+ // Track date range
153
+ if (!latestDate || date > latestDate) latestDate = date;
154
+ if (!oldestDate || date < oldestDate) oldestDate = date;
155
+
156
+ if (date < oneYearAgo) {
157
+ beforeOneYearCount++;
158
+ if (beforeOneYearCount <= 5) {
159
+ console.log(`⚠️ Excluding old conversation: ${date.toISOString()} (${conversation.filename})`);
160
+ }
161
+ return;
162
+ }
163
+
164
+ validConversations++;
165
+ const dateKey = date.toISOString().split('T')[0]; // YYYY-MM-DD
166
+
167
+ const current = dailyActivity.get(dateKey) || {
168
+ conversations: 0,
169
+ tokens: 0,
170
+ messages: 0,
171
+ tools: 0
172
+ };
173
+
174
+ current.conversations += 1;
175
+ current.tokens += conversation.tokens || 0;
176
+ current.messages += conversation.messageCount || 0;
177
+ current.tools += (conversation.toolUsage?.totalToolCalls || 0);
178
+
179
+ dailyActivity.set(dateKey, current);
180
+ });
181
+
182
+ console.log(`🔥 Valid conversations in last year: ${validConversations}`);
183
+ console.log(`🔥 Excluded conversations (older than 1 year): ${beforeOneYearCount}`);
184
+ console.log(`🔥 Complete date range in data: ${oldestDate?.toLocaleDateString()} to ${latestDate?.toLocaleDateString()}`);
185
+ console.log(`🔥 One year ago cutoff: ${oneYearAgo.toLocaleDateString()}`);
186
+
187
+ // Show first and last few conversations for debugging
188
+ const sortedDates = Array.from(dailyActivity.keys()).sort();
189
+ console.log(`🔥 First 5 activity dates:`, sortedDates.slice(0, 5));
190
+ console.log(`🔥 Last 5 activity dates:`, sortedDates.slice(-5));
191
+ console.log(`🔥 Total activity days: ${sortedDates.length}`);
192
+
193
+ // Calculate total activity for the year (based on current metric)
194
+ let totalActivity = 0;
195
+ dailyActivity.forEach(activity => {
196
+ totalActivity += activity[this.currentMetric] || 0;
197
+ });
198
+
199
+ // Calculate dynamic thresholds based on data distribution
200
+ const messageCounts = Array.from(dailyActivity.values())
201
+ .map(activity => activity[this.currentMetric] || 0)
202
+ .filter(count => count > 0)
203
+ .sort((a, b) => a - b);
204
+
205
+ const thresholds = this.calculateDynamicThresholds(messageCounts);
206
+
207
+ // Calculate total tools for debugging
208
+ let totalTools = 0;
209
+ let totalMessages = 0;
210
+ dailyActivity.forEach(activity => {
211
+ totalTools += activity.tools || 0;
212
+ totalMessages += activity.messages || 0;
213
+ });
214
+
215
+ console.log(`🔥 Processed activity data: ${dailyActivity.size} active days, ${totalActivity} total ${this.currentMetric}`);
216
+ console.log(`🔥 Debug totals: ${totalMessages} messages, ${totalTools} tools`);
217
+ console.log(`🔥 ${this.currentMetric} range: ${Math.min(...messageCounts)} to ${Math.max(...messageCounts)} ${this.currentMetric} per day`);
218
+ console.log(`🔥 Dynamic thresholds:`, thresholds);
219
+ console.log(`🔥 Sample ${this.currentMetric} counts:`, messageCounts.slice(0, 10), '...', messageCounts.slice(-10));
220
+
221
+ return {
222
+ dailyActivity,
223
+ totalActivity,
224
+ activeDays: dailyActivity.size,
225
+ thresholds
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Calculate dynamic thresholds based on data distribution
231
+ * Ensures that even 1 message shows visible color (like GitHub)
232
+ */
233
+ calculateDynamicThresholds(messageCounts) {
234
+ if (messageCounts.length === 0) {
235
+ return { level1: 1, level2: 5, level3: 15, level4: 30 };
236
+ }
237
+
238
+ const len = messageCounts.length;
239
+ const max = messageCounts[len - 1];
240
+ const min = messageCounts[0];
241
+
242
+ // ALWAYS start level1 at 1 so any activity shows color
243
+ let level1 = 1;
244
+ let level2, level3, level4;
245
+
246
+ if (len <= 4) {
247
+ // Very few data points - simple distribution
248
+ level2 = Math.max(2, Math.ceil(max * 0.3));
249
+ level3 = Math.max(level2 + 1, Math.ceil(max * 0.6));
250
+ level4 = Math.max(level3 + 1, Math.ceil(max * 0.8));
251
+ } else {
252
+ // Use percentiles but ensure good visual distribution
253
+ const p33 = messageCounts[Math.floor(len * 0.33)] || 2;
254
+ const p66 = messageCounts[Math.floor(len * 0.66)] || 3;
255
+ const p85 = messageCounts[Math.floor(len * 0.85)] || 4;
256
+
257
+ // Ensure reasonable spacing between levels
258
+ level2 = Math.max(2, Math.min(p33, max * 0.2));
259
+ level3 = Math.max(level2 + 1, Math.min(p66, max * 0.5));
260
+ level4 = Math.max(level3 + 1, Math.min(p85, max * 0.75));
261
+ }
262
+
263
+ return { level1, level2, level3, level4 };
264
+ }
265
+
266
+ /**
267
+ * Render the heatmap calendar
268
+ */
269
+ async renderHeatmap() {
270
+ if (!this.activityData) return;
271
+
272
+ const { dailyActivity, totalActivity } = this.activityData;
273
+
274
+ // Generate calendar structure
275
+ const calendarData = this.generateCalendarData(dailyActivity);
276
+
277
+ const container = this.container.querySelector('.activity-heatmap-container');
278
+ const modeClass = this.currentMetric === 'tools' ? 'tools-mode' : '';
279
+ container.className = `activity-heatmap-container ${modeClass}`;
280
+ container.innerHTML = `
281
+ <div class="heatmap-header">
282
+ <div class="heatmap-legend">
283
+ <span class="heatmap-legend-text">Less</span>
284
+ <div class="heatmap-legend-scale">
285
+ <div class="heatmap-legend-square level-0"></div>
286
+ <div class="heatmap-legend-square level-1"></div>
287
+ <div class="heatmap-legend-square level-2"></div>
288
+ <div class="heatmap-legend-square level-3"></div>
289
+ <div class="heatmap-legend-square level-4"></div>
290
+ </div>
291
+ <span class="heatmap-legend-more">More</span>
292
+ </div>
293
+ </div>
294
+ <div class="heatmap-grid">
295
+ <div class="heatmap-months" id="heatmap-months-container">
296
+ ${calendarData.months.map((month, index) =>
297
+ `<div class="heatmap-month" data-week-index="${index}">${month}</div>`
298
+ ).join('')}
299
+ </div>
300
+ <div class="heatmap-weekdays">
301
+ <div class="heatmap-weekday">Mon</div>
302
+ <div class="heatmap-weekday"></div>
303
+ <div class="heatmap-weekday">Wed</div>
304
+ <div class="heatmap-weekday"></div>
305
+ <div class="heatmap-weekday">Fri</div>
306
+ <div class="heatmap-weekday"></div>
307
+ <div class="heatmap-weekday"></div>
308
+ </div>
309
+ <div class="heatmap-weeks">
310
+ ${calendarData.weeks.map(week => this.renderWeek(week, dailyActivity)).join('')}
311
+ </div>
312
+ </div>
313
+ `;
314
+
315
+ this.attachEventListeners();
316
+ this.attachSettingsListeners();
317
+ this.positionMonthLabels();
318
+
319
+ // Re-position months on window resize
320
+ this.resizeHandler = () => this.positionMonthLabels();
321
+ window.addEventListener('resize', this.resizeHandler);
322
+ }
323
+
324
+ /**
325
+ * Position month labels based on actual week positions
326
+ */
327
+ positionMonthLabels() {
328
+ setTimeout(() => {
329
+ const weeksContainer = this.container.querySelector('.heatmap-weeks');
330
+ const monthsContainer = this.container.querySelector('#heatmap-months-container');
331
+ const monthElements = monthsContainer?.querySelectorAll('.heatmap-month');
332
+ const weekElements = weeksContainer?.children;
333
+
334
+ if (!weeksContainer || !monthsContainer || !monthElements || !weekElements) return;
335
+
336
+ // Calculate the actual width and position of each week column
337
+ Array.from(monthElements).forEach((monthEl, index) => {
338
+ if (index < weekElements.length && monthEl.textContent.trim()) {
339
+ const weekEl = weekElements[index];
340
+ const weekRect = weekEl.getBoundingClientRect();
341
+ const containerRect = monthsContainer.getBoundingClientRect();
342
+
343
+ // Position month label at the start of its corresponding week
344
+ const leftPosition = weekRect.left - containerRect.left;
345
+ monthEl.style.left = `${leftPosition}px`;
346
+ }
347
+ });
348
+ }, 50); // Small delay to ensure DOM is fully rendered
349
+ }
350
+
351
+ /**
352
+ * Generate calendar structure for the last year
353
+ */
354
+ generateCalendarData(dailyActivity) {
355
+ const today = new Date();
356
+ const oneYearAgo = new Date(today);
357
+ oneYearAgo.setFullYear(today.getFullYear() - 1);
358
+ oneYearAgo.setDate(today.getDate() + 1);
359
+
360
+ // Find the start of the week (Sunday) for the start date
361
+ const startDate = new Date(oneYearAgo);
362
+ startDate.setDate(startDate.getDate() - startDate.getDay());
363
+
364
+ const weeks = [];
365
+ const months = [];
366
+ const current = new Date(startDate);
367
+
368
+ // Generate weeks first
369
+ while (current <= today) {
370
+ const week = [];
371
+
372
+ for (let day = 0; day < 7; day++) {
373
+ if (current <= today) {
374
+ week.push(new Date(current));
375
+ } else {
376
+ week.push(null);
377
+ }
378
+ current.setDate(current.getDate() + 1);
379
+ }
380
+
381
+ weeks.push(week);
382
+ }
383
+
384
+ // Generate month labels - show when month changes
385
+ let lastDisplayedMonth = -1;
386
+
387
+ for (let i = 0; i < weeks.length; i++) {
388
+ const week = weeks[i];
389
+ let monthName = '';
390
+
391
+ if (week && week.length > 0) {
392
+ // Get the most representative day of the week (middle of week)
393
+ const middleDay = week[3] || week[2] || week[1] || week[0];
394
+
395
+ if (middleDay) {
396
+ const currentMonth = middleDay.getMonth();
397
+
398
+ // Show month name if it's the first occurrence or if month changed
399
+ if (currentMonth !== lastDisplayedMonth) {
400
+ monthName = middleDay.toLocaleDateString('en-US', { month: 'short' });
401
+ lastDisplayedMonth = currentMonth;
402
+ }
403
+ }
404
+ }
405
+
406
+ months.push(monthName);
407
+ }
408
+
409
+ return { weeks, months };
410
+ }
411
+
412
+ /**
413
+ * Render a week column
414
+ */
415
+ renderWeek(week, dailyActivity) {
416
+ const weekHtml = week.map(date => {
417
+ if (!date) return '<div class="heatmap-day empty"></div>';
418
+
419
+ const dateKey = date.toISOString().split('T')[0];
420
+ const activity = dailyActivity.get(dateKey);
421
+ const level = this.getActivityLevel(activity);
422
+ const modeClass = this.currentMetric === 'tools' ? 'tools-mode' : '';
423
+
424
+ return `
425
+ <div class="heatmap-day level-${level} ${modeClass}"
426
+ data-date="${dateKey}"
427
+ data-activity='${JSON.stringify(activity || { conversations: 0, tokens: 0, messages: 0, tools: 0 })}'>
428
+ </div>
429
+ `;
430
+ }).join('');
431
+
432
+ return `<div class="heatmap-week">${weekHtml}</div>`;
433
+ }
434
+
435
+ /**
436
+ * Calculate activity level based on current metric using dynamic thresholds
437
+ */
438
+ getActivityLevel(activity) {
439
+ if (!activity) return 0;
440
+
441
+ const metricValue = activity[this.currentMetric] || 0;
442
+ if (metricValue === 0) return 0;
443
+
444
+ const thresholds = this.activityData?.thresholds;
445
+
446
+ if (!thresholds) {
447
+ // Fallback to static levels if thresholds not available
448
+ if (metricValue >= 50) return 4;
449
+ if (metricValue >= 30) return 3;
450
+ if (metricValue >= 15) return 2;
451
+ if (metricValue >= 1) return 1;
452
+ return 0;
453
+ }
454
+
455
+ // Use dynamic thresholds for better distribution
456
+ if (metricValue >= thresholds.level4) return 4;
457
+ if (metricValue >= thresholds.level3) return 3;
458
+ if (metricValue >= thresholds.level2) return 2;
459
+ if (metricValue >= thresholds.level1) return 1;
460
+
461
+ return 0;
462
+ }
463
+
464
+ /**
465
+ * Attach event listeners for tooltips and interactions
466
+ */
467
+ attachEventListeners() {
468
+ const days = this.container.querySelectorAll('.heatmap-day');
469
+
470
+ days.forEach(day => {
471
+ day.addEventListener('mouseenter', (e) => this.showTooltip(e));
472
+ day.addEventListener('mouseleave', () => this.hideTooltip());
473
+ day.addEventListener('mousemove', (e) => this.updateTooltipPosition(e));
474
+ });
475
+ }
476
+
477
+ /**
478
+ * Attach event listeners for settings dropdown
479
+ */
480
+ attachSettingsListeners() {
481
+ // Remove existing listeners first to prevent duplicates
482
+ this.removeSettingsListeners();
483
+
484
+ const settingsButton = document.querySelector('.heatmap-settings');
485
+ const dropdown = document.getElementById('heatmap-settings-dropdown');
486
+ const metricOptions = dropdown?.querySelectorAll('.heatmap-metric-option');
487
+
488
+ console.log('🔥 Attaching settings listeners:', {
489
+ settingsButton: !!settingsButton,
490
+ dropdown: !!dropdown,
491
+ metricOptions: metricOptions?.length || 0
492
+ });
493
+
494
+ if (settingsButton && dropdown) {
495
+ // Store references to handlers for cleanup
496
+ this.settingsClickHandler = (e) => {
497
+ e.stopPropagation();
498
+ dropdown.classList.toggle('show');
499
+ console.log('🔥 Settings dropdown toggled:', dropdown.classList.contains('show'));
500
+ };
501
+
502
+ this.documentClickHandler = (e) => {
503
+ if (!settingsButton.contains(e.target) && !dropdown.contains(e.target)) {
504
+ dropdown.classList.remove('show');
505
+ }
506
+ };
507
+
508
+ // Add event listeners
509
+ settingsButton.addEventListener('click', this.settingsClickHandler);
510
+ document.addEventListener('click', this.documentClickHandler);
511
+ }
512
+
513
+ // Handle metric selection
514
+ if (metricOptions) {
515
+ this.metricHandlers = [];
516
+ metricOptions.forEach(option => {
517
+ const handler = (e) => {
518
+ const metric = e.target.dataset.metric;
519
+ this.changeMetric(metric);
520
+
521
+ // Update active state
522
+ metricOptions.forEach(opt => opt.classList.remove('active'));
523
+ e.target.classList.add('active');
524
+
525
+ // Close dropdown
526
+ dropdown.classList.remove('show');
527
+ };
528
+
529
+ option.addEventListener('click', handler);
530
+ this.metricHandlers.push({ element: option, handler });
531
+ });
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Remove existing settings event listeners to prevent duplicates
537
+ */
538
+ removeSettingsListeners() {
539
+ const settingsButton = document.querySelector('.heatmap-settings');
540
+
541
+ if (this.settingsClickHandler && settingsButton) {
542
+ settingsButton.removeEventListener('click', this.settingsClickHandler);
543
+ }
544
+
545
+ if (this.documentClickHandler) {
546
+ document.removeEventListener('click', this.documentClickHandler);
547
+ }
548
+
549
+ if (this.metricHandlers) {
550
+ this.metricHandlers.forEach(({ element, handler }) => {
551
+ element.removeEventListener('click', handler);
552
+ });
553
+ this.metricHandlers = [];
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Change the activity metric (messages or tools)
559
+ */
560
+ async changeMetric(metric) {
561
+ console.log(`🔥 Changing metric to: ${metric}`);
562
+ this.currentMetric = metric;
563
+
564
+ // Recalculate thresholds based on new metric
565
+ if (this.activityData) {
566
+ await this.recalculateForMetric(metric);
567
+ await this.renderHeatmap();
568
+ this.updateTitle();
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Recalculate activity data for the new metric
574
+ */
575
+ async recalculateForMetric(metric) {
576
+ const { dailyActivity } = this.activityData;
577
+
578
+ // Recalculate thresholds based on the new metric
579
+ const metricCounts = Array.from(dailyActivity.values())
580
+ .map(activity => activity[metric] || 0)
581
+ .filter(count => count > 0)
582
+ .sort((a, b) => a - b);
583
+
584
+ const thresholds = this.calculateDynamicThresholds(metricCounts);
585
+
586
+ // Calculate new total activity
587
+ let totalActivity = 0;
588
+ dailyActivity.forEach(activity => {
589
+ totalActivity += activity[metric] || 0;
590
+ });
591
+
592
+ this.activityData.thresholds = thresholds;
593
+ this.activityData.totalActivity = totalActivity;
594
+
595
+ console.log(`🔥 Recalculated for ${metric}:`, thresholds);
596
+ console.log(`🔥 New total: ${totalActivity}`);
597
+ }
598
+
599
+ /**
600
+ * Show tooltip on day hover
601
+ */
602
+ showTooltip(event) {
603
+ const day = event.target;
604
+ const date = day.dataset.date;
605
+ const activity = JSON.parse(day.dataset.activity || '{}');
606
+
607
+ if (!date) return;
608
+
609
+ const dateObj = new Date(date);
610
+ const formattedDate = dateObj.toLocaleDateString('en-US', {
611
+ weekday: 'short',
612
+ month: 'short',
613
+ day: 'numeric',
614
+ year: 'numeric'
615
+ });
616
+
617
+ const currentValue = activity[this.currentMetric] || 0;
618
+ const otherMetric = this.currentMetric === 'messages' ? 'conversations' : 'messages';
619
+ const otherValue = activity[otherMetric] || 0;
620
+
621
+ let activityText = `No ${this.currentMetric}`;
622
+ if (currentValue > 0) {
623
+ const suffix = currentValue === 1 ? '' : 's';
624
+ activityText = `${currentValue} ${this.currentMetric.slice(0, -1)}${suffix}`;
625
+
626
+ if (otherValue > 0) {
627
+ const otherSuffix = otherValue === 1 ? '' : 's';
628
+ const otherLabel = otherMetric === 'conversations' ? 'conversation' : 'message';
629
+ activityText += ` • ${otherValue} ${otherLabel}${otherSuffix}`;
630
+ }
631
+ }
632
+
633
+ this.tooltip.querySelector('.heatmap-tooltip-date').textContent = formattedDate;
634
+ this.tooltip.querySelector('.heatmap-tooltip-activity').textContent = activityText;
635
+
636
+ this.tooltip.classList.add('show');
637
+ this.updateTooltipPosition(event);
638
+ }
639
+
640
+ /**
641
+ * Hide tooltip
642
+ */
643
+ hideTooltip() {
644
+ this.tooltip.classList.remove('show');
645
+ }
646
+
647
+ /**
648
+ * Update tooltip position
649
+ */
650
+ updateTooltipPosition(event) {
651
+ const tooltip = this.tooltip;
652
+ const rect = tooltip.getBoundingClientRect();
653
+
654
+ let x = event.pageX + 10;
655
+ let y = event.pageY - rect.height - 10;
656
+
657
+ // Adjust if tooltip would go off screen
658
+ if (x + rect.width > window.innerWidth) {
659
+ x = event.pageX - rect.width - 10;
660
+ }
661
+
662
+ if (y < window.scrollY) {
663
+ y = event.pageY + 10;
664
+ }
665
+
666
+ tooltip.style.left = `${x}px`;
667
+ tooltip.style.top = `${y}px`;
668
+ }
669
+
670
+ /**
671
+ * Update the title with total activity count
672
+ */
673
+ updateTitle() {
674
+ if (!this.activityData) return;
675
+
676
+ const { totalActivity } = this.activityData;
677
+ const titleElement = document.getElementById('activity-total');
678
+
679
+ if (titleElement) {
680
+ titleElement.textContent = `${this.formatNumber(totalActivity)} ${this.currentMetric} in the last year`;
681
+ }
682
+ }
683
+
684
+ /**
685
+ * Format large numbers with commas
686
+ */
687
+ formatNumber(num) {
688
+ if (num >= 1000) {
689
+ return (num / 1000).toFixed(1) + 'k';
690
+ }
691
+ return num.toLocaleString();
692
+ }
693
+
694
+ /**
695
+ * Show error state
696
+ */
697
+ showErrorState() {
698
+ const container = this.container.querySelector('.activity-heatmap-container');
699
+ container.innerHTML = `
700
+ <div class="heatmap-empty-state">
701
+ <div class="heatmap-empty-icon">📊</div>
702
+ <div class="heatmap-empty-text">Unable to load activity data</div>
703
+ <div class="heatmap-empty-subtext">Please try refreshing the page</div>
704
+ </div>
705
+ `;
706
+ }
707
+
708
+ /**
709
+ * Refresh the heatmap data
710
+ */
711
+ async refresh() {
712
+ console.log('🔥 Refreshing heatmap data...');
713
+ await this.loadActivityData();
714
+ this.positionMonthLabels();
715
+ }
716
+
717
+ /**
718
+ * Cleanup resources
719
+ */
720
+ destroy() {
721
+ if (this.tooltip && this.tooltip.parentNode) {
722
+ this.tooltip.parentNode.removeChild(this.tooltip);
723
+ }
724
+
725
+ // Remove resize listener
726
+ if (this.resizeHandler) {
727
+ window.removeEventListener('resize', this.resizeHandler);
728
+ }
729
+
730
+ // Remove settings listeners
731
+ this.removeSettingsListeners();
732
+
733
+ console.log('🔥 ActivityHeatmap destroyed');
734
+ }
735
+ }
736
+
737
+ // Make it globally available
738
+ window.ActivityHeatmap = ActivityHeatmap;