From 332d4e824819787f0bc83f2fa1bc10ea5014a84c Mon Sep 17 00:00:00 2001 From: ashish-choudhari-git Date: Mon, 28 Jul 2025 20:56:24 +0530 Subject: [PATCH 1/4] Moving data fetching to backend --- backend/middlewares/authenticateGitHub.js | 15 ++ backend/routes/details.js | 271 ++++++++++++++++++++++ backend/server.js | 2 + package.json | 4 +- src/Routes/Router.tsx | 2 + src/hooks/useGitHubData.ts | 124 +++++----- src/pages/Home/Home.tsx | 157 +++++++++++-- src/pages/UserAnalytics/UserAnalytics.tsx | 7 + 8 files changed, 493 insertions(+), 89 deletions(-) create mode 100644 backend/middlewares/authenticateGitHub.js create mode 100644 backend/routes/details.js create mode 100644 src/pages/UserAnalytics/UserAnalytics.tsx diff --git a/backend/middlewares/authenticateGitHub.js b/backend/middlewares/authenticateGitHub.js new file mode 100644 index 0000000..86287a7 --- /dev/null +++ b/backend/middlewares/authenticateGitHub.js @@ -0,0 +1,15 @@ +const {Octokit} = require("@octokit/rest"); +//ashish-choudhari-git Code +const authenticateGitHub = (req,res,next)=>{ + const {username,token} = req.body; + + if(!username || !token) { + return res.status(400).json({ message : 'Username and token are required'}); + } + + req.octokit = new Octokit({auth:token}); + req.githubUsername = username; + next(); +} + +module.exports = authenticateGitHub; \ No newline at end of file diff --git a/backend/routes/details.js b/backend/routes/details.js new file mode 100644 index 0000000..ab3deb7 --- /dev/null +++ b/backend/routes/details.js @@ -0,0 +1,271 @@ +//Aashish Choudhari's Code | GSSoC Contributor +const express = require('express'); +const router = express.Router(); +const authenticateGitHub = require('../middlewares/authenticateGitHub'); + +router.post('/get-data', authenticateGitHub, async (req,res)=>{ + + try{ + const { octokit, githubUsername } = req; + + // Fetch user's issues and PRs specifically for Home page + const [issuesResponse, prsResponse] = await Promise.all([ + // Get issues created by user + octokit.rest.search.issuesAndPullRequests({ + q: `author:${githubUsername} type:issue`, + sort: 'created', + order: 'desc', + per_page: 100 + }), + // Get pull requests created by user + octokit.rest.search.issuesAndPullRequests({ + q: `author:${githubUsername} type:pr`, + sort: 'created', + order: 'desc', + per_page: 100 + }) + ]); + + // Process issues data + const issues = issuesResponse.data.items.map(issue => ({ + id: issue.id, + title: issue.title, + state: issue.state, + created_at: issue.created_at, + repository_url: issue.repository_url, + html_url: issue.html_url, + number: issue.number, + labels: issue.labels, + assignees: issue.assignees, + user: issue.user + })); + // Process pull requests data + const prs = prsResponse.data.items.map(pr => ({ + id: pr.id, + title: pr.title, + state: pr.state, + created_at: pr.created_at, + repository_url: pr.repository_url, + html_url: pr.html_url, + number: pr.number, + pull_request: { + merged_at: pr.pull_request?.merged_at || null + }, + labels: pr.labels, + assignees: pr.assignees, + user: pr.user + })); + + const responseData = { + issues, + prs, + totalIssues: issuesResponse.data.total_count, + totalPrs: prsResponse.data.total_count + }; + + res.json(responseData); + } catch (error) { + console.error('Error fetching user data:', error); + res.status(500).json({ message: 'Error fetching user data', error: error.message }); + } +}); + +// New route for comprehensive user analytics +router.post('/user-profile', authenticateGitHub, async (req, res) => { + try { + const { octokit, githubUsername } = req; + + // Fetch user profile + const userResponse = await octokit.rest.users.getByUsername({ + username: githubUsername + }); + + // Fetch user repositories + const reposResponse = await octokit.rest.repos.listForUser({ + username: githubUsername, + type: 'all', + sort: 'updated', + per_page: 100 + }); + + // Calculate language statistics + const languageStats = {}; + const repositories = []; + + for (const repo of reposResponse.data) { + try { + const languagesResponse = await octokit.rest.repos.listLanguages({ + owner: githubUsername, + repo: repo.name + }); + + Object.entries(languagesResponse.data).forEach(([lang, bytes]) => { + languageStats[lang] = (languageStats[lang] || 0) + bytes; + }); + + repositories.push({ + name: repo.name, + description: repo.description, + stars: repo.stargazers_count, + forks: repo.forks_count, + watchers: repo.watchers_count, + language: repo.language, + html_url: repo.html_url, + updated_at: repo.updated_at + }); + } catch (err) { + console.log(`Error fetching languages for ${repo.name}:`, err.message); + } + } + + // Sort repositories by stars + const repositoryRanking = repositories.sort((a, b) => b.stars - a.stars); + + // Calculate social stats + const socialStats = { + totalStars: repositories.reduce((sum, repo) => sum + repo.stars, 0), + totalForks: repositories.reduce((sum, repo) => sum + repo.forks, 0), + totalWatchers: repositories.reduce((sum, repo) => sum + repo.watchers, 0) + }; + + // Calculate real contribution stats + const currentDate = new Date(); + const oneYearAgo = new Date(currentDate.getFullYear() - 1, currentDate.getMonth(), currentDate.getDate()); + + // Get user events for contribution analysis + let userEvents = []; + try { + const eventsResponse = await octokit.rest.activity.listPublicEventsForUser({ + username: githubUsername, + per_page: 100 + }); + userEvents = eventsResponse.data; + } catch (err) { + console.log('Could not fetch user events:', err.message); + } + + // Calculate contributions from repositories and events + const totalContributions = userResponse.data.public_repos + + userResponse.data.public_gists + + (userEvents.length || 0); + + // Calculate streaks and activity patterns + const contributionsByDate = {}; + userEvents.forEach(event => { + const date = event.created_at.split('T')[0]; + contributionsByDate[date] = (contributionsByDate[date] || 0) + 1; + }); + + // Calculate longest and current streak + const dates = Object.keys(contributionsByDate).sort(); + let longestStreak = 0; + let currentStreak = 0; + let tempStreak = 0; + + for (let i = 0; i < dates.length; i++) { + if (i === 0 || isConsecutiveDay(dates[i-1], dates[i])) { + tempStreak++; + } else { + longestStreak = Math.max(longestStreak, tempStreak); + tempStreak = 1; + } + } + longestStreak = Math.max(longestStreak, tempStreak); + + // Current streak calculation + const today = new Date().toISOString().split('T')[0]; + if (dates.length > 0) { + const lastDate = dates[dates.length - 1]; + if (isRecentDate(lastDate, today)) { + currentStreak = calculateCurrentStreak(dates); + } + } + + // Most active day calculation + const dayActivityCount = { Sun: 0, Mon: 0, Tue: 0, Wed: 0, Thu: 0, Fri: 0, Sat: 0 }; + userEvents.forEach(event => { + const dayOfWeek = new Date(event.created_at).getDay(); + const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + dayActivityCount[dayNames[dayOfWeek]]++; + }); + + const mostActiveDay = Object.keys(dayActivityCount).reduce((a, b) => + dayActivityCount[a] > dayActivityCount[b] ? a : b + ); + + // Average contributions per day (last 365 days) + const averagePerDay = userEvents.length > 0 ? (userEvents.length / 365).toFixed(1) : 0; + + const contributionStats = { + totalContributions, + longestStreak, + currentStreak, + mostActiveDay: getDayFullName(mostActiveDay), + averagePerDay: parseFloat(averagePerDay) + }; + + // Helper functions + function isConsecutiveDay(date1, date2) { + const d1 = new Date(date1); + const d2 = new Date(date2); + const diffTime = Math.abs(d2 - d1); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays === 1; + } + + function isRecentDate(date, today) { + const diffTime = Math.abs(new Date(today) - new Date(date)); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays <= 1; + } + + function calculateCurrentStreak(dates) { + let streak = 0; + for (let i = dates.length - 1; i > 0; i--) { + if (isConsecutiveDay(dates[i-1], dates[i])) { + streak++; + } else { + break; + } + } + return streak + 1; + } + + function getDayFullName(shortDay) { + const dayMap = { + 'Sun': 'Sunday', + 'Mon': 'Monday', + 'Tue': 'Tuesday', + 'Wed': 'Wednesday', + 'Thu': 'Thursday', + 'Fri': 'Friday', + 'Sat': 'Saturday' + }; + return dayMap[shortDay] || 'Monday'; + } + + const responseData = { + profile: userResponse.data, + repositories, + languageStats, + contributionStats, + rankings: { + repositoryRanking + }, + highlights: { + topRepo: repositoryRanking[0] || null, + totalStars: socialStats.totalStars + }, + stars: repositories.filter(repo => repo.stars > 0), + commitHistory: [], // Would need more complex API calls + socialStats + }; + + res.json(responseData); + } catch (error) { + console.error('Error fetching user profile:', error); + res.status(500).json({ message: 'Error fetching user profile', error: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 3f19f00..59e7c8a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -23,6 +23,8 @@ app.use(session({ })); app.use(passport.initialize()); app.use(passport.session()); +const githubRoutes = require('./routes/details'); +app.use('/api/github', githubRoutes); // Routes const authRoutes = require('./routes/auth'); diff --git a/package.json b/package.json index 7b1e40b..377ab1f 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "@mui/icons-material": "^5.15.6", "@mui/material": "^5.15.6", "@vitejs/plugin-react": "^4.3.3", - "axios": "^1.7.7", - "lucide-react": "^0.525.0", + "axios": "^1.11.0", + "lucide-react": "^0.532.0", "octokit": "^4.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/Routes/Router.tsx b/src/Routes/Router.tsx index 900a915..8524e0f 100644 --- a/src/Routes/Router.tsx +++ b/src/Routes/Router.tsx @@ -6,6 +6,7 @@ import Contributors from "../pages/Contributors/Contributors" import Signup from "../pages/Signup/Signup.tsx" import Login from "../pages/Login/Login.tsx" import UserProfile from "../pages/UserProfile/UserProfile.tsx" +import UserAnalytics from "../pages/UserAnalytics/UserAnalytics.tsx"; @@ -16,6 +17,7 @@ const Router = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index 9add583..4577e6b 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -1,62 +1,62 @@ -import { useState, useCallback } from 'react'; - -export const useGitHubData = (octokit) => { - const [issues, setIssues] = useState([]); - const [prs, setPrs] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - - const fetchAll = async (url, params) => { - let page = 1; - let results = []; - let hasMore = true; - - while (hasMore) { - const response = await octokit.request(url, { ...params, page }); - results = results.concat(response.data.items); - hasMore = response.data.items.length === 100; - page++; - } - - return results; - }; - - const fetchData = useCallback(async (username) => { - if (!octokit || !username) return; - - setLoading(true); - setError(''); - - try { - const [issuesResponse, prsResponse] = await Promise.all([ - fetchAll('GET /search/issues', { - q: `author:${username} is:issue`, - sort: 'created', - order: 'desc', - per_page: 100, - }), - fetchAll('GET /search/issues', { - q: `author:${username} is:pr`, - sort: 'created', - order: 'desc', - per_page: 100, - }), - ]); - - setIssues(issuesResponse); - setPrs(prsResponse); - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } - }, [octokit]); - - return { - issues, - prs, - loading, - error, - fetchData, - }; -}; \ No newline at end of file +// import { useState, useCallback } from 'react'; + +// export const useGitHubData = (octokit) => { +// const [issues, setIssues] = useState([]); +// const [prs, setPrs] = useState([]); +// const [loading, setLoading] = useState(false); +// const [error, setError] = useState(''); + +// const fetchAll = async (url, params) => { +// let page = 1; +// let results = []; +// let hasMore = true; + +// while (hasMore) { +// const response = await octokit.request(url, { ...params, page }); +// results = results.concat(response.data.items); +// hasMore = response.data.items.length === 100; +// page++; +// } + +// return results; +// }; + +// const fetchData = useCallback(async (username) => { +// if (!octokit || !username) return; + +// setLoading(true); +// setError(''); + +// try { +// const [issuesResponse, prsResponse] = await Promise.all([ +// fetchAll('GET /search/issues', { +// q: `author:${username} is:issue`, +// sort: 'created', +// order: 'desc', +// per_page: 100, +// }), +// fetchAll('GET /search/issues', { +// q: `author:${username} is:pr`, +// sort: 'created', +// order: 'desc', +// per_page: 100, +// }), +// ]); + +// setIssues(issuesResponse); +// setPrs(prsResponse); +// } catch (err) { +// setError(err.message); +// } finally { +// setLoading(false); +// } +// }, [octokit]); + +// return { +// issues, +// prs, +// loading, +// error, +// fetchData, +// }; +// }; \ No newline at end of file diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 613a32e..da29f4e 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; import { Container, Box, @@ -23,11 +24,14 @@ import { InputLabel, } from "@mui/material"; import { useTheme } from "@mui/material/styles"; -import { useGitHubAuth } from "../../hooks/useGitHubAuth"; -import { useGitHubData } from "../../hooks/useGitHubData"; +// import { useGitHubAuth } from "../../hooks/useGitHubAuth"; +// import { useGitHubData } from "../../hooks/useGitHubData"; import { usePagination } from "../../hooks/usePagination"; +import { BarChart3 } from "lucide-react"; +import axios from 'axios'; const ROWS_PER_PAGE = 10; +const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000'; interface GitHubItem { id: number; @@ -40,23 +44,34 @@ interface GitHubItem { } const Home: React.FC = () => { + const navigate = useNavigate(); const theme = useTheme(); - const { - username, - setUsername, - token, - setToken, - error: authError, - getOctokit, - } = useGitHubAuth(); - const octokit = getOctokit(); - const { - issues, - prs, - loading, - error: dataError, - fetchData, - } = useGitHubData(octokit); + // const { + // username, + // setUsername, + // token, + // setToken, + // error: authError, + // getOctokit, + // } = useGitHubAuth(); + // const octokit = getOctokit(); + // const { + // issues, + // prs, + // loading, + // error: dataError, + // fetchData, + // } = useGitHubData(octokit); + +//state management + const [username, setUsername] = useState(''); + const [token, setToken] = useState(''); + const [issues, setIssues] = useState([]); + const [prs, setPrs] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const { page, itemsPerPage, handleChangePage, paginateData } = usePagination(ROWS_PER_PAGE); @@ -68,9 +83,76 @@ const Home: React.FC = () => { const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); - const handleSubmit = (e: React.FormEvent): void => { + //validation of username and token + const validateCredentials= async()=>{ + if(!username.trim()){ + setError('Username is required.') + return false; + } + if(!token.trim()){ + setError('Personal acess token is required.') + return false; + } + + setError(''); + return true; + + } + + // Navigate to analytics page with username and token + const handleViewAnalytics = () => { + if (!username.trim() || !token.trim()) { + setError('Please enter username and token first'); + return; + } + // Pass username and token as state to analytics page + navigate('/analytics', { + state: { + username: username.trim(), + token: token.trim() + } + }); + }; + + + + //fetching data from backend + const fetchData= async()=>{ + + setLoading(true); + setError(''); + + try{ + + console.log('Request payload:', { username, token: token ? 'PROVIDED' : 'MISSING' }); + + const response = await axios.post(`${backendUrl}/api/github/get-data`,{ + username, token + }); //we get data from backend by providng username and token + + setIssues(response.data.issues); + setPrs(response.data.prs); + + }catch(err:any){ + + console.error('Error response:', err.response?.data); + setError(err.response?.data?.message || `Error fetching GitHub data: ${err.message}`); + setIssues([]); + setPrs([]); + } + finally{ + setLoading(false); + } + } + + + const handleSubmit = async (e: React.FormEvent)=> { e.preventDefault(); - fetchData(username); + const isValid = await validateCredentials(); + if(isValid){ + fetchData(); + } + }; const formatDate = (dateString: string): string => @@ -149,9 +231,34 @@ const Home: React.FC = () => { required sx={{ flex: 1 }} /> - + + + + @@ -229,9 +336,9 @@ const Home: React.FC = () => { - {(authError || dataError) && ( + {(error) && ( - {authError || dataError} + {error} )} diff --git a/src/pages/UserAnalytics/UserAnalytics.tsx b/src/pages/UserAnalytics/UserAnalytics.tsx new file mode 100644 index 0000000..954a092 --- /dev/null +++ b/src/pages/UserAnalytics/UserAnalytics.tsx @@ -0,0 +1,7 @@ +export default function UserAnalytics() { + return ( +
+

Text Analytics coming soon

+
+ ); +} From 4e93202b3785f16a428b740a268dd9dac6a810d0 Mon Sep 17 00:00:00 2001 From: ashish-choudhari-git Date: Mon, 28 Jul 2025 21:36:36 +0530 Subject: [PATCH 2/4] Used octokit graphql for more accurate data fetching --- backend/routes/details.js | 311 ++++++++++++++---- backend/test-api.html | 0 package.json | 2 +- .../UserAnalyticsComp/UserStats.tsx | 0 src/pages/Home/Home.tsx | 8 +- .../UserAnalyticsComp/ContributionStats.tsx | 0 .../UserAnalyticsComp/LanguageStats.tsx | 0 .../UserAnalyticsComp/RepositoryTable.tsx | 0 .../components/UserAnalyticsComp/UserForm.tsx | 0 .../UserAnalyticsComp/UserProfile.tsx | 0 .../UserAnalyticsComp/UserStats.tsx | 0 11 files changed, 244 insertions(+), 77 deletions(-) create mode 100644 backend/test-api.html create mode 100644 src/components/UserAnalyticsComp/UserStats.tsx create mode 100644 src/pages/UserAnalytics/components/UserAnalyticsComp/ContributionStats.tsx create mode 100644 src/pages/UserAnalytics/components/UserAnalyticsComp/LanguageStats.tsx create mode 100644 src/pages/UserAnalytics/components/UserAnalyticsComp/RepositoryTable.tsx create mode 100644 src/pages/UserAnalytics/components/UserAnalyticsComp/UserForm.tsx create mode 100644 src/pages/UserAnalytics/components/UserAnalyticsComp/UserProfile.tsx create mode 100644 src/pages/UserAnalytics/components/UserAnalyticsComp/UserStats.tsx diff --git a/backend/routes/details.js b/backend/routes/details.js index ab3deb7..a96e89b 100644 --- a/backend/routes/details.js +++ b/backend/routes/details.js @@ -3,6 +3,58 @@ const express = require('express'); const router = express.Router(); const authenticateGitHub = require('../middlewares/authenticateGitHub'); +// Simple in-memory cache for language data +const languageCache = new Map(); +const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes + +// Helper function to create delay +const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + +// Helper function to limit concurrent API calls +const createLimiter = (limit) => { + let running = 0; + const queue = []; + + const run = async (fn) => { + return new Promise((resolve, reject) => { + queue.push({ fn, resolve, reject }); + process(); + }); + }; + + const process = async () => { + if (running >= limit || queue.length === 0) return; + + running++; + const { fn, resolve, reject } = queue.shift(); + + try { + const result = await fn(); + resolve(result); + } catch (error) { + reject(error); + } finally { + running--; + process(); + } + }; + + return run; +}; + +// Cache cleanup function to prevent memory leaks +const cleanupCache = () => { + const now = Date.now(); + for (const [key, value] of languageCache.entries()) { + if (now - value.timestamp > CACHE_DURATION) { + languageCache.delete(key); + } + } +}; + +// Run cache cleanup every 15 minutes +setInterval(cleanupCache, 15 * 60 * 1000); + router.post('/get-data', authenticateGitHub, async (req,res)=>{ try{ @@ -88,33 +140,80 @@ router.post('/user-profile', authenticateGitHub, async (req, res) => { per_page: 100 }); - // Calculate language statistics + // Calculate language statistics with concurrency control and caching const languageStats = {}; const repositories = []; - for (const repo of reposResponse.data) { + // Create limiter for 3 concurrent requests (GitHub recommends max 5) + const limit = createLimiter(3); + + // Helper function to get cached or fetch language data + const getLanguageData = async (repo) => { + const cacheKey = `${githubUsername}/${repo.name}`; + const cached = languageCache.get(cacheKey); + + // Check if cache is valid (not expired) + if (cached && (Date.now() - cached.timestamp) < CACHE_DURATION) { + return cached.data; + } + try { const languagesResponse = await octokit.rest.repos.listLanguages({ owner: githubUsername, repo: repo.name }); - Object.entries(languagesResponse.data).forEach(([lang, bytes]) => { - languageStats[lang] = (languageStats[lang] || 0) + bytes; - }); - - repositories.push({ - name: repo.name, - description: repo.description, - stars: repo.stargazers_count, - forks: repo.forks_count, - watchers: repo.watchers_count, - language: repo.language, - html_url: repo.html_url, - updated_at: repo.updated_at + // Cache the result + languageCache.set(cacheKey, { + data: languagesResponse.data, + timestamp: Date.now() }); + + return languagesResponse.data; } catch (err) { console.log(`Error fetching languages for ${repo.name}:`, err.message); + return {}; + } + }; + + // Process repositories in batches with concurrency control + const batchSize = 10; + const repos = reposResponse.data; + + for (let i = 0; i < repos.length; i += batchSize) { + const batch = repos.slice(i, i + batchSize); + + // Process batch with concurrency limit + const batchPromises = batch.map(repo => + limit(async () => { + const languages = await getLanguageData(repo); + + // Add languages to stats + Object.entries(languages).forEach(([lang, bytes]) => { + languageStats[lang] = (languageStats[lang] || 0) + bytes; + }); + + // Build repository object + return { + name: repo.name, + description: repo.description, + stars: repo.stargazers_count, + forks: repo.forks_count, + watchers: repo.watchers_count, + language: repo.language, + html_url: repo.html_url, + updated_at: repo.updated_at + }; + }) + ); + + // Wait for current batch to complete + const batchResults = await Promise.all(batchPromises); + repositories.push(...batchResults); + + // Add small delay between batches to be respectful to GitHub API + if (i + batchSize < repos.length) { + await delay(100); // 100ms delay between batches } } @@ -128,73 +227,143 @@ router.post('/user-profile', authenticateGitHub, async (req, res) => { totalWatchers: repositories.reduce((sum, repo) => sum + repo.watchers, 0) }; - // Calculate real contribution stats - const currentDate = new Date(); - const oneYearAgo = new Date(currentDate.getFullYear() - 1, currentDate.getMonth(), currentDate.getDate()); - - // Get user events for contribution analysis - let userEvents = []; + // Calculate real contribution stats using GitHub GraphQL API + let totalContributions = 0; + let longestStreak = 0; + let currentStreak = 0; + let contributionsByDate = {}; + let dayActivityCount = { Sun: 0, Mon: 0, Tue: 0, Wed: 0, Thu: 0, Fri: 0, Sat: 0 }; + try { - const eventsResponse = await octokit.rest.activity.listPublicEventsForUser({ - username: githubUsername, - per_page: 100 - }); - userEvents = eventsResponse.data; - } catch (err) { - console.log('Could not fetch user events:', err.message); - } + // Get current year and last year for comprehensive data + const currentYear = new Date().getFullYear(); + const lastYear = currentYear - 1; + + // GraphQL query to get contribution data for current and last year + const contributionQuery = ` + query($username: String!, $from: DateTime!, $to: DateTime!) { + user(login: $username) { + contributionsCollection(from: $from, to: $to) { + totalContributions + contributionCalendar { + totalContributions + weeks { + contributionDays { + date + contributionCount + weekday + } + } + } + } + } + } + `; - // Calculate contributions from repositories and events - const totalContributions = userResponse.data.public_repos + - userResponse.data.public_gists + - (userEvents.length || 0); + // Query for current year + const currentYearFrom = `${currentYear}-01-01T00:00:00Z`; + const currentYearTo = `${currentYear}-12-31T23:59:59Z`; + + // Query for last year + const lastYearFrom = `${lastYear}-01-01T00:00:00Z`; + const lastYearTo = `${lastYear}-12-31T23:59:59Z`; + + // Execute both queries with error handling + const [currentYearResponse, lastYearResponse] = await Promise.allSettled([ + octokit.graphql(contributionQuery, { + username: githubUsername, + from: currentYearFrom, + to: currentYearTo + }), + octokit.graphql(contributionQuery, { + username: githubUsername, + from: lastYearFrom, + to: lastYearTo + }) + ]); - // Calculate streaks and activity patterns - const contributionsByDate = {}; - userEvents.forEach(event => { - const date = event.created_at.split('T')[0]; - contributionsByDate[date] = (contributionsByDate[date] || 0) + 1; - }); + // Combine contribution data from both years with error handling + const allContributionDays = []; + + // Process last year data if successful + if (lastYearResponse.status === 'fulfilled' && + lastYearResponse.value?.user?.contributionsCollection?.contributionCalendar?.weeks) { + lastYearResponse.value.user.contributionsCollection.contributionCalendar.weeks.forEach(week => { + week.contributionDays.forEach(day => { + if (day.contributionCount > 0) { + allContributionDays.push(day); + contributionsByDate[day.date] = day.contributionCount; + + // Count day activity (0=Sunday, 1=Monday, etc.) + const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + dayActivityCount[dayNames[day.weekday]] += day.contributionCount; + } + }); + }); + } - // Calculate longest and current streak - const dates = Object.keys(contributionsByDate).sort(); - let longestStreak = 0; - let currentStreak = 0; - let tempStreak = 0; - - for (let i = 0; i < dates.length; i++) { - if (i === 0 || isConsecutiveDay(dates[i-1], dates[i])) { - tempStreak++; - } else { - longestStreak = Math.max(longestStreak, tempStreak); - tempStreak = 1; + // Process current year data if successful + if (currentYearResponse.status === 'fulfilled' && + currentYearResponse.value?.user?.contributionsCollection?.contributionCalendar?.weeks) { + currentYearResponse.value.user.contributionsCollection.contributionCalendar.weeks.forEach(week => { + week.contributionDays.forEach(day => { + if (day.contributionCount > 0) { + allContributionDays.push(day); + contributionsByDate[day.date] = day.contributionCount; + + // Count day activity + const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + dayActivityCount[dayNames[day.weekday]] += day.contributionCount; + } + }); + }); } - } - longestStreak = Math.max(longestStreak, tempStreak); - - // Current streak calculation - const today = new Date().toISOString().split('T')[0]; - if (dates.length > 0) { - const lastDate = dates[dates.length - 1]; - if (isRecentDate(lastDate, today)) { - currentStreak = calculateCurrentStreak(dates); + + // Calculate total contributions from successful responses + totalContributions = + (currentYearResponse.status === 'fulfilled' ? + (currentYearResponse.value?.user?.contributionsCollection?.totalContributions || 0) : 0) + + (lastYearResponse.status === 'fulfilled' ? + (lastYearResponse.value?.user?.contributionsCollection?.totalContributions || 0) : 0); + + // Calculate streaks using accurate contribution dates + const contributionDates = Object.keys(contributionsByDate).sort(); + + // Calculate longest streak + let tempStreak = 0; + for (let i = 0; i < contributionDates.length; i++) { + if (i === 0 || isConsecutiveDay(contributionDates[i-1], contributionDates[i])) { + tempStreak++; + } else { + longestStreak = Math.max(longestStreak, tempStreak); + tempStreak = 1; + } + } + longestStreak = Math.max(longestStreak, tempStreak); + + // Calculate current streak + const today = new Date().toISOString().split('T')[0]; + if (contributionDates.length > 0) { + const lastContributionDate = contributionDates[contributionDates.length - 1]; + if (isRecentDate(lastContributionDate, today)) { + currentStreak = calculateCurrentStreak(contributionDates); + } } + + } catch (err) { + console.log('Could not fetch contribution data from GraphQL:', err.message); + // Fallback to basic calculation if GraphQL fails + totalContributions = userResponse.data.public_repos + userResponse.data.public_gists; } // Most active day calculation - const dayActivityCount = { Sun: 0, Mon: 0, Tue: 0, Wed: 0, Thu: 0, Fri: 0, Sat: 0 }; - userEvents.forEach(event => { - const dayOfWeek = new Date(event.created_at).getDay(); - const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - dayActivityCount[dayNames[dayOfWeek]]++; - }); - const mostActiveDay = Object.keys(dayActivityCount).reduce((a, b) => dayActivityCount[a] > dayActivityCount[b] ? a : b ); - // Average contributions per day (last 365 days) - const averagePerDay = userEvents.length > 0 ? (userEvents.length / 365).toFixed(1) : 0; + // Average contributions per day (based on actual contribution data) + const daysWithContributions = Object.keys(contributionsByDate).length; + const averagePerDay = daysWithContributions > 0 ? (totalContributions / 365).toFixed(1) : 0; const contributionStats = { totalContributions, @@ -220,7 +389,7 @@ router.post('/user-profile', authenticateGitHub, async (req, res) => { } function calculateCurrentStreak(dates) { - let streak = 0; + let streak = 1; // Start with 1 if we have any contributions for (let i = dates.length - 1; i > 0; i--) { if (isConsecutiveDay(dates[i-1], dates[i])) { streak++; @@ -228,7 +397,7 @@ router.post('/user-profile', authenticateGitHub, async (req, res) => { break; } } - return streak + 1; + return streak; } function getDayFullName(shortDay) { diff --git a/backend/test-api.html b/backend/test-api.html new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 377ab1f..e33766a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@mui/material": "^5.15.6", "@vitejs/plugin-react": "^4.3.3", "axios": "^1.11.0", - "lucide-react": "^0.532.0", + "lucide-react": "^0.535.0", "octokit": "^4.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/components/UserAnalyticsComp/UserStats.tsx b/src/components/UserAnalyticsComp/UserStats.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index da29f4e..1351036 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -124,8 +124,6 @@ const Home: React.FC = () => { try{ - console.log('Request payload:', { username, token: token ? 'PROVIDED' : 'MISSING' }); - const response = await axios.post(`${backendUrl}/api/github/get-data`,{ username, token }); //we get data from backend by providng username and token @@ -135,8 +133,8 @@ const Home: React.FC = () => { }catch(err:any){ - console.error('Error response:', err.response?.data); - setError(err.response?.data?.message || `Error fetching GitHub data: ${err.message}`); + console.error('Error response:', err.response?.status, err.response?.statusText); + setError(err.response?.data?.message || 'Failed to fetch GitHub data. Please check your credentials.'); setIssues([]); setPrs([]); } @@ -144,7 +142,7 @@ const Home: React.FC = () => { setLoading(false); } } - + const handleSubmit = async (e: React.FormEvent)=> { e.preventDefault(); diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/ContributionStats.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/ContributionStats.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/LanguageStats.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/LanguageStats.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/RepositoryTable.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/RepositoryTable.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/UserForm.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/UserForm.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/UserProfile.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/UserProfile.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/UserStats.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/UserStats.tsx new file mode 100644 index 0000000..e69de29 From 0c12ee25cded6ba33d3a1229a4948e5c9a61ab10 Mon Sep 17 00:00:00 2001 From: ashish-choudhari-git Date: Mon, 28 Jul 2025 21:53:52 +0530 Subject: [PATCH 3/4] added p-limit npm for rate limiting --- backend/package.json | 1 + backend/routes/details.js | 36 +++--------------------------------- package.json | 2 +- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/backend/package.json b/backend/package.json index 53bd64b..4e671e5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "express": "^4.21.1", "express-session": "^1.18.1", "mongoose": "^8.8.2", + "p-limit": "^6.2.0", "passport": "^0.7.0", "passport-local": "^1.0.0" }, diff --git a/backend/routes/details.js b/backend/routes/details.js index a96e89b..8edba38 100644 --- a/backend/routes/details.js +++ b/backend/routes/details.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); const authenticateGitHub = require('../middlewares/authenticateGitHub'); +const pLimit = require('p-limit'); // Simple in-memory cache for language data const languageCache = new Map(); @@ -10,37 +11,6 @@ const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes // Helper function to create delay const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); -// Helper function to limit concurrent API calls -const createLimiter = (limit) => { - let running = 0; - const queue = []; - - const run = async (fn) => { - return new Promise((resolve, reject) => { - queue.push({ fn, resolve, reject }); - process(); - }); - }; - - const process = async () => { - if (running >= limit || queue.length === 0) return; - - running++; - const { fn, resolve, reject } = queue.shift(); - - try { - const result = await fn(); - resolve(result); - } catch (error) { - reject(error); - } finally { - running--; - process(); - } - }; - - return run; -}; // Cache cleanup function to prevent memory leaks const cleanupCache = () => { @@ -144,8 +114,8 @@ router.post('/user-profile', authenticateGitHub, async (req, res) => { const languageStats = {}; const repositories = []; - // Create limiter for 3 concurrent requests (GitHub recommends max 5) - const limit = createLimiter(3); + // Create limiter for 3 concurrent requests using p-limit + const limit = pLimit(3); // Helper function to get cached or fetch language data const getLanguageData = async (repo) => { diff --git a/package.json b/package.json index e33766a..377ab1f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@mui/material": "^5.15.6", "@vitejs/plugin-react": "^4.3.3", "axios": "^1.11.0", - "lucide-react": "^0.535.0", + "lucide-react": "^0.532.0", "octokit": "^4.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", From f6ab14c75e4c43eea096eaa9a2da8b8f1632b41f Mon Sep 17 00:00:00 2001 From: ashish-choudhari-git Date: Tue, 29 Jul 2025 07:52:50 +0530 Subject: [PATCH 4/4] optimized date calculations --- backend/package.json | 1 + backend/routes/details.js | 87 ++++++++++++++++++++++++++------------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/backend/package.json b/backend/package.json index 4e671e5..2a7650e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "bcryptjs": "^2.4.3", "body-parser": "^1.20.3", "cors": "^2.8.5", + "date-fns": "^4.1.0", "dotenv": "^16.4.5", "express": "^4.21.1", "express-session": "^1.18.1", diff --git a/backend/routes/details.js b/backend/routes/details.js index 8edba38..b47aab2 100644 --- a/backend/routes/details.js +++ b/backend/routes/details.js @@ -3,6 +3,7 @@ const express = require('express'); const router = express.Router(); const authenticateGitHub = require('../middlewares/authenticateGitHub'); const pLimit = require('p-limit'); +const { parseISO, differenceInDays, isYesterday, isToday, format } = require('date-fns'); // Simple in-memory cache for language data const languageCache = new Map(); @@ -296,23 +297,27 @@ router.post('/user-profile', authenticateGitHub, async (req, res) => { (lastYearResponse.status === 'fulfilled' ? (lastYearResponse.value?.user?.contributionsCollection?.totalContributions || 0) : 0); - // Calculate streaks using accurate contribution dates + // Calculate streaks using accurate contribution dates with improved logic const contributionDates = Object.keys(contributionsByDate).sort(); - // Calculate longest streak + // Calculate longest streak with proper error handling let tempStreak = 0; - for (let i = 0; i < contributionDates.length; i++) { - if (i === 0 || isConsecutiveDay(contributionDates[i-1], contributionDates[i])) { - tempStreak++; - } else { - longestStreak = Math.max(longestStreak, tempStreak); - tempStreak = 1; + if (contributionDates.length > 0) { + tempStreak = 1; // Start with 1 for first date + + for (let i = 1; i < contributionDates.length; i++) { + if (isConsecutiveDay(contributionDates[i-1], contributionDates[i])) { + tempStreak++; + } else { + longestStreak = Math.max(longestStreak, tempStreak); + tempStreak = 1; // Reset streak + } } + longestStreak = Math.max(longestStreak, tempStreak); // Check final streak } - longestStreak = Math.max(longestStreak, tempStreak); - // Calculate current streak - const today = new Date().toISOString().split('T')[0]; + // Calculate current streak with proper date handling + const today = format(new Date(), 'yyyy-MM-dd'); // Use date-fns for consistent formatting if (contributionDates.length > 0) { const lastContributionDate = contributionDates[contributionDates.length - 1]; if (isRecentDate(lastContributionDate, today)) { @@ -333,7 +338,7 @@ router.post('/user-profile', authenticateGitHub, async (req, res) => { // Average contributions per day (based on actual contribution data) const daysWithContributions = Object.keys(contributionsByDate).length; - const averagePerDay = daysWithContributions > 0 ? (totalContributions / 365).toFixed(1) : 0; + const averagePerDay = daysWithContributions > 0 ? (totalContributions / daysWithContributions).toFixed(1) : 0; const contributionStats = { totalContributions, @@ -343,31 +348,57 @@ router.post('/user-profile', authenticateGitHub, async (req, res) => { averagePerDay: parseFloat(averagePerDay) }; - // Helper functions + // Helper functions with proper date handling using date-fns function isConsecutiveDay(date1, date2) { - const d1 = new Date(date1); - const d2 = new Date(date2); - const diffTime = Math.abs(d2 - d1); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - return diffDays === 1; + try { + // Parse ISO date strings to Date objects in UTC + const d1 = parseISO(date1); + const d2 = parseISO(date2); + + // Check if date2 is exactly 1 day after date1 (not using Math.abs) + const dayDifference = differenceInDays(d2, d1); + return dayDifference === 1; + } catch (error) { + console.error('Error comparing dates:', error); + return false; + } } function isRecentDate(date, today) { - const diffTime = Math.abs(new Date(today) - new Date(date)); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - return diffDays <= 1; + try { + // Parse the contribution date + const contributionDate = parseISO(date); + const todayDate = parseISO(today); + + // Check if the contribution was today or yesterday + return isToday(contributionDate) || isYesterday(contributionDate) || + differenceInDays(todayDate, contributionDate) <= 1; + } catch (error) { + console.error('Error checking recent date:', error); + return false; + } } function calculateCurrentStreak(dates) { - let streak = 1; // Start with 1 if we have any contributions - for (let i = dates.length - 1; i > 0; i--) { - if (isConsecutiveDay(dates[i-1], dates[i])) { - streak++; - } else { - break; + if (!dates || dates.length === 0) return 0; + + try { + let streak = 1; // Start with 1 if we have any contributions + + // Work backwards from the most recent date + for (let i = dates.length - 1; i > 0; i--) { + if (isConsecutiveDay(dates[i-1], dates[i])) { + streak++; + } else { + break; // Break on first non-consecutive day + } } + + return streak; + } catch (error) { + console.error('Error calculating current streak:', error); + return 0; } - return streak; } function getDayFullName(shortDay) {