diff --git a/README.md b/README.md index d5b55a0..a65fb82 100644 --- a/README.md +++ b/README.md @@ -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) 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/models/User.js b/backend/models/User.js index 779294f..b667987 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -18,6 +18,8 @@ const UserSchema = new mongoose.Schema({ }, }); + + UserSchema.pre('save', async function (next) { if (!this.isModified('password')) @@ -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); diff --git a/backend/package.json b/backend/package.json index 53bd64b..bf23a95 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/routes/details.js b/backend/routes/details.js new file mode 100644 index 0000000..af93962 --- /dev/null +++ b/backend/routes/details.js @@ -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; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 3f19f00..d6be412 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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(() => { @@ -36,4 +38,4 @@ mongoose.connect(process.env.MONGO_URI, {}).then(() => { }); }).catch((err) => { console.log('MongoDB connection error:', err); -}); +}); \ No newline at end of file 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 7b1e40b..a1200aa 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Routes/Router.tsx b/src/Routes/Router.tsx index 900a915..a912d43 100644 --- a/src/Routes/Router.tsx +++ b/src/Routes/Router.tsx @@ -1,12 +1,12 @@ import { Navigate, Route, Routes } from "react-router-dom" -import Home from "../pages/Home/Home" -import About from "../pages/About/About" -import Contact from "../pages/Contact/Contact" -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 Home from "../pages/Home/Home"; // Import the Home component +import About from "../pages/About/About"; // Import the About component +import Contact from "../pages/Contact/Contact"; // Import the Contact component +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"; const Router = () => { @@ -15,7 +15,8 @@ const Router = () => { {/* Redirect from root (/) to the home page */} } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/UserAnalyticsComp/ContributionStats.tsx b/src/components/UserAnalyticsComp/ContributionStats.tsx new file mode 100644 index 0000000..8249a36 --- /dev/null +++ b/src/components/UserAnalyticsComp/ContributionStats.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { + Grid, + Card, + CardContent, + Typography, + Box +} from '@mui/material'; + +interface ContributionStatsProps { + contributionStats: { + pushEvents: number; + pullRequestEvents: number; + issueEvents: number; + createEvents: number; + }; +} + +const ContributionStats: React.FC = ({ contributionStats }) => { + const statsItems = [ + { label: 'Push Events', value: contributionStats.pushEvents }, + { label: 'Pull Requests', value: contributionStats.pullRequestEvents }, + { label: 'Issues', value: contributionStats.issueEvents }, + { label: 'Repositories Created', value: contributionStats.createEvents } + ]; + + return ( + + + + + + Contribution Breakdown + + + {statsItems.map((item, index) => ( + + {item.label}: + {item.value} + + ))} + + + + + + ); +}; + +export default ContributionStats; \ No newline at end of file diff --git a/src/components/UserAnalyticsComp/LanguageStats.tsx b/src/components/UserAnalyticsComp/LanguageStats.tsx new file mode 100644 index 0000000..1b20343 --- /dev/null +++ b/src/components/UserAnalyticsComp/LanguageStats.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { + Grid, + Card, + CardContent, + Typography, + Box, + LinearProgress +} from '@mui/material'; + +interface LanguageStatsProps { + languageStats: Record; +} + +const LanguageStats: React.FC = ({ languageStats }) => { + const getTopLanguages = (languageStats: Record) => { + const total = Object.values(languageStats).reduce((sum, bytes) => sum + bytes, 0); + return Object.entries(languageStats) + .sort(([, a], [, b]) => b - a) + .slice(0, 5) + .map(([language, bytes]) => ({ + language, + percentage: ((bytes / total) * 100).toFixed(1) + })); + }; + + return ( + + + + + + Top Programming Languages + + {getTopLanguages(languageStats).map(({ language, percentage }) => ( + + + {language} + {percentage}% + + + + ))} + + + + + ); +}; + +export default LanguageStats; \ No newline at end of file diff --git a/src/components/UserAnalyticsComp/RepositoryTable.tsx b/src/components/UserAnalyticsComp/RepositoryTable.tsx new file mode 100644 index 0000000..768a2ea --- /dev/null +++ b/src/components/UserAnalyticsComp/RepositoryTable.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { + Grid, + Card, + CardContent, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Box, + Chip +} from '@mui/material'; + +interface RepositoryTableProps { + repositories: any[]; +} + +const RepositoryTable: React.FC = ({ repositories }) => { + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString(); + }; + + return ( + + + + + + Top Repositories by Stars + + + + + + Repository + Stars + Forks + Language + Updated + + + + {repositories.map((repo: any) => ( + + + + + + {repo.name} + + + {repo.description && ( + + {repo.description.substring(0, 100)}... + + )} + + + {repo.stargazers_count} + {repo.forks_count} + + {repo.language && ( + + )} + + {formatDate(repo.updated_at)} + + ))} + +
+
+
+
+
+
+ ); +}; + +export default RepositoryTable; \ No newline at end of file diff --git a/src/components/UserAnalyticsComp/UserFrom.tsx b/src/components/UserAnalyticsComp/UserFrom.tsx new file mode 100644 index 0000000..9846993 --- /dev/null +++ b/src/components/UserAnalyticsComp/UserFrom.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { + Card, + CardContent, + Typography, + Box, + TextField, + Button, + CircularProgress +} from '@mui/material'; + +interface UserFormProps { + onSubmit: (username: string, token: string) => void; + loading: boolean; +} + +const UserForm: React.FC = ({ onSubmit, loading }) => { + const [username, setUsername] = useState(''); + const [token, setToken] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(username, token); + }; + + return ( + + + + GitHub User Analytics + +
+ + setUsername(e.target.value)} + required + sx={{ flex: 1 }} + /> + setToken(e.target.value)} + type="password" + required + sx={{ flex: 1 }} + /> + + +
+
+
+ ); +}; + +export default UserForm; \ No newline at end of file diff --git a/src/components/UserAnalyticsComp/UserProfile.tsx b/src/components/UserAnalyticsComp/UserProfile.tsx new file mode 100644 index 0000000..6d6add0 --- /dev/null +++ b/src/components/UserAnalyticsComp/UserProfile.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { + Grid, + Card, + CardContent, + Typography, + Avatar, + Box, + Chip +} from '@mui/material'; + +interface UserProfileProps { + userData: { + profile: any; + socialStats: any; + }; +} + +const UserProfile: React.FC = ({ userData }) => { + return ( + + + + + + + {userData.profile.name || userData.profile.login} + + + @{userData.profile.login} + + {userData.profile.bio && ( + + {userData.profile.bio} + + )} + + + + + + + + + ); +}; + +export default UserProfile; \ No newline at end of file diff --git a/src/components/UserAnalyticsComp/UserStats.tsx b/src/components/UserAnalyticsComp/UserStats.tsx new file mode 100644 index 0000000..10357f3 --- /dev/null +++ b/src/components/UserAnalyticsComp/UserStats.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { + Grid, + Card, + CardContent, + Typography +} from '@mui/material'; +import { Star, GitFork, Code, Activity } from 'lucide-react'; + +interface UserStatsProps { + userData: { + rankings: any; + contributionStats: any; + }; +} + +const UserStats: React.FC = ({ userData }) => { + const statsData = [ + { + icon: , + value: userData.rankings.totalStars, + label: 'Total Stars' + }, + { + icon: , + value: userData.rankings.totalForks, + label: 'Total Forks' + }, + { + icon: , + value: userData.rankings.publicRepos, + label: 'Public Repos' + }, + { + icon: , + value: userData.contributionStats.totalEvents, + label: 'Activities' + } + ]; + + return ( + + {statsData.map((stat, index) => ( + + + + {stat.icon} + {stat.value} + {stat.label} + + + + ))} + + ); +}; + +export default UserStats; \ No newline at end of file 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/index.css b/src/index.css index bd6213e..404589f 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,21 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + + +html, body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow-x: hidden; +} + +body { + background-color: #000000; +} + +#root { + width: 100%; + min-height: 100vh; +} \ No newline at end of file diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 613a32e..7b5ae78 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -22,12 +22,18 @@ import { FormControl, InputLabel, } from "@mui/material"; +import { BarChart3 } from "lucide-react"; +import { useNavigate } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; -import { useGitHubAuth } from "../../hooks/useGitHubAuth"; -import { useGitHubData } from "../../hooks/useGitHubData"; +// import { useGitHubAuth } from "../../hooks/useGitHubAuth"; import { usePagination } from "../../hooks/usePagination"; +// import { useGitHubAuth } from "../../hooks/useGitHubAuth"; +// import { useGitHubData } from "../../hooks/useGitHubData"; +//moving data fetching to the backend for security purpose - ashish-choudhari-git +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 +46,58 @@ interface GitHubItem { } const Home: React.FC = () => { - 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 navigate = useNavigate(); + + //moved fetching to the backend routes - details.js + + // Hooks for managing user authentication + // const { + // username, + // setUsername, + // token, + // setToken, + // error: authError, + // getOctokit, + // validateCredentials, + // } = 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 theme = useTheme(); +// const { +// username, +// setUsername, +// token, +// setToken, +// error: authError, +// getOctokit, +// } = useGitHubAuth(); +// const octokit = getOctokit(); +// const { +// issues, +// prs, +// loading, +// error: dataError, +// fetchData, +// } = useGitHubData(octokit); + const { page, itemsPerPage, handleChangePage, paginateData } = usePagination(ROWS_PER_PAGE); @@ -68,9 +109,80 @@ 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); + } + } + + + + // Handle data submission to fetch GitHub data + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - fetchData(username); + // ashish-choudhari-git Code for security + const isValid = await validateCredentials(); + if(isValid){ + fetchData(); + } + }; const formatDate = (dateString: string): string => @@ -149,9 +261,35 @@ const Home: React.FC = () => { required sx={{ flex: 1 }} /> - + + + + + @@ -186,48 +324,78 @@ const Home: React.FC = () => { sx={{ minWidth: 150 }} /> + + + {/* End Date */} + setEndDate(e.target.value)} + InputLabelProps={{ shrink: true }} + sx={{ + flexBasis: { xs: "100%", sm: "100%", md: "48%", lg: "23%" }, + flexGrow: 1, + }} + /> + + + +{/* Tabs and State Dropdown */} + + setTab(newValue)} + variant="scrollable" + scrollButtons="auto" + sx={{ flexGrow: 1, minWidth: "200px" }} + > + + + + + + State + + + + + +{/* Error Alert */} +{(error) && ( + + {error} + +)} - - setTab(newValue)} sx={{ flex: 1 }}> - - - - - State - - - {(authError || dataError) && ( diff --git a/src/pages/UserAnalytics/UserAnalytics.tsx b/src/pages/UserAnalytics/UserAnalytics.tsx new file mode 100644 index 0000000..4f661d0 --- /dev/null +++ b/src/pages/UserAnalytics/UserAnalytics.tsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from 'react'; +import { Container, Alert, Box,Grid } from '@mui/material'; +import { useLocation } from 'react-router-dom'; +import axios from 'axios'; +import UserForm from './components/UserAnalyticsComp/UserForm'; +import UserProfile from './components/UserAnalyticsComp/UserProfile'; +import UserStats from './components/UserAnalyticsComp/UserStats'; +import LanguageStats from './components/UserAnalyticsComp/LanguageStats'; +import ContributionStats from './components/UserAnalyticsComp/ContributionStats'; +import RepositoryTable from './components/UserAnalyticsComp/RepositoryTable'; + +const backendUrl = import.meta.env.VITE_BACKEND_URL; + +interface UserData { + profile: any; + repositories: any[]; + languageStats: Record; + contributionStats: any; + rankings: any; + highlights: any; + stars: any[]; + commitHistory: any[]; + socialStats: any; +} + +const UserAnalytics: React.FC = () => { + const location = useLocation(); + const [userData, setUserData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + // Get username and token from navigation state + const navigationState = location.state as { username?: string; token?: string } || {}; + const [initialUsername] = useState(navigationState.username || ''); + const [initialToken] = useState(navigationState.token || ''); + + // Auto-fetch data if username and token are provided from Home page + useEffect(() => { + if (initialUsername && initialToken) { + handleSubmit(initialUsername, initialToken); + } + }, [initialUsername, initialToken]); + + const handleSubmit = async (username: string, token: string) => { + setLoading(true); + setError(''); + + try { + const response = await axios.post(`${backendUrl}/api/github/user-profile`, { + username, + token + }); + setUserData(response.data); + } catch (err: any) { + setError(err.response?.data?.message || 'Error fetching user data'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + {error && ( + + {error} + + )} + + {userData && ( + + + + + + + + + + + + + + )} + + + ); +}; + +export default UserAnalytics; \ No newline at end of file diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/ContributionStats.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/ContributionStats.tsx new file mode 100644 index 0000000..da952db --- /dev/null +++ b/src/pages/UserAnalytics/components/UserAnalyticsComp/ContributionStats.tsx @@ -0,0 +1,210 @@ +import React from 'react'; +import { Paper, Typography, Grid, Box, Chip } from '@mui/material'; +import { Calendar, GitCommit, TrendingUp } from 'lucide-react'; + +interface ContributionStatsProps { + contributionStats: { + totalContributions?: number; + longestStreak?: number; + currentStreak?: number; + mostActiveDay?: string; + averagePerDay?: number; + }; +} + +const ContributionStats: React.FC = ({ contributionStats }) => { + if (!contributionStats) { + return ( + + + Contribution Statistics + + + No contribution data available + + + ); + } + + const { + totalContributions = 0, + longestStreak = 0, + currentStreak = 0, + mostActiveDay = 'N/A', + averagePerDay = 0 + } = contributionStats; + + return ( + + + Contribution Statistics + + + + + + + + + {totalContributions.toLocaleString()} + + + Total Contributions + + + + + + + + + + + {longestStreak} + + + Longest Streak (days) + + + + + + + + + + + {currentStreak} + + + Current Streak (days) + + + + + + + + + + + ); +}; + +export default ContributionStats; diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/LanguageStats.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/LanguageStats.tsx new file mode 100644 index 0000000..fc897da --- /dev/null +++ b/src/pages/UserAnalytics/components/UserAnalyticsComp/LanguageStats.tsx @@ -0,0 +1,209 @@ +import React, { useState, useEffect } from 'react'; +import { Paper, Typography, Box, useTheme, useMediaQuery } from '@mui/material'; +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts'; + +interface LanguageStatsProps { + languageStats: Record; +} + +const LanguageStats: React.FC = ({ languageStats }) => { + const [animationKey, setAnimationKey] = useState(0); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + useEffect(() => { + setAnimationKey(prev => prev + 1); + }, [languageStats]); + + if (!languageStats || Object.keys(languageStats).length === 0) { + return ( + + + Programming Languages + + + No language data available + + + ); + } + + const totalBytes = Object.values(languageStats).reduce((sum, bytes) => sum + bytes, 0); + const languageEntries = Object.entries(languageStats) + .sort(([, a], [, b]) => b - a) + .slice(0, 8); + + const modernColors = [ + '#3b82f6', '#8b5cf6', '#06b6d4', '#10b981', + '#f59e0b', '#ef4444', '#ec4899', '#6366f1' + ]; + + const pieData = languageEntries.map(([language, bytes], index) => ({ + name: language, + value: bytes, + percentage: ((bytes / totalBytes) * 100).toFixed(1), + color: modernColors[index % modernColors.length] + })); + + const CustomTooltip = ({ active, payload }: any) => { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( + + + {data.name} + + + {data.percentage}% + + + ); + } + return null; + }; + + return ( + + + Programming Languages + + + + + + + {pieData.map((entry, index) => ( + + ))} + + } + position={{ x: 0, y: 0 }} + allowEscapeViewBox={{ x: false, y: false }} + wrapperStyle={{ + pointerEvents: 'none', + zIndex: 1000 + }} + /> + { + const dataItem = pieData.find(item => item.name === value); + return ( + + {value} ({dataItem?.percentage || '0'}%) + + ); + }} + /> + + + + + ); +}; + +export default LanguageStats; diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/RepositoryTable.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/RepositoryTable.tsx new file mode 100644 index 0000000..d28697e --- /dev/null +++ b/src/pages/UserAnalytics/components/UserAnalyticsComp/RepositoryTable.tsx @@ -0,0 +1,321 @@ +import React from 'react'; +import { + Paper, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Chip, + Link, + Box, + Card, + CardContent, + Grid, + useTheme, + useMediaQuery, +} from '@mui/material'; +import { Star, GitFork, Eye } from 'lucide-react'; + +interface Repository { + name: string; + description?: string; + stars: number; + forks: number; + watchers: number; + language?: string; + html_url: string; + updated_at: string; +} + +interface RepositoryTableProps { + repositories: Repository[]; +} + +const RepositoryTable: React.FC = ({ repositories }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + if (!repositories || repositories.length === 0) { + return ( + + + Top Repositories + + + No repository data available + + + ); + } + + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleDateString(); + }; + + const getLanguageColor = (language: string): string => { + const colors: { [key: string]: string } = { + 'JavaScript': '#1976d2', + 'TypeScript': '#42A5F5', + 'Python': '#64B5F6', + 'Java': '#90CAF9', + 'C++': '#BBDEFB', + 'C#': '#1565c0', + 'PHP': '#2196F3', + 'Ruby': '#1e88e5', + 'Go': '#1976d2', + 'Rust': '#0d47a1', + }; + return colors[language] || '#1976d2'; + }; + + return ( + + + Top Repositories + + + {isMobile ? ( + // Mobile Card View + + {repositories.slice(0, 10).map((repo, index) => ( + + + + + + {repo.name} + + {repo.description && ( + + {repo.description.length > 120 + ? `${repo.description.substring(0, 120)}...` + : repo.description} + + )} + + + + {repo.language && ( + + )} + + Updated: {formatDate(repo.updated_at)} + + + + + + + + {repo.stars} + + + + + + {repo.forks} + + + + + + {repo.watchers} + + + + + + + ))} + + ) : ( + // Desktop Table View + + + + + Repository + Language + Stars + Forks + Watchers + Updated + + + + {repositories.slice(0, 10).map((repo, index) => ( + + + + + {repo.name} + + {repo.description && ( + + {repo.description.length > 100 + ? `${repo.description.substring(0, 100)}...` + : repo.description} + + )} + + + + {repo.language && ( + + )} + + + + + + {repo.stars} + + + + + + + + {repo.forks} + + + + + + + + {repo.watchers} + + + + + + {formatDate(repo.updated_at)} + + + + ))} + +
+
+ )} +
+ ); +}; + +export default RepositoryTable; diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/UserForm.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/UserForm.tsx new file mode 100644 index 0000000..20c7799 --- /dev/null +++ b/src/pages/UserAnalytics/components/UserAnalyticsComp/UserForm.tsx @@ -0,0 +1,181 @@ +import React, { useState, useEffect } from 'react'; +import { Box, TextField, Button, Paper, CircularProgress } from '@mui/material'; + +interface UserFormProps { + onSubmit: (username: string, token: string) => void; + loading: boolean; + initialUsername?: string; + initialToken?: string; +} + +const UserForm: React.FC = ({ + onSubmit, + loading, + initialUsername = '', + initialToken = '' +}) => { + const [username, setUsername] = useState(initialUsername); + const [token, setToken] = useState(initialToken); + + // Update form when initial values change + useEffect(() => { + setUsername(initialUsername); + setToken(initialToken); + }, [initialUsername, initialToken]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(username, token); + }; + + return ( + +
+ + setUsername(e.target.value)} + required + sx={{ + flex: { md: 1 }, + width: { xs: '100%', md: 'auto' }, + minWidth: { md: '280px' }, + '& .MuiOutlinedInput-root': { + borderRadius: 3, + backgroundColor: '#374151', + fontFamily: '"Inter", "SF Pro Display", "Roboto", sans-serif', + fontSize: '1rem', + '& fieldset': { + borderColor: '#6b7280', + borderWidth: '2px' + }, + '&:hover fieldset': { + borderColor: '#3b82f6', + }, + '&.Mui-focused fieldset': { + borderColor: '#3b82f6', + borderWidth: '2px' + }, + '& input': { + color: '#f9fafb', + fontWeight: 500 + }, + }, + '& .MuiInputLabel-root': { + color: '#d1d5db', + fontWeight: 500, + fontFamily: '"Inter", "SF Pro Display", "Roboto", sans-serif', + '&.Mui-focused': { + color: '#3b82f6', + fontWeight: 600 + } + } + }} + /> + setToken(e.target.value)} + type="password" + required + sx={{ + flex: { md: 1 }, + width: { xs: '100%', md: 'auto' }, + minWidth: { md: '280px' }, + '& .MuiOutlinedInput-root': { + borderRadius: 3, + backgroundColor: '#374151', + fontFamily: '"Inter", "SF Pro Display", "Roboto", sans-serif', + fontSize: '1rem', + '& fieldset': { + borderColor: '#6b7280', + borderWidth: '2px' + }, + '&:hover fieldset': { + borderColor: '#3b82f6', + }, + '&.Mui-focused fieldset': { + borderColor: '#3b82f6', + borderWidth: '2px' + }, + '& input': { + color: '#f9fafb', + fontWeight: 500 + }, + }, + '& .MuiInputLabel-root': { + color: '#d1d5db', + fontWeight: 500, + fontFamily: '"Inter", "SF Pro Display", "Roboto", sans-serif', + '&.Mui-focused': { + color: '#3b82f6', + fontWeight: 600 + } + } + }} + /> + + +
+
+ ); +}; + +export default UserForm; diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/UserProfile.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/UserProfile.tsx new file mode 100644 index 0000000..c41a1fd --- /dev/null +++ b/src/pages/UserAnalytics/components/UserAnalyticsComp/UserProfile.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { Box, Paper, Avatar, Typography, Chip, Grid } from '@mui/material'; + +interface UserProfileProps { + userData: { + profile: any; + }; +} + +const UserProfile: React.FC = ({ userData }) => { + const { profile } = userData; + + return ( + + + + + + + + + + + {profile.name || profile.login} + + + @{profile.login} + + {profile.bio && ( + + {profile.bio} + + )} + + + + + + + + + + ); +}; + +export default UserProfile; diff --git a/src/pages/UserAnalytics/components/UserAnalyticsComp/UserStats.tsx b/src/pages/UserAnalytics/components/UserAnalyticsComp/UserStats.tsx new file mode 100644 index 0000000..cadb0f8 --- /dev/null +++ b/src/pages/UserAnalytics/components/UserAnalyticsComp/UserStats.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { Grid, Paper, Typography, Box } from '@mui/material'; +import { Star, GitFork, Eye, Users } from 'lucide-react'; + +interface UserStatsProps { + userData: { + rankings: any; + highlights: any; + socialStats: any; + stars: any[]; + }; +} + +const UserStats: React.FC = ({ userData }) => { + const { rankings, socialStats, stars } = userData; + + const statCards = [ + { + title: 'Total Stars', + value: stars?.length || 0, + icon: , + color: '#3b82f6', + bgColor: '#1f2937', + gradient: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)' + }, + { + title: 'Repositories', + value: rankings?.repositoryRanking?.length || 0, + icon: , + color: '#8b5cf6', + bgColor: '#1f2937', + gradient: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)' + }, + { + title: 'Watchers', + value: socialStats?.totalWatchers || 0, + icon: , + color: '#06b6d4', + bgColor: '#1f2937', + gradient: 'linear-gradient(135deg, #06b6d4 0%, #0891b2 100%)' + }, + { + title: 'Forks', + value: socialStats?.totalForks || 0, + icon: , + color: '#10b981', + bgColor: '#1f2937', + gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)' + } + ]; + + return ( + + {statCards.map((stat, index) => ( + + + + {stat.icon} + + + {typeof stat.value === 'number' ? stat.value.toLocaleString() : stat.value} + + + {stat.title} + + + + ))} + + ); +}; + +export default UserStats; diff --git a/src/pages/UserProfile/UserProfile.tsx b/src/pages/UserProfile/UserProfile.tsx index 8a7d993..87b3ba0 100644 --- a/src/pages/UserProfile/UserProfile.tsx +++ b/src/pages/UserProfile/UserProfile.tsx @@ -1,6 +1,9 @@ import { useParams } from "react-router-dom"; import { useEffect, useState } from "react"; +<<<<<<< HEAD +======= import toast from "react-hot-toast"; +>>>>>>> f00eb9898001de6940350eaf72bd05f9ac76129a type PR = { title: string; @@ -18,6 +21,16 @@ export default function UserProfile() { async function fetchData() { if (!username) return; +<<<<<<< HEAD + const userRes = await fetch(`https://api.github.com/users/${username}`); + const userData = await userRes.json(); + setProfile(userData); + + const prsRes = await fetch(`https://api.github.com/search/issues?q=author:${username}+type:pr`); + const prsData = await prsRes.json(); + setPRs(prsData.items); + setLoading(false); +======= try { const userRes = await fetch(`https://api.github.com/users/${username}`); const userData = await userRes.json(); @@ -31,11 +44,36 @@ export default function UserProfile() { } finally { setLoading(false); } +>>>>>>> f00eb9898001de6940350eaf72bd05f9ac76129a } fetchData(); }, [username]); +<<<<<<< HEAD + if (loading) return
Loading...
; + + return ( +
+ {profile && ( +
+ +

{profile.login}

+

{profile.bio}

+
+ )} + +

Pull Requests

+ +======= const handleCopyLink = () => { navigator.clipboard.writeText(window.location.href); toast.success("🔗 Shareable link copied to clipboard!"); @@ -81,6 +119,7 @@ export default function UserProfile() { ) : (

No pull requests found for this user.

)} +>>>>>>> f00eb9898001de6940350eaf72bd05f9ac76129a
); }