import React, { useEffect, useState } from "react"; import { cn } from "@/lib/utils"; interface ContributionDay { date: string; count: number; } interface GithubContributionsProps { username?: string; githubKey?: string; className?: string; year?: number; } const colorLevels = [ "bg-[#ebedf0]", // 0 contributions "bg-[#9be9a8]", // 1-3 contributions "bg-[#40c463]", // 4-7 contributions "bg-[#30a14e]", // 8-12 contributions "bg-[#216e39]", // 13+ contributions ]; const getColorLevel = (count: number): string => { if (count === 0) return colorLevels[0]; if (count <= 3) return colorLevels[1]; if (count <= 7) return colorLevels[2]; if (count <= 12) return colorLevels[3]; return colorLevels[4]; }; const formatDate = (dateString: string): string => { const date = new Date(dateString); return date.toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric", weekday: "long", }); }; async function fetchGithubContributions( username?: string, githubKey?: string ): Promise { const token = githubKey; if (!token) { throw new Error("GitHub token is required"); } const query = ` query($username: String!) { user(login: $username) { contributionsCollection { contributionCalendar { totalContributions weeks { contributionDays { contributionCount date } } } } } } `; try { const response = await fetch("https://api.github.com/graphql", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ query, variables: { username }, }), }); if (!response.ok) { const errorData = await response.json(); console.error("GitHub API Error:", errorData); throw new Error(errorData.message || "Failed to fetch GitHub data"); } const data = await response.json(); if (data.errors) { console.error("GitHub API Error:", data.errors); throw new Error(data.errors[0]?.message || "GitHub API Error"); } const calendar = data.data?.user?.contributionsCollection?.contributionCalendar; if (!calendar) { throw new Error("No contribution data found"); } const contributions: ContributionDay[] = []; calendar.weeks.forEach((week: any) => { week.contributionDays.forEach((day: any) => { contributions.push({ date: day.date, count: day.contributionCount, }); }); }); return contributions; } catch (error) { console.error("Error fetching GitHub contributions:", error); throw error; } } const GithubContributions: React.FC = ({ username, githubKey, className, year = new Date().getFullYear(), }) => { const [weeks, setWeeks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function loadContributions() { try { setLoading(true); const contributions = await fetchGithubContributions( username, githubKey ); const yearContributions = contributions.filter((day) => { const date = new Date(day.date); return date.getFullYear() === year; }); const groupedWeeks: ContributionDay[][] = []; let currentWeek: ContributionDay[] = []; yearContributions.forEach((day, index) => { currentWeek.push(day); if ( currentWeek.length === 7 || index === yearContributions.length - 1 ) { // 如果是最后一周且不满7天,用空数据填充 if (currentWeek.length < 7) { const emptyDays = 7 - currentWeek.length; for (let i = 0; i < emptyDays; i++) { currentWeek.push({ date: "", count: 0, }); } } groupedWeeks.push([...currentWeek]); currentWeek = []; } }); setWeeks(groupedWeeks); setError(null); } catch (err) { setError("Failed to load GitHub contributions"); } finally { setLoading(false); } } if (username) { loadContributions(); } }, [githubKey, username, year]); if (loading) { return
; } if (error) { return
{error}
; } const goGithub = () => { window.open(`https://github.com/${username}`, "_blank"); }; return (
{weeks.map((week, weekIndex) => (
{week.map((day, dayIndex) => (
{day.date && (
{formatDate(day.date)}: {day.count} contributions
)}
))}
))}
); }; export default GithubContributions;