src/components/Heatmap.tsx CHANGED
@@ -1,8 +1,9 @@
1
- import React 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
 
8
  type ViewMode = 'daily' | 'weekly';
@@ -28,8 +29,24 @@ const Heatmap: React.FC<HeatmapProps> = ({
28
  showHeader = true,
29
  viewMode
30
  }) => {
31
- // Process data based on view mode
32
- const processedData = viewMode === 'weekly' ? aggregateToWeeklyData(data) : data;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  return (
35
  <div className="flex flex-col items-center w-full mx-auto">
@@ -58,21 +75,11 @@ const Heatmap: React.FC<HeatmapProps> = ({
58
  ) : (
59
  <ActivityCalendar
60
  data={processedData}
61
- theme={{
62
- dark: ["#161b22", color],
63
- light: ["#e0e0e0", color],
64
- }}
65
  blockSize={11}
66
  blockMargin={2}
67
  hideTotalCount
68
- renderBlock={(block, activity) => (
69
- <Tooltip
70
- title={`${activity.count} new repos on ${activity.date}`}
71
- arrow
72
- >
73
- {block}
74
- </Tooltip>
75
- )}
76
  />
77
  )}
78
  </div>
@@ -80,4 +87,4 @@ const Heatmap: React.FC<HeatmapProps> = ({
80
  );
81
  };
82
 
83
- export default Heatmap;
 
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';
 
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
  ) : (
76
  <ActivityCalendar
77
  data={processedData}
78
+ theme={theme}
 
 
 
79
  blockSize={11}
80
  blockMargin={2}
81
  hideTotalCount
82
+ renderBlock={renderBlock}
 
 
 
 
 
 
 
83
  />
84
  )}
85
  </div>
 
87
  );
88
  };
89
 
90
+ export default React.memo(Heatmap);
src/components/HeatmapGrid.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react";
2
  import { ProviderInfo, CalendarData } from "../types/heatmap";
3
  import OrganizationCard from "./OrganizationCard";
4
  import ProviderHeatmapSkeleton from "./ProviderHeatmapSkeleton";
@@ -15,6 +15,11 @@ interface HeatmapGridProps {
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,7 +36,7 @@ const HeatmapGrid: React.FC<HeatmapGridProps> = ({ sortedProviders, calendarData
31
  <div className="flex justify-center">
32
  <ViewToggle
33
  viewMode={viewMode}
34
- onToggle={setViewMode}
35
  />
36
  </div>
37
 
@@ -49,4 +54,4 @@ const HeatmapGrid: React.FC<HeatmapGridProps> = ({ sortedProviders, calendarData
49
  );
50
  };
51
 
52
- export default HeatmapGrid;
 
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
  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
  <div className="flex justify-center">
37
  <ViewToggle
38
  viewMode={viewMode}
39
+ onToggle={handleViewModeToggle}
40
  />
41
  </div>
42
 
 
54
  );
55
  };
56
 
57
+ export default React.memo(HeatmapGrid);
src/components/UserSearchDialog.tsx CHANGED
@@ -131,6 +131,7 @@ const UserSearchDialog = () => {
131
  fullName={userInfo.fullName}
132
  avatarUrl={userInfo.avatarUrl || ''}
133
  authorId={currentSearchTerm}
 
134
  />
135
  </div>
136
  <div>
 
131
  fullName={userInfo.fullName}
132
  avatarUrl={userInfo.avatarUrl || ''}
133
  authorId={currentSearchTerm}
134
+ viewMode="daily"
135
  />
136
  </div>
137
  <div>
src/components/WeeklyHeatmap.tsx CHANGED
@@ -1,6 +1,7 @@
1
- import React from "react";
2
  import { Tooltip } from "@mui/material";
3
  import { getWeekDateRange } from "../utils/weeklyCalendar";
 
4
 
5
  type WeeklyActivity = {
6
  date: string;
@@ -14,24 +15,52 @@ type WeeklyHeatmapProps = {
14
  };
15
 
16
  const WeeklyHeatmap: React.FC<WeeklyHeatmapProps> = ({ data, color }) => {
17
- // Group data by month and year
18
- const groupedData = data.reduce((acc, activity) => {
19
- const date = new Date(activity.date);
20
- const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
 
 
 
21
 
22
- if (!acc[yearMonth]) {
23
- acc[yearMonth] = [];
24
- }
25
- acc[yearMonth].push(activity);
26
- return acc;
27
- }, {} as Record<string, WeeklyActivity[]>);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- // Get color intensity based on level
30
- const getColorIntensity = (level: number) => {
31
- if (level === 0) return '#161b22'; // Dark background for no activity
32
- const intensities = ['#0e4429', '#006d32', '#26a641', '#39d353'];
33
- return intensities[Math.min(level - 1, 3)] || color;
34
- };
 
 
 
 
 
 
35
 
36
  // Get month names
37
  const getMonthName = (yearMonth: string) => {
@@ -40,8 +69,6 @@ const WeeklyHeatmap: React.FC<WeeklyHeatmapProps> = ({ data, color }) => {
40
  return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
41
  };
42
 
43
- const sortedMonths = Object.keys(groupedData).sort();
44
-
45
  return (
46
  <div className="w-full">
47
  <div className="flex flex-wrap gap-4 justify-center">
@@ -62,7 +89,7 @@ const WeeklyHeatmap: React.FC<WeeklyHeatmapProps> = ({ data, color }) => {
62
  <div
63
  className="w-3 h-3 rounded-sm cursor-pointer transition-opacity hover:opacity-80"
64
  style={{
65
- backgroundColor: getColorIntensity(activity.level),
66
  }}
67
  />
68
  </Tooltip>
@@ -82,7 +109,7 @@ const WeeklyHeatmap: React.FC<WeeklyHeatmapProps> = ({ data, color }) => {
82
  key={level}
83
  className="w-2.5 h-2.5 rounded-sm"
84
  style={{
85
- backgroundColor: getColorIntensity(level),
86
  }}
87
  />
88
  ))}
@@ -93,4 +120,4 @@ const WeeklyHeatmap: React.FC<WeeklyHeatmapProps> = ({ data, color }) => {
93
  );
94
  };
95
 
96
- export default WeeklyHeatmap;
 
1
+ import React, { useMemo, useCallback, useEffect, useState } from "react";
2
  import { Tooltip } from "@mui/material";
3
  import { getWeekDateRange } from "../utils/weeklyCalendar";
4
+ import { getHeatmapColorIntensity } from "../utils/heatmapColors";
5
 
6
  type WeeklyActivity = {
7
  date: string;
 
15
  };
16
 
17
  const WeeklyHeatmap: React.FC<WeeklyHeatmapProps> = ({ data, color }) => {
18
+ // Track theme changes
19
+ const [isDark, setIsDark] = useState(false);
20
+
21
+ useEffect(() => {
22
+ const checkTheme = () => {
23
+ setIsDark(document.documentElement.classList.contains('dark'));
24
+ };
25
 
26
+ // Initial check
27
+ checkTheme();
28
+
29
+ // Watch for theme changes
30
+ const observer = new MutationObserver(checkTheme);
31
+ observer.observe(document.documentElement, {
32
+ attributes: true,
33
+ attributeFilter: ['class']
34
+ });
35
+
36
+ return () => observer.disconnect();
37
+ }, []);
38
+ // Memoize the grouped data to avoid recalculation
39
+ const groupedData = useMemo(() => {
40
+ return data.reduce((acc, activity) => {
41
+ const date = new Date(activity.date);
42
+ const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
43
+
44
+ if (!acc[yearMonth]) {
45
+ acc[yearMonth] = [];
46
+ }
47
+ acc[yearMonth].push(activity);
48
+ return acc;
49
+ }, {} as Record<string, WeeklyActivity[]>);
50
+ }, [data]);
51
 
52
+ // Memoize sorted months
53
+ const sortedMonths = useMemo(() => Object.keys(groupedData).sort(), [groupedData]);
54
+
55
+ // Memoize the color intensity function
56
+ const getColorIntensity = useCallback((level: number) => {
57
+ return getHeatmapColorIntensity(level, color) || undefined;
58
+ }, [color]);
59
+
60
+ // Get the exact same empty dot color as ActivityCalendar
61
+ const emptyDotColor = useMemo(() => {
62
+ return isDark ? '#374151' : '#d1d5db';
63
+ }, [isDark]);
64
 
65
  // Get month names
66
  const getMonthName = (yearMonth: string) => {
 
69
  return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
70
  };
71
 
 
 
72
  return (
73
  <div className="w-full">
74
  <div className="flex flex-wrap gap-4 justify-center">
 
89
  <div
90
  className="w-3 h-3 rounded-sm cursor-pointer transition-opacity hover:opacity-80"
91
  style={{
92
+ backgroundColor: activity.level === 0 ? emptyDotColor : getColorIntensity(activity.level),
93
  }}
94
  />
95
  </Tooltip>
 
109
  key={level}
110
  className="w-2.5 h-2.5 rounded-sm"
111
  style={{
112
+ backgroundColor: level === 0 ? emptyDotColor : getColorIntensity(level),
113
  }}
114
  />
115
  ))}
 
120
  );
121
  };
122
 
123
+ export default React.memo(WeeklyHeatmap);
src/pages/[author]/index.tsx CHANGED
@@ -40,6 +40,7 @@ const OpenSourceHeatmap: React.FC<OpenSourceHeatmapProps> = ({
40
  fullName={fullName ?? providerName}
41
  avatarUrl={avatarUrl ?? ''}
42
  authorId={providerName}
 
43
  />
44
  ))}
45
  </div>
 
40
  fullName={fullName ?? providerName}
41
  avatarUrl={avatarUrl ?? ''}
42
  authorId={providerName}
43
+ viewMode="daily"
44
  />
45
  ))}
46
  </div>
src/utils/heatmapColors.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Shared color utilities for consistent heatmap theming
2
+
3
+ // Use exact same colors for both daily and weekly views
4
+ const EMPTY_DOT_DARK = "#374151"; // gray-600 equivalent
5
+ const EMPTY_DOT_LIGHT = "#d1d5db"; // gray-300 equivalent
6
+
7
+ export const getHeatmapTheme = (primaryColor: string) => ({
8
+ dark: [EMPTY_DOT_DARK, primaryColor],
9
+ light: [EMPTY_DOT_LIGHT, primaryColor],
10
+ });
11
+
12
+ export const getHeatmapColorIntensity = (level: number, primaryColor: string) => {
13
+ if (level === 0) {
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 = () => ({
23
+ dark: EMPTY_DOT_DARK,
24
+ light: EMPTY_DOT_LIGHT,
25
+ });
26
+
27
+ export const getEmptyDotClasses = () => "bg-[#d1d5db] dark:bg-[#374151]";