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/package.json b/backend/package.json index 9891c08..d502ed9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,10 +15,12 @@ "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", "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 new file mode 100644 index 0000000..b47aab2 --- /dev/null +++ b/backend/routes/details.js @@ -0,0 +1,441 @@ +//Aashish Choudhari's Code | GSSoC Contributor +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(); +const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes + +// Helper function to create delay +const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + + +// 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{ + 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 with concurrency control and caching + const languageStats = {}; + const repositories = []; + + // 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) => { + 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 + }); + + // 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 + } + } + + // 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 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 { + // 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 + } + } + } + } + } + } + `; + + // 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 + }) + ]); + + // 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; + } + }); + }); + } + + // 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; + } + }); + }); + } + + // 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 with improved logic + const contributionDates = Object.keys(contributionsByDate).sort(); + + // Calculate longest streak with proper error handling + let tempStreak = 0; + 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 + } + + // 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)) { + 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 mostActiveDay = Object.keys(dayActivityCount).reduce((a, b) => + dayActivityCount[a] > dayActivityCount[b] ? a : b + ); + + // Average contributions per day (based on actual contribution data) + const daysWithContributions = Object.keys(contributionsByDate).length; + const averagePerDay = daysWithContributions > 0 ? (totalContributions / daysWithContributions).toFixed(1) : 0; + + const contributionStats = { + totalContributions, + longestStreak, + currentStreak, + mostActiveDay: getDayFullName(mostActiveDay), + averagePerDay: parseFloat(averagePerDay) + }; + + // Helper functions with proper date handling using date-fns + function isConsecutiveDay(date1, date2) { + 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) { + 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) { + 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; + } + } + + 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/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 6343fcc..7f3446b 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,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 7f33a32..ee65674 100644 --- a/src/Routes/Router.tsx +++ b/src/Routes/Router.tsx @@ -1,11 +1,15 @@ -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 ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx"; + +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 UserAnalytics from "../pages/UserAnalytics/UserAnalytics.tsx"; + + const Router = () => { return ( @@ -13,6 +17,7 @@ const Router = () => { } /> } /> } /> + } /> } /> } /> } /> 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/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index a0ebe10..34d96fd 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -1,71 +1,67 @@ -import { useState, useCallback } from 'react'; -export const useGitHubData = (getOctokit: () => any) => { - const [issues, setIssues] = useState([]); - const [prs, setPrs] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [totalIssues, setTotalIssues] = useState(0); - const [totalPrs, setTotalPrs] = useState(0); - const [rateLimited, setRateLimited] = useState(false); - const fetchPaginated = async (octokit: any, username: string, type: string, page = 1, per_page = 10) => { - const q = `author:${username} is:${type}`; - const response = await octokit.request('GET /search/issues', { - q, - sort: 'created', - order: 'desc', - per_page, - page, - }); +// import { useState, useCallback } from 'react'; - return { - items: response.data.items, - total: response.data.total_count, - }; - }; +// export const useGitHubData = (octokit) => { +// const [issues, setIssues] = useState([]); +// const [prs, setPrs] = useState([]); +// const [loading, setLoading] = useState(false); +// const [error, setError] = useState(''); - const fetchData = useCallback( - async (username: string, page = 1, perPage = 10) => { - - const octokit = getOctokit(); +// const fetchAll = async (url, params) => { +// let page = 1; +// let results = []; +// let hasMore = true; - if (!octokit || !username || rateLimited) return; +// while (hasMore) { +// const response = await octokit.request(url, { ...params, page }); +// results = results.concat(response.data.items); +// hasMore = response.data.items.length === 100; +// page++; +// } - setLoading(true); - setError(''); +// 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, +// }; +// }; - try { - const [issueRes, prRes] = await Promise.all([ - fetchPaginated(octokit, username, 'issue', page, perPage), - fetchPaginated(octokit, username, 'pr', page, perPage), - ]); - setIssues(issueRes.items); - setPrs(prRes.items); - setTotalIssues(issueRes.total); - setTotalPrs(prRes.total); - } catch (err: any) { - if (err.status === 403) { - setError('GitHub API rate limit exceeded. Please wait or use a token.'); - setRateLimited(true); // Prevent further fetches - } else { - setError(err.message || 'Failed to fetch data'); - } - } finally { - setLoading(false); - } - }, - [getOctokit, rateLimited] - ); - return { - issues, - prs, - totalIssues, - totalPrs, - loading, - error, - fetchData, - }; -}; diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index d0ff154..0f07cd9 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -1,4 +1,7 @@ -import React, { useState, useEffect } from "react"; + +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; + import { Container, Box, @@ -23,10 +26,16 @@ 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,28 +49,37 @@ 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); + +//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); - const { - username, - setUsername, - token, - setToken, - error: authError, - getOctokit, - } = useGitHubAuth(); - - //const octokit = getOctokit(); - - const { - issues, - prs, - totalIssues, - totalPrs, - loading, - error: dataError, - fetchData, - } = useGitHubData(getOctokit); const [tab, setTab] = useState(0); const [page, setPage] = useState(0); @@ -80,14 +98,75 @@ const Home: React.FC = () => { } }, [tab, page]); - const handleSubmit = (e: React.FormEvent): void => { - e.preventDefault(); - setPage(0); - fetchData(username, 1, ROWS_PER_PAGE); + //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() + } + }); }; - const handlePageChange = (_: unknown, newPage: number) => { - setPage(newPage); + + + //fetching data from backend + const fetchData= async()=>{ + + setLoading(true); + setError(''); + + try{ + + 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?.status, err.response?.statusText); + setError(err.response?.data?.message || 'Failed to fetch GitHub data. Please check your credentials.'); + setIssues([]); + setPrs([]); + } + finally{ + setLoading(false); + } + } + + + const handleSubmit = async (e: React.FormEvent)=> { + e.preventDefault(); + + const isValid = await validateCredentials(); + if(isValid){ + fetchData(); + } + }; const formatDate = (dateString: string): string => @@ -154,9 +233,34 @@ const Home: React.FC = () => { required sx={{ flex: 1, minWidth: 150 }} /> - + + + + @@ -243,9 +347,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

+
+ ); +} 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