fix bugs
#4
by
evijit
HF Staff
- opened
- src/components/Heatmap.tsx +23 -16
- src/components/HeatmapGrid.tsx +8 -3
- src/components/UserSearchDialog.tsx +1 -0
- src/components/WeeklyHeatmap.tsx +49 -22
- src/pages/[author]/index.tsx +1 -0
- src/utils/heatmapColors.ts +27 -0
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 |
-
//
|
| 32 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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={
|
| 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={
|
| 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 |
-
//
|
| 18 |
-
const
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
//
|
| 30 |
-
const
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 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]";
|