mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-05 00:32:03 +00:00
Full year for commit chart, improved commit resolution, capturing about 99% now
This commit is contained in:
parent
1fc4925d88
commit
8dc95e92bb
@ -1,11 +1,25 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useFetchGithubCommits } from '@/hooks/githubQueries/useFetchGithubCommits';
|
||||
import { Tooltip } from 'primereact/tooltip';
|
||||
|
||||
const GithubContributionChart = ({ username }) => {
|
||||
const [contributionData, setContributionData] = useState({});
|
||||
const [totalCommits, setTotalCommits] = useState(0);
|
||||
|
||||
const { data: commits, isLoading, isFetching } = useFetchGithubCommits(username);
|
||||
const handleNewCommit = useCallback(({ contributionData, totalCommits }) => {
|
||||
setContributionData(contributionData);
|
||||
setTotalCommits(totalCommits);
|
||||
}, []);
|
||||
|
||||
const { data, isLoading, isFetching } = useFetchGithubCommits(username, handleNewCommit);
|
||||
|
||||
// Initialize from cached data if available
|
||||
useEffect(() => {
|
||||
if (data && !isLoading) {
|
||||
setContributionData(data.contributionData);
|
||||
setTotalCommits(data.totalCommits);
|
||||
}
|
||||
}, [data, isLoading]);
|
||||
|
||||
const getColor = useCallback((count) => {
|
||||
if (count === 0) return 'bg-gray-100';
|
||||
@ -17,65 +31,110 @@ const GithubContributionChart = ({ username }) => {
|
||||
|
||||
const generateCalendar = useCallback(() => {
|
||||
const today = new Date();
|
||||
const sixMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 5, 1);
|
||||
const oneYearAgo = new Date(today.getFullYear() - 1, today.getMonth(), today.getDate());
|
||||
const calendar = [];
|
||||
|
||||
// Create 7 rows for days of the week
|
||||
for (let i = 0; i < 7; i++) {
|
||||
calendar[i] = [];
|
||||
}
|
||||
|
||||
for (let d = new Date(sixMonthsAgo); d <= today; d.setDate(d.getDate() + 1)) {
|
||||
// Fill in the dates
|
||||
for (let d = new Date(oneYearAgo); d <= today; d.setDate(d.getDate() + 1)) {
|
||||
const dateString = d.toISOString().split('T')[0];
|
||||
const count = contributionData[dateString] || 0;
|
||||
calendar.push({ date: new Date(d), count });
|
||||
const dayOfWeek = d.getDay();
|
||||
calendar[dayOfWeek].push({ date: new Date(d), count });
|
||||
}
|
||||
|
||||
return calendar;
|
||||
}, [contributionData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (commits) {
|
||||
let commitCount = 0;
|
||||
|
||||
const newContributionData = {};
|
||||
commits.forEach(commit => {
|
||||
const date = commit.commit.author.date.split('T')[0];
|
||||
newContributionData[date] = (newContributionData[date] || 0) + 1;
|
||||
commitCount++;
|
||||
});
|
||||
|
||||
setContributionData(newContributionData);
|
||||
setTotalCommits(commitCount);
|
||||
|
||||
console.log(`Total commits fetched: ${commitCount}`);
|
||||
}
|
||||
}, [commits]);
|
||||
|
||||
const calendar = generateCalendar();
|
||||
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
|
||||
const getMonthLabels = useCallback(() => {
|
||||
const today = new Date();
|
||||
const oneYearAgo = new Date(today.getFullYear() - 1, today.getMonth(), today.getDate());
|
||||
const months = [];
|
||||
let currentMonth = -1;
|
||||
|
||||
for (let d = new Date(oneYearAgo); d <= today; d.setDate(d.getDate() + 1)) {
|
||||
const month = d.getMonth();
|
||||
if (month !== currentMonth) {
|
||||
months.push({
|
||||
name: d.toLocaleString('default', { month: 'short' }),
|
||||
index: calendar[0].findIndex(
|
||||
(_, weekIndex) => calendar[0][weekIndex]?.date.getMonth() === month
|
||||
)
|
||||
});
|
||||
currentMonth = month;
|
||||
}
|
||||
}
|
||||
return months;
|
||||
}, [calendar]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto py-2 px-4 max-w-[900px] bg-gray-800 rounded-lg">
|
||||
<div className="mx-auto py-4 px-8 max-w-[1000px] bg-gray-800 rounded-lg">
|
||||
{(isLoading || isFetching) && <p>Loading contribution data... ({totalCommits} commits fetched)</p>}
|
||||
{!isLoading && !isFetching &&
|
||||
<div className="flex justify-between items-center pr-1">
|
||||
<p className="mb-2">Total commits: {totalCommits}</p>
|
||||
<i className="pi pi-question-circle cursor-pointer" data-pr-tooltip="Total number of commits made to GitHub repositories over the last 6 months. (may not be 100% accurate)" />
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-base font-semibold text-gray-200">
|
||||
{totalCommits} contributions in the last year
|
||||
</h3>
|
||||
<i className="pi pi-question-circle text-lg cursor-pointer text-gray-400 hover:text-gray-200"
|
||||
data-pr-tooltip="Total number of commits made to GitHub repositories over the last year. (may not be 100% accurate)" />
|
||||
<Tooltip target=".pi-question-circle" position="top" />
|
||||
</div>
|
||||
}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{calendar.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-3 h-3 ${getColor(day.count)} rounded-sm cursor-pointer transition-all duration-200 ease-in-out hover:transform hover:scale-150`}
|
||||
title={`${day.date.toDateString()}: ${day.count} contribution${day.count !== 1 ? 's' : ''}`}
|
||||
></div>
|
||||
))}
|
||||
<div className="flex">
|
||||
{/* Days of week labels */}
|
||||
<div className="flex flex-col gap-[3px] text-[11px] text-gray-400 pr-3">
|
||||
{weekDays.map((day, index) => (
|
||||
<div key={day} className="h-[12px] leading-[12px]">
|
||||
{index % 2 === 0 && day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{/* Calendar grid */}
|
||||
<div className="flex gap-[3px]">
|
||||
{calendar[0].map((_, weekIndex) => (
|
||||
<div key={weekIndex} className="flex flex-col gap-[3px]">
|
||||
{calendar.map((row, dayIndex) => (
|
||||
row[weekIndex] && (
|
||||
<div
|
||||
key={`${weekIndex}-${dayIndex}`}
|
||||
className={`w-[12px] h-[12px] ${getColor(row[weekIndex].count)} rounded-[2px] cursor-pointer transition-colors duration-100`}
|
||||
title={`${row[weekIndex].date.toDateString()}: ${row[weekIndex].count} contribution${row[weekIndex].count !== 1 ? 's' : ''}`}
|
||||
></div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Month labels moved to bottom */}
|
||||
<div className="flex text-[11px] text-gray-400 h-[20px] mt-1">
|
||||
{getMonthLabels().map((month, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute"
|
||||
style={{ marginLeft: `${month.index * 15}px` }}
|
||||
>
|
||||
{month.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-400 flex items-center">
|
||||
<div className="text-[11px] text-gray-400 flex items-center justify-end">
|
||||
<span className="mr-2">Less</span>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-3 h-3 bg-gray-100 rounded-sm"></div>
|
||||
<div className="w-3 h-3 bg-green-300 rounded-sm"></div>
|
||||
<div className="w-3 h-3 bg-green-400 rounded-sm"></div>
|
||||
<div className="w-3 h-3 bg-green-600 rounded-sm"></div>
|
||||
<div className="w-3 h-3 bg-green-700 rounded-sm"></div>
|
||||
<div className="flex gap-[3px]">
|
||||
<div className="w-[12px] h-[12px] bg-gray-100 rounded-[2px]"></div>
|
||||
<div className="w-[12px] h-[12px] bg-green-300 rounded-[2px]"></div>
|
||||
<div className="w-[12px] h-[12px] bg-green-400 rounded-[2px]"></div>
|
||||
<div className="w-[12px] h-[12px] bg-green-600 rounded-[2px]"></div>
|
||||
<div className="w-[12px] h-[12px] bg-green-700 rounded-[2px]"></div>
|
||||
</div>
|
||||
<span className="ml-2">More</span>
|
||||
</div>
|
||||
|
@ -1,22 +1,38 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getAllCommits } from '@/lib/github';
|
||||
|
||||
export function useFetchGithubCommits(username) {
|
||||
const fetchCommits = async () => {
|
||||
const sixMonthsAgo = new Date();
|
||||
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
|
||||
|
||||
const commits = [];
|
||||
for await (const commit of getAllCommits(username, sixMonthsAgo)) {
|
||||
commits.push(commit);
|
||||
}
|
||||
return commits;
|
||||
};
|
||||
|
||||
export function useFetchGithubCommits(username, onCommitReceived) {
|
||||
return useQuery({
|
||||
queryKey: ['githubCommits', username],
|
||||
queryFn: fetchCommits,
|
||||
queryFn: async () => {
|
||||
const oneYearAgo = new Date();
|
||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||
|
||||
const commits = [];
|
||||
const contributionData = {};
|
||||
let totalCommits = 0;
|
||||
|
||||
for await (const commit of getAllCommits(username, oneYearAgo)) {
|
||||
commits.push(commit);
|
||||
const date = commit.commit.author.date.split('T')[0];
|
||||
contributionData[date] = (contributionData[date] || 0) + 1;
|
||||
totalCommits++;
|
||||
|
||||
// Call the callback with the running totals
|
||||
onCommitReceived?.({
|
||||
contributionData,
|
||||
totalCommits
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
commits,
|
||||
contributionData,
|
||||
totalCommits
|
||||
};
|
||||
},
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
cacheTime: 1000 * 60 * 60, // 1 hour
|
||||
});
|
||||
}
|
@ -21,44 +21,45 @@ const octokit = new ThrottledOctokit({
|
||||
});
|
||||
|
||||
export async function* getAllCommits(username, since) {
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const { data: repos } = await octokit.repos.listForUser({
|
||||
username,
|
||||
per_page: 100,
|
||||
page,
|
||||
});
|
||||
|
||||
if (repos.length === 0) break;
|
||||
|
||||
const repoPromises = repos.map(repo =>
|
||||
octokit.repos.listCommits({
|
||||
owner: username,
|
||||
repo: repo.name,
|
||||
since: since.toISOString(),
|
||||
per_page: 100,
|
||||
})
|
||||
);
|
||||
|
||||
const repoResults = await Promise.allSettled(repoPromises);
|
||||
|
||||
for (const result of repoResults) {
|
||||
if (result.status === 'fulfilled') {
|
||||
for (const commit of result.value.data) {
|
||||
yield commit;
|
||||
}
|
||||
} else {
|
||||
console.warn(`Error fetching commits: ${result.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
page++;
|
||||
} catch (error) {
|
||||
console.error("Error fetching repositories:", error.message);
|
||||
break;
|
||||
// Create time windows of 1 month each
|
||||
const endDate = new Date();
|
||||
let currentDate = new Date(since);
|
||||
|
||||
while (currentDate < endDate) {
|
||||
let nextDate = new Date(currentDate);
|
||||
nextDate.setMonth(nextDate.getMonth() + 1);
|
||||
|
||||
// If next date would be in the future, use current date instead
|
||||
if (nextDate > endDate) {
|
||||
nextDate = endDate;
|
||||
}
|
||||
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const { data } = await octokit.search.commits({
|
||||
q: `author:${username} committer-date:${currentDate.toISOString().split('T')[0]}..${nextDate.toISOString().split('T')[0]}`,
|
||||
per_page: 100,
|
||||
page,
|
||||
});
|
||||
|
||||
if (data.items.length === 0) break;
|
||||
|
||||
for (const commit of data.items) {
|
||||
yield commit;
|
||||
}
|
||||
|
||||
if (data.items.length < 100) break;
|
||||
page++;
|
||||
} catch (error) {
|
||||
console.error("Error fetching commits:", error.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next time window
|
||||
currentDate = nextDate;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user