| import React, { useMemo, useCallback, useEffect, useState } from "react"; |
| import { Tooltip } from "@mui/material"; |
| import { getWeekDateRange } from "../utils/weeklyCalendar"; |
| import { getHeatmapColorIntensity } from "../utils/heatmapColors"; |
|
|
| type WeeklyActivity = { |
| date: string; |
| count: number; |
| level: number; |
| }; |
|
|
| type WeeklyHeatmapProps = { |
| data: WeeklyActivity[]; |
| color: string; |
| }; |
|
|
| const WeeklyHeatmap: React.FC<WeeklyHeatmapProps> = ({ data, color }) => { |
| |
| const [isDark, setIsDark] = useState(false); |
| |
| useEffect(() => { |
| const checkTheme = () => { |
| setIsDark(document.documentElement.classList.contains('dark')); |
| }; |
| |
| |
| checkTheme(); |
| |
| |
| const observer = new MutationObserver(checkTheme); |
| observer.observe(document.documentElement, { |
| attributes: true, |
| attributeFilter: ['class'] |
| }); |
| |
| return () => observer.disconnect(); |
| }, []); |
| |
| const groupedData = useMemo(() => { |
| return data.reduce((acc, activity) => { |
| const date = new Date(activity.date); |
| const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; |
| |
| if (!acc[yearMonth]) { |
| acc[yearMonth] = []; |
| } |
| acc[yearMonth].push(activity); |
| return acc; |
| }, {} as Record<string, WeeklyActivity[]>); |
| }, [data]); |
|
|
| |
| const sortedMonths = useMemo(() => Object.keys(groupedData).sort(), [groupedData]); |
|
|
| |
| const getColorIntensity = useCallback((level: number) => { |
| return getHeatmapColorIntensity(level, color) || undefined; |
| }, [color]); |
| |
| |
| const emptyDotColor = useMemo(() => { |
| return isDark ? '#374151' : '#d1d5db'; |
| }, [isDark]); |
|
|
| |
| const getMonthName = (yearMonth: string) => { |
| const [year, month] = yearMonth.split('-'); |
| const date = new Date(parseInt(year), parseInt(month) - 1); |
| return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); |
| }; |
|
|
| return ( |
| <div className="w-full"> |
| <div className="flex flex-wrap gap-4 justify-center"> |
| {sortedMonths.map((yearMonth) => { |
| const monthData = groupedData[yearMonth]; |
| return ( |
| <div key={yearMonth} className="flex flex-col items-center"> |
| <div className="text-xs mb-2 text-gray-600 dark:text-gray-400"> |
| {getMonthName(yearMonth)} |
| </div> |
| <div className="flex gap-1"> |
| {monthData.map((activity, index) => ( |
| <Tooltip |
| key={`${yearMonth}-${index}`} |
| title={`${activity.count} new repos in week of ${getWeekDateRange(activity.date)}`} |
| arrow |
| > |
| <div |
| className="w-3 h-3 rounded-sm cursor-pointer transition-opacity hover:opacity-80" |
| style={{ |
| backgroundColor: activity.level === 0 ? emptyDotColor : getColorIntensity(activity.level), |
| }} |
| /> |
| </Tooltip> |
| ))} |
| </div> |
| </div> |
| ); |
| })} |
| </div> |
| |
| {/* Legend */} |
| <div className="flex items-center justify-center mt-4 text-xs text-gray-600 dark:text-gray-400"> |
| <span className="mr-2">Less</span> |
| <div className="flex gap-1"> |
| {[0, 1, 2, 3, 4].map((level) => ( |
| <div |
| key={level} |
| className="w-2.5 h-2.5 rounded-sm" |
| style={{ |
| backgroundColor: level === 0 ? emptyDotColor : getColorIntensity(level), |
| }} |
| /> |
| ))} |
| </div> |
| <span className="ml-2">More</span> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default React.memo(WeeklyHeatmap); |