| 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; |
|
|