UNPKG

@msh-01/react-github-activity

Version:

A beautifully designed, highly customizable React component for displaying GitHub contribution graphs with TypeScript support

438 lines (433 loc) 16.1 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { GitHubContributions: () => GitHubContributions, cn: () => cn, formatDate: () => formatDate, getDateMonthsAgo: () => getDateMonthsAgo, getYearBounds: () => getYearBounds, isValidGitHubToken: () => isValidGitHubToken, isValidGitHubUsername: () => isValidGitHubUsername }); module.exports = __toCommonJS(index_exports); // src/GitHubContributions.tsx var import_react = require("react"); // src/utils.ts var import_clsx = require("clsx"); function cn(...inputs) { return (0, import_clsx.clsx)(inputs); } function formatDate(date) { const d = new Date(date); return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }); } function getDateMonthsAgo(months) { const date = /* @__PURE__ */ new Date(); date.setMonth(date.getMonth() - months); return date; } function getYearBounds(year) { return { start: new Date(year, 0, 1), end: new Date(year, 11, 31) }; } function isValidGitHubUsername(username) { const githubUsernameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/; return githubUsernameRegex.test(username); } function isValidGitHubToken(token) { const githubTokenRegex = /^gh[pousr]_[A-Za-z0-9_]{36,}$/; return githubTokenRegex.test(token); } // src/GitHubContributions.tsx var import_jsx_runtime = require("react/jsx-runtime"); var GITHUB_GRAPHQL_API = "https://api.github.com/graphql"; var CONTRIBUTION_LEVELS = { NONE: "bg-black/5 dark:bg-white/10", FIRST_QUARTILE: "bg-green-300 dark:bg-green-900", SECOND_QUARTILE: "bg-green-400 dark:bg-green-700", THIRD_QUARTILE: "bg-green-600 dark:bg-green-500", FOURTH_QUARTILE: "bg-green-700 dark:bg-green-300" }; var MONTHS = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; var MonthLabels = ({ columns }) => { const monthLabels = []; let currentMonth = -1; columns.forEach((column, columnIndex) => { if (column.length > 0) { const firstDay = new Date(column[0].date); const month = firstDay.getMonth(); if (month !== currentMonth) { monthLabels.push({ month: MONTHS[month], offset: columnIndex * 11 // 11px per column (10px + 1px gap) }); currentMonth = month; } } }); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex mb-2 text-xs text-gray-600 dark:text-gray-400", children: monthLabels.map((label, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: "absolute", style: { left: `${label.offset}px` }, children: label.month }, `${label.month}-${index}` )) }); }; var ContributionGrid = ({ columns }) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex gap-0.5", children: columns.map((column, columnIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-col gap-0.5", children: column.map((day, dayIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: cn( "w-[10px] h-[10px] rounded-[3px]", CONTRIBUTION_LEVELS[day.contributionLevel] ), title: `${day.contributionCount} contributions on ${new Date( day.date ).toLocaleDateString()}` }, `${columnIndex}-${dayIndex}` )) }, columnIndex)) }); }; var ContributionLegend = ({ stats, months, currentYear }) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between mt-4 text-xs text-gray-600 dark:text-gray-400", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ stats?.totalContributions, " contributions in", " ", months ? `last ${months} months` : currentYear ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-1", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs", children: "Less" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex gap-0.5", children: Object.entries(CONTRIBUTION_LEVELS).map( ([level, className]) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: cn( "w-[10px] h-[10px] rounded-[3px]", className ) }, level ) ) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs", children: "More" }) ] }) ] }); }; var ContributionStatsGrid = ({ stats }) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4 mt-6", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-xs uppercase text-gray-600 dark:text-gray-400", children: "Total Contributions" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-2xl font-bold", children: stats.totalContributions }) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-xs uppercase text-gray-600 dark:text-gray-400", children: "Daily Average" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-2xl font-bold", children: stats.avgContributionsPerDay }) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-xs uppercase text-gray-600 dark:text-gray-400", children: "Longest Streak" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-2xl font-bold", children: [ stats.longestStreak, " days" ] }) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-xs uppercase text-gray-600 dark:text-gray-400", children: "Current Streak" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-2xl font-bold", children: [ stats.currentStreak, " days" ] }) ] }) ] }); }; var GitHubContributions = ({ username, token, showStats = false, year, months, showLabels = true, daysPerColumn = 7, className }) => { const [data, setData] = (0, import_react.useState)(null); const [loading, setLoading] = (0, import_react.useState)(true); const [, setError] = (0, import_react.useState)(null); const currentYear = year || (/* @__PURE__ */ new Date()).getFullYear(); const generateEmptyContributionData = (year2, monthsBack) => { let startDate; let endDate; if (monthsBack) { endDate = /* @__PURE__ */ new Date(); startDate = new Date(endDate); startDate.setMonth(endDate.getMonth() - monthsBack); } else { startDate = new Date(year2, 0, 1); endDate = new Date(year2, 11, 31); } const weeks = []; const currentDate = new Date(startDate); const startSunday = new Date(currentDate); startSunday.setDate(currentDate.getDate() - currentDate.getDay()); let weekDays = []; const iterDate = new Date(startSunday); while (iterDate <= endDate) { weekDays.push({ date: iterDate.toISOString().split("T")[0], contributionCount: 0, contributionLevel: "NONE" }); if (weekDays.length === 7) { weeks.push({ contributionDays: [...weekDays] }); weekDays = []; } iterDate.setDate(iterDate.getDate() + 1); } if (weekDays.length > 0) { weeks.push({ contributionDays: weekDays }); } return { totalContributions: 0, weeks, firstContribution: null, lastContribution: null }; }; (0, import_react.useEffect)(() => { const fetchContributions = async () => { try { setLoading(true); setError(null); let fromDate; let toDate; if (months) { const today = /* @__PURE__ */ new Date(); const startDate = new Date(today); startDate.setMonth(today.getMonth() - months); fromDate = startDate.toISOString(); toDate = today.toISOString(); } else { fromDate = `${currentYear}-01-01T00:00:00Z`; toDate = `${currentYear}-12-31T23:59:59Z`; } const query = ` query($username: String!, $from: DateTime!, $to: DateTime!) { user(login: $username) { contributionsCollection(from: $from, to: $to) { contributionCalendar { totalContributions weeks { contributionDays { date contributionCount contributionLevel } } } } } } `; const headers = { "Content-Type": "application/json", Authorization: `token ${token}` }; console.log("GitHub API Request:", { url: GITHUB_GRAPHQL_API, tokenPreview: `${token.substring(0, 8)}...`, username, dateRange: months ? `${months} months` : `year ${currentYear}`, fromDate, toDate }); const response = await fetch(GITHUB_GRAPHQL_API, { method: "POST", headers, body: JSON.stringify({ query, variables: { username, from: fromDate, to: toDate } }) }); if (!response.ok) { const errorText = await response.text(); console.error("GitHub API Error:", { status: response.status, statusText: response.statusText, body: errorText, headers: Object.fromEntries(response.headers.entries()) }); if (response.status === 403 && errorText.includes("rate limit exceeded")) { throw new Error( "GitHub API rate limit exceeded. Please check your token or try again later." ); } throw new Error( `GitHub API error (${response.status}): ${response.statusText}. ${errorText}` ); } const result = await response.json(); if (result.errors) { console.error("GraphQL errors:", result.errors); throw new Error(`GraphQL error: ${result.errors[0].message}`); } const contributionData = result.data.user.contributionsCollection.contributionCalendar; const allDays = contributionData.weeks.flatMap( (week) => week.contributionDays ); const daysWithContributions = allDays.filter( (day) => day.contributionCount > 0 ); const firstContribution = daysWithContributions.length > 0 ? daysWithContributions[0].date : null; const lastContribution = daysWithContributions.length > 0 ? daysWithContributions[daysWithContributions.length - 1].date : null; setData({ totalContributions: contributionData.totalContributions, weeks: contributionData.weeks, firstContribution, lastContribution }); } catch (err) { const errorMessage = err instanceof Error ? err.message : "An error occurred"; console.error("GitHub Contributions Error:", errorMessage); setError(errorMessage); const emptyData = generateEmptyContributionData(currentYear, months); setData(emptyData); } finally { setLoading(false); } }; fetchContributions(); }, [username, token, currentYear, months]); const calculateStats = () => { if (!data) return null; const allDays = data.weeks.flatMap((week) => week.contributionDays); const daysWithContributions = allDays.filter( (day) => day.contributionCount > 0 ); const totalDays = allDays.length; const avgContributionsPerDay = data.totalContributions / totalDays; const longestStreak = calculateLongestStreak(allDays); const currentStreak = calculateCurrentStreak(allDays); return { totalContributions: data.totalContributions, avgContributionsPerDay: avgContributionsPerDay.toFixed(2), totalActiveDays: daysWithContributions.length, longestStreak, currentStreak }; }; const calculateLongestStreak = (days) => { let maxStreak = 0; let currentStreak = 0; for (const day of days) { if (day.contributionCount > 0) { currentStreak++; maxStreak = Math.max(maxStreak, currentStreak); } else { currentStreak = 0; } } return maxStreak; }; const calculateCurrentStreak = (days) => { let streak = 0; const today = /* @__PURE__ */ new Date(); const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); for (let i = days.length - 1; i >= 0; i--) { const dayDate = new Date(days[i].date); if (days[i].contributionCount > 0) { streak++; } else if (dayDate < yesterday) { break; } } return streak; }; const regroupDaysByColumns = (weeks) => { const allDays = weeks.flatMap((week) => week.contributionDays); const columns2 = []; for (let i = 0; i < allDays.length; i += daysPerColumn) { columns2.push(allDays.slice(i, i + daysPerColumn)); } return columns2; }; if (loading) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("flex items-center justify-center p-8", className), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-white" }) }); } if (!data) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("p-4 text-gray-500 dark:text-gray-400", className), children: "No contribution data found." }); } const stats = calculateStats(); const columns = regroupDaysByColumns(data.weeks); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("space-y-4", className), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative", children: [ showLabels && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative mb-8", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MonthLabels, { columns }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ContributionGrid, { columns }) }), showLabels && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( ContributionLegend, { stats: stats || void 0, months, currentYear } ), showStats && stats && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ContributionStatsGrid, { stats }) ] }) }); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { GitHubContributions, cn, formatDate, getDateMonthsAgo, getYearBounds, isValidGitHubToken, isValidGitHubUsername });