|
|
import React, { useEffect, useState } from "react"; |
|
|
import ActivityCalendar from "react-activity-calendar"; |
|
|
import { Tooltip, Avatar } from "@mui/material"; |
|
|
import Link from "next/link"; |
|
|
import { aggregateToWeeklyData } from "../utils/weeklyCalendar"; |
|
|
import WeeklyHeatmap from "./WeeklyHeatmap"; |
|
|
import { getHeatmapTheme, getHeatmapColorIntensity } from "../utils/heatmapColors"; |
|
|
|
|
|
type ViewMode = 'daily' | 'weekly'; |
|
|
|
|
|
type HeatmapProps = { |
|
|
data: Array<{ date: string; count: number; level: number }>; |
|
|
color: string; |
|
|
providerName: string; |
|
|
fullName: string; |
|
|
avatarUrl: string; |
|
|
authorId: string; |
|
|
showHeader?: boolean; |
|
|
viewMode: ViewMode; |
|
|
}; |
|
|
|
|
|
const Heatmap: React.FC<HeatmapProps> = ({ |
|
|
data, |
|
|
color, |
|
|
providerName, |
|
|
fullName, |
|
|
avatarUrl, |
|
|
authorId, |
|
|
showHeader = true, |
|
|
viewMode |
|
|
}) => { |
|
|
|
|
|
const processedData = viewMode === 'weekly' ? aggregateToWeeklyData(data) : data; |
|
|
|
|
|
|
|
|
const [isDarkMode, setIsDarkMode] = useState(false); |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
const checkTheme = () => { |
|
|
setIsDarkMode(document.documentElement.classList.contains('dark')); |
|
|
}; |
|
|
|
|
|
checkTheme(); |
|
|
|
|
|
|
|
|
const observer = new MutationObserver(checkTheme); |
|
|
observer.observe(document.documentElement, { |
|
|
attributes: true, |
|
|
attributeFilter: ['class'] |
|
|
}); |
|
|
|
|
|
return () => observer.disconnect(); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const emptyColor = isDarkMode ? "#374151" : "#d1d5db"; |
|
|
|
|
|
|
|
|
const getThemeAwareColors = () => { |
|
|
const colors = [emptyColor]; |
|
|
|
|
|
for (let level = 1; level <= 4; level++) { |
|
|
const intensity = getHeatmapColorIntensity(level, color, isDarkMode); |
|
|
if (intensity) { |
|
|
colors.push(intensity); |
|
|
} |
|
|
} |
|
|
return colors; |
|
|
}; |
|
|
|
|
|
const themeColors = getThemeAwareColors(); |
|
|
|
|
|
return ( |
|
|
<div className="flex flex-col items-center w-full mx-auto"> |
|
|
{showHeader && ( |
|
|
<div className="flex flex-col sm:flex-row items-center mb-4 w-full justify-center"> |
|
|
{avatarUrl && ( |
|
|
<Avatar src={avatarUrl} alt={fullName} className="mb-2 sm:mb-0 sm:mr-4" sx={{ width: 48, height: 48 }} /> |
|
|
)} |
|
|
<div className="text-center sm:text-left"> |
|
|
<h2 className="text-lg font-semibold"> |
|
|
<Link |
|
|
href={`https://huggingface.co/${authorId}`} |
|
|
target="_blank" |
|
|
rel="noopener noreferrer" |
|
|
className="hover:text-blue-500 hover:underline" |
|
|
> |
|
|
{fullName} |
|
|
</Link> |
|
|
</h2> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
<div className="w-full overflow-x-auto flex justify-center"> |
|
|
{viewMode === 'weekly' ? ( |
|
|
<WeeklyHeatmap data={processedData} color={color} /> |
|
|
) : ( |
|
|
<ActivityCalendar |
|
|
key={`${isDarkMode}-${color}`} // Force re-render on theme change |
|
|
data={processedData} |
|
|
theme={{ |
|
|
light: themeColors, |
|
|
dark: themeColors |
|
|
}} |
|
|
blockSize={11} |
|
|
blockMargin={2} |
|
|
hideTotalCount |
|
|
renderBlock={(block, activity) => { |
|
|
const activityData = activity as any; // Type assertion since react-activity-calendar may not have our extended type |
|
|
const itemsText = activityData.items && activityData.items.length > 0 |
|
|
? activityData.items.join(', ') |
|
|
: 'No releases'; |
|
|
|
|
|
const tooltipTitle = activity.count > 0 |
|
|
? `${activity.count} new repos on ${activity.date}: ${itemsText}` |
|
|
: `No repos on ${activity.date}`; |
|
|
|
|
|
return ( |
|
|
<Tooltip |
|
|
title={tooltipTitle} |
|
|
arrow |
|
|
> |
|
|
{block} |
|
|
</Tooltip> |
|
|
); |
|
|
}} |
|
|
/> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default Heatmap; |
|
|
|