Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,26 @@ $ npm start

---


## API Endpoints

### Backend Data Fetching

The application uses two main API endpoints:

#### 1. `/api/github/get-data` (Home Page)
- **Purpose**: Fetches basic GitHub data for home dashboard
- **Returns**: User issues and pull requests
- **Used in**: Home page for displaying recent activity


#### 2. `/api/github/user-profile` (Analytics Page)
- **Purpose**: Fetches comprehensive user analytics data
- **Returns**: Complete user profile, repositories, contribution stats, language statistics, rankings
- **Used in**: Analytics dashboard for detailed insights

---

### 🌟 Coming Soon
- Add options to track stars, followers, following
- Add options to track engagements (e.g. comments, closing, opening and merging PRs)
Expand Down
15 changes: 15 additions & 0 deletions backend/middlewares/authenticateGitHub.js
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const UserSchema = new mongoose.Schema({
},
});



UserSchema.pre('save', async function (next) {

if (!this.isModified('password'))
Expand All @@ -32,6 +34,8 @@ UserSchema.pre('save', async function (next) {
}
});



// Compare passwords during login
UserSchema.methods.comparePassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
Expand Down
5 changes: 5 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"license": "ISC",
"description": "",
"dependencies": {
<<<<<<< HEAD
"@octokit/rest": "^22.0.0",
"axios": "^1.11.0",
=======
>>>>>>> f00eb9898001de6940350eaf72bd05f9ac76129a
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.3",
"cors": "^2.8.5",
Expand Down
272 changes: 272 additions & 0 deletions backend/routes/details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
//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;
4 changes: 3 additions & 1 deletion backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ app.use(passport.session());

// Routes
const authRoutes = require('./routes/auth');
const githubRoutes = require('./routes/details');
app.use('/api/auth', authRoutes);
app.use('/api/github', githubRoutes);

// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, {}).then(() => {
Expand All @@ -36,4 +38,4 @@ mongoose.connect(process.env.MONGO_URI, {}).then(() => {
});
}).catch((err) => {
console.log('MongoDB connection error:', err);
});
});
Empty file added backend/test-api.html
Empty file.
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,27 @@
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.3.0",
<<<<<<< HEAD
"react-router-dom": "^6.28.0",
"recharts": "^3.1.0"
=======
"react-router-dom": "^6.28.0"
>>>>>>> f00eb9898001de6940350eaf72bd05f9ac76129a
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@types/jasmine": "^5.1.8",
"@types/node": "^22.10.1",
<<<<<<< HEAD
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-redux": "^7.1.34",
=======
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/react-redux": "^7.1.34",
"@types/react-router-dom": "^5.3.3",
>>>>>>> f00eb9898001de6940350eaf72bd05f9ac76129a
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"bcryptjs": "^3.0.2",
Expand Down
Loading