src/components/Heatmap.tsx CHANGED
@@ -1,10 +1,10 @@
1
- import React, { useMemo, useCallback } from "react";
2
  import ActivityCalendar from "react-activity-calendar";
3
  import { Tooltip, Avatar } from "@mui/material";
4
  import Link from "next/link";
5
  import { aggregateToWeeklyData } from "../utils/weeklyCalendar";
6
- import { getHeatmapTheme } from "../utils/heatmapColors";
7
  import WeeklyHeatmap from "./WeeklyHeatmap";
 
8
 
9
  type ViewMode = 'daily' | 'weekly';
10
 
@@ -29,24 +29,41 @@ const Heatmap: React.FC<HeatmapProps> = ({
29
  showHeader = true,
30
  viewMode
31
  }) => {
32
- // Memoize the weekly data processing to avoid recalculation on every render
33
- const weeklyData = useMemo(() => aggregateToWeeklyData(data), [data]);
34
 
35
- // Choose data based on view mode
36
- const processedData = viewMode === 'weekly' ? weeklyData : data;
37
 
38
- // Memoize the theme to prevent recreation
39
- const theme = useMemo(() => getHeatmapTheme(color), [color]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- // Memoize the render block callback
42
- const renderBlock = useCallback((block: React.ReactElement, activity: any) => (
43
- <Tooltip
44
- title={`${activity.count} new repos on ${activity.date}`}
45
- arrow
46
- >
47
- {block}
48
- </Tooltip>
49
- ), []);
 
 
50
 
51
  return (
52
  <div className="flex flex-col items-center w-full mx-auto">
@@ -75,11 +92,21 @@ const Heatmap: React.FC<HeatmapProps> = ({
75
  ) : (
76
  <ActivityCalendar
77
  data={processedData}
78
- theme={theme}
 
 
 
79
  blockSize={11}
80
  blockMargin={2}
81
  hideTotalCount
82
- renderBlock={renderBlock}
 
 
 
 
 
 
 
83
  />
84
  )}
85
  </div>
@@ -87,4 +114,4 @@ const Heatmap: React.FC<HeatmapProps> = ({
87
  );
88
  };
89
 
90
- export default React.memo(Heatmap);
 
1
+ import React, { useEffect, useState } from "react";
2
  import ActivityCalendar from "react-activity-calendar";
3
  import { Tooltip, Avatar } from "@mui/material";
4
  import Link from "next/link";
5
  import { aggregateToWeeklyData } from "../utils/weeklyCalendar";
 
6
  import WeeklyHeatmap from "./WeeklyHeatmap";
7
+ import { getHeatmapTheme, getHeatmapColorIntensity } from "../utils/heatmapColors";
8
 
9
  type ViewMode = 'daily' | 'weekly';
10
 
 
29
  showHeader = true,
30
  viewMode
31
  }) => {
32
+ // Process data based on view mode
33
+ const processedData = viewMode === 'weekly' ? aggregateToWeeklyData(data) : data;
34
 
35
+ // Track theme state for proper ActivityCalendar theming
36
+ const [isDarkMode, setIsDarkMode] = useState(false);
37
 
38
+ useEffect(() => {
39
+ // Check initial theme
40
+ const checkTheme = () => {
41
+ setIsDarkMode(document.documentElement.classList.contains('dark'));
42
+ };
43
+
44
+ checkTheme();
45
+
46
+ // Watch for theme changes
47
+ const observer = new MutationObserver(checkTheme);
48
+ observer.observe(document.documentElement, {
49
+ attributes: true,
50
+ attributeFilter: ['class']
51
+ });
52
+
53
+ return () => observer.disconnect();
54
+ }, []);
55
 
56
+ // Use theme-aware colors
57
+ const emptyColor = isDarkMode ? "#374151" : "#d1d5db";
58
+
59
+ // Create intensity levels for daily view (same as weekly)
60
+ const intensityColors = [
61
+ emptyColor, // level 0
62
+ getHeatmapColorIntensity(1, color), // level 1 (40% intensity)
63
+ getHeatmapColorIntensity(2, color), // level 2 (60% intensity)
64
+ getHeatmapColorIntensity(3, color), // level 3 (80% intensity)
65
+ getHeatmapColorIntensity(4, color) // level 4 (100% intensity)
66
+ ].filter((color): color is string => color !== null); // Remove any null values and type guard
67
 
68
  return (
69
  <div className="flex flex-col items-center w-full mx-auto">
 
92
  ) : (
93
  <ActivityCalendar
94
  data={processedData}
95
+ theme={{
96
+ light: intensityColors,
97
+ dark: intensityColors
98
+ }}
99
  blockSize={11}
100
  blockMargin={2}
101
  hideTotalCount
102
+ renderBlock={(block, activity) => (
103
+ <Tooltip
104
+ title={`${activity.count} new repos on ${activity.date}`}
105
+ arrow
106
+ >
107
+ {block}
108
+ </Tooltip>
109
+ )}
110
  />
111
  )}
112
  </div>
 
114
  );
115
  };
116
 
117
+ export default Heatmap;
src/components/HeatmapGrid.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useCallback } from "react";
2
  import { ProviderInfo, CalendarData } from "../types/heatmap";
3
  import OrganizationCard from "./OrganizationCard";
4
  import ProviderHeatmapSkeleton from "./ProviderHeatmapSkeleton";
@@ -15,11 +15,6 @@ interface HeatmapGridProps {
15
  const HeatmapGrid: React.FC<HeatmapGridProps> = ({ sortedProviders, calendarData, isLoading }) => {
16
  const [viewMode, setViewMode] = useState<ViewMode>('weekly');
17
 
18
- // Memoize the toggle handler to prevent unnecessary re-renders
19
- const handleViewModeToggle = useCallback((newMode: ViewMode) => {
20
- setViewMode(newMode);
21
- }, []);
22
-
23
  if (isLoading) {
24
  return (
25
  <div className="flex flex-col gap-8 max-w-4xl mx-auto mb-16">
@@ -36,7 +31,7 @@ const HeatmapGrid: React.FC<HeatmapGridProps> = ({ sortedProviders, calendarData
36
  <div className="flex justify-center">
37
  <ViewToggle
38
  viewMode={viewMode}
39
- onToggle={handleViewModeToggle}
40
  />
41
  </div>
42
 
@@ -54,4 +49,4 @@ const HeatmapGrid: React.FC<HeatmapGridProps> = ({ sortedProviders, calendarData
54
  );
55
  };
56
 
57
- export default React.memo(HeatmapGrid);
 
1
+ import React, { useState } from "react";
2
  import { ProviderInfo, CalendarData } from "../types/heatmap";
3
  import OrganizationCard from "./OrganizationCard";
4
  import ProviderHeatmapSkeleton from "./ProviderHeatmapSkeleton";
 
15
  const HeatmapGrid: React.FC<HeatmapGridProps> = ({ sortedProviders, calendarData, isLoading }) => {
16
  const [viewMode, setViewMode] = useState<ViewMode>('weekly');
17
 
 
 
 
 
 
18
  if (isLoading) {
19
  return (
20
  <div className="flex flex-col gap-8 max-w-4xl mx-auto mb-16">
 
31
  <div className="flex justify-center">
32
  <ViewToggle
33
  viewMode={viewMode}
34
+ onToggle={setViewMode}
35
  />
36
  </div>
37
 
 
49
  );
50
  };
51
 
52
+ export default HeatmapGrid;
src/styles/globals.css CHANGED
@@ -88,3 +88,64 @@
88
  display: none; /* Safari and Chrome */
89
  }
90
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  display: none; /* Safari and Chrome */
89
  }
90
  }
91
+
92
+ /* Activity Calendar theme overrides for consistent empty dot colors */
93
+ /* Try multiple possible selectors for react-activity-calendar */
94
+ .react-activity-calendar rect[data-level="0"] {
95
+ fill: #d1d5db !important; /* Light theme empty dots */
96
+ }
97
+
98
+ .dark .react-activity-calendar rect[data-level="0"] {
99
+ fill: #374151 !important; /* Dark theme empty dots */
100
+ }
101
+
102
+ /* Alternative selectors in case the structure is different */
103
+ .react-activity-calendar .react-activity-calendar__block[data-level="0"] {
104
+ fill: #d1d5db !important;
105
+ }
106
+
107
+ .dark .react-activity-calendar .react-activity-calendar__block[data-level="0"] {
108
+ fill: #374151 !important;
109
+ }
110
+
111
+ /* More general fallback selectors */
112
+ .react-activity-calendar [data-level="0"] {
113
+ fill: #d1d5db !important;
114
+ }
115
+
116
+ .dark .react-activity-calendar [data-level="0"] {
117
+ fill: #374151 !important;
118
+ }
119
+
120
+ /* SVG rect elements specifically */
121
+ .react-activity-calendar svg rect[data-level="0"] {
122
+ fill: #d1d5db !important;
123
+ }
124
+
125
+ .dark .react-activity-calendar svg rect[data-level="0"] {
126
+ fill: #374151 !important;
127
+ }
128
+
129
+ /* Override any background-color styles as well */
130
+ .react-activity-calendar [data-level="0"] {
131
+ fill: #d1d5db !important;
132
+ background-color: #d1d5db !important;
133
+ }
134
+
135
+ .dark .react-activity-calendar [data-level="0"] {
136
+ fill: #374151 !important;
137
+ background-color: #374151 !important;
138
+ }
139
+
140
+ /* Even more specific selectors with higher specificity */
141
+ html.dark .react-activity-calendar svg rect[data-level="0"],
142
+ html.dark .react-activity-calendar [data-level="0"] {
143
+ fill: #374151 !important;
144
+ background-color: #374151 !important;
145
+ }
146
+
147
+ html:not(.dark) .react-activity-calendar svg rect[data-level="0"],
148
+ html:not(.dark) .react-activity-calendar [data-level="0"] {
149
+ fill: #d1d5db !important;
150
+ background-color: #d1d5db !important;
151
+ }
src/utils/heatmapColors.ts CHANGED
@@ -14,9 +14,37 @@ export const getHeatmapColorIntensity = (level: number, primaryColor: string) =>
14
  return null; // Will use CSS classes for theme-aware empty state
15
  }
16
 
17
- // Use different intensities of the primary color or default green scale
18
- const greenIntensities = ['#0e4429', '#006d32', '#26a641', '#39d353'];
19
- return greenIntensities[Math.min(level - 1, 3)] || primaryColor;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  };
21
 
22
  export const getEmptyDotColors = () => ({
 
14
  return null; // Will use CSS classes for theme-aware empty state
15
  }
16
 
17
+ // Create different intensities of the primary color
18
+ const hexToRgb = (hex: string) => {
19
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
20
+ return result ? {
21
+ r: parseInt(result[1], 16),
22
+ g: parseInt(result[2], 16),
23
+ b: parseInt(result[3], 16)
24
+ } : null;
25
+ };
26
+
27
+ const rgbToHex = (r: number, g: number, b: number) => {
28
+ return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
29
+ };
30
+
31
+ const rgb = hexToRgb(primaryColor);
32
+ if (!rgb) {
33
+ // Fallback to green scale if color parsing fails
34
+ const greenIntensities = ['#0e4429', '#006d32', '#26a641', '#39d353'];
35
+ return greenIntensities[Math.min(level - 1, 3)];
36
+ }
37
+
38
+ // Create intensity levels by adjusting brightness
39
+ // Level 1: 40% intensity, Level 2: 60%, Level 3: 80%, Level 4: 100%
40
+ const intensityMultipliers = [0.4, 0.6, 0.8, 1.0];
41
+ const multiplier = intensityMultipliers[Math.min(level - 1, 3)];
42
+
43
+ const adjustedR = Math.round(rgb.r * multiplier);
44
+ const adjustedG = Math.round(rgb.g * multiplier);
45
+ const adjustedB = Math.round(rgb.b * multiplier);
46
+
47
+ return rgbToHex(adjustedR, adjustedG, adjustedB);
48
  };
49
 
50
  export const getEmptyDotColors = () => ({