├── .env ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── features.txt ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── App.jsx ├── assets │ └── images │ │ ├── avatar.png │ │ ├── chart.png │ │ ├── contributionGraph.svg │ │ ├── friday.png │ │ ├── logo.png │ │ ├── monday.png │ │ ├── octocat.png │ │ ├── saturday.png │ │ ├── sunday.png │ │ ├── thursday.png │ │ ├── tuesday.png │ │ ├── webLogo.png │ │ └── wednesday.png ├── components │ ├── ActivityStats.jsx │ ├── Canvas.jsx │ ├── ChartAndProductiveDay.jsx │ ├── ContributionGraph.jsx │ ├── ContributionSummary.jsx │ ├── Cursor.jsx │ ├── Footer.jsx │ ├── GithubContributionCalendar.jsx │ ├── LanguageChart.jsx │ ├── Loader.jsx │ ├── Modal.jsx │ ├── PopularPR.jsx │ ├── Quote.jsx │ ├── TopRepos.jsx │ └── UserDetails.jsx ├── constants │ └── scrollClass.js ├── index.css ├── main.jsx ├── pages │ ├── Homepage.jsx │ └── StatsPage.jsx └── utils │ ├── getActiveDays.js │ ├── getAllRepos.js │ ├── getBadge.js │ ├── getCanvas.js │ ├── getContributionData.js │ ├── getDayIndex.js │ ├── getMostUsedLanguages.js │ ├── getOctokit.js │ ├── getQuote.js │ ├── getScrollAnimation.js │ ├── moveCursor.js │ └── validateGithubUsername.js ├── tailwind.config.js ├── vercel.json └── vite.config.js /.env: -------------------------------------------------------------------------------- 1 | VITE_GITHUB_TOKEN = "GITHUB_TOKEN" 2 | VITE_API_NINJAS_KEY = "API_NINJAS_KEY" -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | yarn.lock 26 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

GitWrap - GitHub Contributions Analyzer

2 |
3 |
GitWrap is a web application that analyzes your GitHub contributions, showcasing a personalized summary of your coding journey throughout the year. Reflect, celebrate, and share your open-source achievements!
4 | 5 | 6 | ## Tech Stack 7 | 8 | - **React:** Frontend framework for building user interfaces. 9 | - **GraphQL:** Query language for APIs, used to fetch GitHub data. 10 | - **GitHub API:** Interface for accessing various GitHub features. 11 | - **Octokit:** GitHub REST API client for programmatic access to GitHub resources. 12 | - **React Icons:** Library providing a set of popular icons for React applications. 13 | 14 | ## Getting Started 15 | Follow these steps to get started with GitWrap: 16 | 17 | ### Prerequisite 18 | 19 | - Node.js (https://nodejs.org/) installed on your machine. 20 | 21 | ### Installation 22 | 23 | 1. Clone the repository: 24 | ```bash 25 | git clone https://github.com/singodiyashubham87/GitWrap.git 26 | cd GitWrap 27 | ``` 28 | 29 | 2. Install dependencies: 30 | ```bash 31 | npm install 32 | ``` 33 | 3. Edit the .env file & add your Github Personal Access Token: 34 | ```bash 35 | VITE_GITHUB_TOKEN = "GITHUB_PERSONAL_ACCESS_TOKEN" 36 | ``` 37 | 4. Start the app: 38 | ```bash 39 | npm run dev 40 | ``` 41 | 42 | ## Features 43 | 44 | - **Contribution Summary:** Get an overview of your GitHub contributions for a specified time range. 45 | - **Total Active Days:** Track the total number of days you actively contributed to repositories. 46 | - **Max Streak:** Identify your longest streak of consecutive days with contributions. 47 | - **Most Productive Date:** Find out the date when you made the maximum number of contributions. 48 | - **Languages Used:** Explore the languages you've used across all your repositories. 49 | 50 | ## Credits 51 | Special thanks to [GitHub](https://github.com/) for providing the [GitHub GraphQL API](https://docs.github.com/en/graphql) that makes this project possible. 52 | 53 | ## Author 54 | * Shubham Singodiya 55 | 56 | ## Contributors 57 | 58 |
59 | 60 | 61 | 62 |
63 | 64 | ## License 65 | This project is licensed under the MIT License. 66 | 67 | ## Support 68 | Support the project by starring the repository. 69 | 70 | ## Using Daytona to Develop GitWrap 71 | To make it easier for others to contribute to this project, we've integrated Daytona for a fast and consistent development environment. Follow the steps below to spin up a Daytona workspace for GitWrap: 72 | 73 | ### Prerequisites 74 | **Daytona** must be installed on your system (follow the instructions here). 75 | 76 | ## Steps to Get Started with Daytona 77 | 78 | 1. Clone the repository (if not done already): 79 | ```bash 80 | git clone https://github.com/singodiyashubham87/GitWrap.git 81 | cd GitWrap 82 | ``` 83 | 84 | 2. Create a Daytona workspace for the project: To quickly spin up a development environment, 85 | use Daytona: 86 | ```bash 87 | daytona create https://github.com/singodiyashubham87/GitWrap 88 | ``` 89 | 90 | 3. Access the workspace: Daytona will create and start a workspace for you. Once it's running, you can access it in your browser or through VS Code using the Remote SSH feature. 91 | 92 | 4. Start working: Once you're inside the Daytona workspace, you can start contributing to the project immediately by editing code, testing, and running the app within a pre-configured environment. 93 | -------------------------------------------------------------------------------- /features.txt: -------------------------------------------------------------------------------- 1 | Canvas on login page 2 | Lazy Loading 3 | Search Functionality 4 | Dependabot 5 | Dockerise 6 | CI/CD 7 | 8 | 9 | Data fetch Done: 10 | 11 | Separate Card: 12 | badgeDetailsIcon 13 | 14 | 15 | Implemented: 16 | 17 | 18 | >Username 19 | >Followers 20 | >Location 21 | >Badge 22 | 23 | >Total Contributions 24 | >Issues Opened 25 | >Issues Closed 26 | >Total Commits 27 | >Total Pull Requests 28 | 29 | Separate Card: 30 | [getActiveDays()] 31 | >Active Days(activeDays) 32 | >Most Productive Date(mostProductiveDate) + maxContributionCount on that day(maxContributionCount) 33 | >Max. Streak(maxStreak) 34 | 35 | Separate Card: 36 | >Top Repo 1 37 | >Top Repo 2 38 | >Top Repo 3 39 | 40 | Separate Card: 41 | >Popular Pr name 42 | >Popular Pr state 43 | >Popular Pr creation date 44 | >Popular Pr URL 45 | 46 | Separate Card: 47 | [getMostUsedLanguage()] 48 | >Most used languages[Array of 5] 49 | {Show using piechart by utilising chart.js} 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GitWrap - GitHub Contributions Analyzer! 8 | 9 | 10 | 11 | logo 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wrapgit", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@cubejs-client/core": "^0.35.23", 14 | "chart.js": "^4.4.1", 15 | "octokit": "^3.1.2", 16 | "react": "^18.3.1", 17 | "react-dom": "^18.3.1", 18 | "react-github-calendar": "^4.1.3", 19 | "react-icons": "^5.2.1", 20 | "react-router-dom": "^6.22.1" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.3.1", 24 | "@types/react-dom": "^18.3.0", 25 | "@vitejs/plugin-react": "^4.2.1", 26 | "autoprefixer": "^10.4.19", 27 | "eslint": "^8.55.0", 28 | "eslint-plugin-react": "^7.34.1", 29 | "eslint-plugin-react-hooks": "^4.6.2", 30 | "eslint-plugin-react-refresh": "^0.4.5", 31 | "postcss": "^8.4.33", 32 | "tailwindcss": "^3.4.1", 33 | "vite": "^5.1.7" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import Homepage from "./pages/Homepage"; 2 | import StatsPage from "./pages/StatsPage"; 3 | import {BrowserRouter as Router,Routes, Route} from "react-router-dom"; 4 | 5 | function App() { 6 | return ( 7 | 8 | 9 | }/> 10 | }/> 11 | 12 | 13 | ) 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/avatar.png -------------------------------------------------------------------------------- /src/assets/images/chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/chart.png -------------------------------------------------------------------------------- /src/assets/images/friday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/friday.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/monday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/monday.png -------------------------------------------------------------------------------- /src/assets/images/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/octocat.png -------------------------------------------------------------------------------- /src/assets/images/saturday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/saturday.png -------------------------------------------------------------------------------- /src/assets/images/sunday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/sunday.png -------------------------------------------------------------------------------- /src/assets/images/thursday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/thursday.png -------------------------------------------------------------------------------- /src/assets/images/tuesday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/tuesday.png -------------------------------------------------------------------------------- /src/assets/images/webLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/webLogo.png -------------------------------------------------------------------------------- /src/assets/images/wednesday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singodiyashubham87/gitwrap/f2605c284e6e7139803f09e9aa0caf06827f03fd/src/assets/images/wednesday.png -------------------------------------------------------------------------------- /src/components/ActivityStats.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import { FaGithubAlt } from "react-icons/fa"; 3 | import { scrollClass } from "../constants/scrollClass"; 4 | 5 | 6 | const ActivityStats = (props) => { 7 | const { activeDays, maxStreak, mostProductiveDate, maxContributionCount } = props; 8 | return ( 9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |

17 | Activity 18 |

19 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default ActivityStats; 43 | -------------------------------------------------------------------------------- /src/components/Canvas.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import getCanvas from "../utils/getCanvas"; 3 | 4 | const Canvas = () => { 5 | useEffect(() => { 6 | getCanvas(); 7 | }, []); 8 | 9 | return ( 10 |
11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Canvas; 17 | -------------------------------------------------------------------------------- /src/components/ChartAndProductiveDay.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import { useState, useEffect } from "react"; 3 | import getDayIndex from "../utils/getDayIndex"; 4 | 5 | import LanguageChart from "../components/LanguageChart"; 6 | 7 | // import Days Name images 8 | import sunday from "../assets/images/sunday.png"; 9 | import monday from "../assets/images/monday.png"; 10 | import tuesday from "../assets/images/tuesday.png"; 11 | import wednesday from "../assets/images/wednesday.png"; 12 | import thursday from "../assets/images/thursday.png"; 13 | import friday from "../assets/images/friday.png"; 14 | import saturday from "../assets/images/saturday.png"; 15 | import { scrollClass } from "../constants/scrollClass"; 16 | 17 | 18 | const ChartAndProductiveDay = (props) => { 19 | const { 20 | langArray, 21 | langButton, 22 | handleLangButton, 23 | mostProductiveDate, 24 | } = props; 25 | 26 | // Day-related states 27 | const [dayIndex, setDayIndex] = useState(null); 28 | const [dayImage, setDayImage] = useState(null); 29 | 30 | // Effect to update day index based on most productive date 31 | useEffect(() => { 32 | if (mostProductiveDate) { 33 | setDayIndex(getDayIndex(mostProductiveDate)); 34 | } 35 | }, [mostProductiveDate]); 36 | 37 | // Effect to set day image based on day index 38 | useEffect(() => { 39 | if (dayIndex !== null) { 40 | switch (dayIndex) { 41 | case 0: 42 | setDayImage(sunday); 43 | break; 44 | case 1: 45 | setDayImage(monday); 46 | break; 47 | case 2: 48 | setDayImage(tuesday); 49 | break; 50 | case 3: 51 | setDayImage(wednesday); 52 | break; 53 | case 4: 54 | setDayImage(thursday); 55 | break; 56 | case 5: 57 | setDayImage(friday); 58 | break; 59 | case 6: 60 | setDayImage(saturday); 61 | break; 62 | default: 63 | setDayImage(null); 64 | } 65 | } 66 | }, [dayIndex]); 67 | 68 | return ( 69 |
70 | {langButton ? ( 71 | 77 | ) : ( 78 |
79 | {langArray && } 80 |

81 | Most Used Languages 82 |

83 |
84 | )} 85 |
86 |
87 | mostProductiveDay 92 |
93 |

94 | Most Productive Day 95 |

96 |
97 |
98 | ); 99 | }; 100 | 101 | export default ChartAndProductiveDay; 102 | -------------------------------------------------------------------------------- /src/components/ContributionGraph.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import { GiWinterGloves } from "react-icons/gi"; 3 | import GithubContributionCalendar from "./GithubContributionCalendar"; 4 | 5 | const ContributionGraph = ({userDetails}) => { 6 | const { username } = userDetails; 7 | 8 | return ( 9 |
10 | 11 |

12 | Contribution Graph 13 |

14 |
15 | {username && ( 16 | 21 | )} 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default ContributionGraph; -------------------------------------------------------------------------------- /src/components/ContributionSummary.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | // import icons from react-icons 3 | import { VscRepo } from "react-icons/vsc"; 4 | import { IoGitCommitOutline } from "react-icons/io5"; 5 | import { IoIosGitMerge, IoIosGitPullRequest } from "react-icons/io"; 6 | import { GoIssueOpened, GoIssueClosed, GoIssueDraft } from "react-icons/go"; 7 | import { GiWinterHat } from "react-icons/gi"; 8 | import { HiOutlineExternalLink } from "react-icons/hi"; 9 | import { scrollClass } from "../constants/scrollClass"; 10 | 11 | const ContributionSummary = (props) => { 12 | const { 13 | totalContributions, 14 | totalRepositoryContributions, 15 | openIssues, 16 | closedIssues, 17 | totalIssueContributions, 18 | totalCommitContributions, 19 | totalPullRequestContributions, 20 | userDetails, 21 | } = props; 22 | 23 | return ( 24 | <> 25 | {totalContributions && ( 26 |
27 | 28 | 29 |

30 | Summary 31 |

32 |
    33 |
  • 34 | 35 | 36 | Total Contributions:{" "} 37 | 38 |
    39 | 40 | {totalContributions} 41 | 42 | 47 | 48 | 49 |
    50 |
  • 51 | 52 |
  • 53 | 54 | 55 | Total Repos Contributed:{" "} 56 | 57 |
    58 | 59 | {totalRepositoryContributions} 60 | 61 | 66 | 67 | 68 |
    69 |
  • 70 | 71 |
  • 72 | 73 | 74 | Issues Open:{" "} 75 | 76 |
    77 | 78 | {openIssues} 79 | 80 | 85 | 86 | 87 |
    88 |
  • 89 | 90 |
  • 91 | 92 | 93 | Issues Closed:{" "} 94 | 95 |
    96 | 97 | {closedIssues} 98 | 99 | 104 | 105 | 106 |
    107 |
  • 108 | 109 |
  • 110 | 111 | 112 | Total Issue Contributions:{" "} 113 | 114 |
    115 | 116 | {totalIssueContributions} 117 | 118 | 123 | 124 | 125 |
    126 |
  • 127 | 128 |
  • 129 | 130 | 131 | Total Commit Contributions:{" "} 132 | 133 |
    134 | 135 | {totalCommitContributions} 136 | 137 | 142 | 143 | 144 |
    145 |
  • 146 | 147 |
  • 148 | 149 | 150 | Pull Requests Contributions:{" "} 151 | 152 |
    153 | 154 | {totalPullRequestContributions} 155 | 156 | 161 | 162 | 163 |
    164 |
  • 165 |
166 |
167 | )} 168 | 169 | ); 170 | }; 171 | 172 | export default ContributionSummary; 173 | -------------------------------------------------------------------------------- /src/components/Cursor.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import moveCursor from "../utils/moveCursor"; 3 | 4 | const Cursor = () => { 5 | useEffect(() => { 6 | moveCursor(); 7 | }); 8 | 9 | return ( 10 | <> 11 |
15 |
19 | 20 | ); 21 | }; 22 | 23 | export default Cursor; 24 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 | 18 | ); 19 | }; 20 | 21 | export default Footer; 22 | -------------------------------------------------------------------------------- /src/components/GithubContributionCalendar.jsx: -------------------------------------------------------------------------------- 1 | import GitHubCalendar from "react-github-calendar"; 2 | 3 | const GithubContributionCalendar = (props) => { 4 | // eslint-disable-next-line react/prop-types 5 | const { ghUsername, sizeOfBlock, marginOfBlock } = props; 6 | 7 | 8 | return ( 9 | 20 | ); 21 | }; 22 | 23 | export default GithubContributionCalendar; 24 | -------------------------------------------------------------------------------- /src/components/LanguageChart.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import Chart from "chart.js/auto"; 3 | 4 | // eslint-disable-next-line react/prop-types 5 | function LanguageChart({data}) { 6 | const chartContainer = useRef(null); 7 | const chartInstance = useRef(null); 8 | 9 | useEffect(() => { 10 | const chartCanvas = chartContainer.current; 11 | if (chartCanvas) { 12 | // Destroy previous chart instance 13 | if (chartInstance.current) { 14 | chartInstance.current.destroy(); 15 | } 16 | 17 | // Create a new chart instance 18 | chartInstance.current = new Chart(chartCanvas, { 19 | type: "pie", 20 | data: { 21 | // eslint-disable-next-line react/prop-types 22 | labels: data.map((row) => row.name), 23 | datasets: [ 24 | { 25 | label: "Lines of Code", 26 | // eslint-disable-next-line react/prop-types 27 | data: data.map((row) => row.count), 28 | }, 29 | ], 30 | }, 31 | }); 32 | } 33 | }, [data]); 34 | 35 | return ( 36 | <> 37 |
38 |
39 | 40 |
41 |
42 | 43 | ); 44 | } 45 | 46 | export default LanguageChart; 47 | -------------------------------------------------------------------------------- /src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import logo from "../assets/images/webLogo.png" 2 | 3 | const Loader = () => { 4 | return ( 5 |
6 | 12 |
13 | ) 14 | } 15 | 16 | export default Loader; -------------------------------------------------------------------------------- /src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | const Modal = (props) => { 2 | // eslint-disable-next-line react/prop-types 3 | const { alert, alertError, closeModal } = props; 4 | 5 | return ( 6 | <> 7 |
11 |
12 |

13 | {alert || "Alert!"} 14 |

15 |

16 | {alertError} 17 |

18 | 24 |
25 | 26 | ); 27 | }; 28 | 29 | export default Modal; -------------------------------------------------------------------------------- /src/components/PopularPR.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import { FaGitAlt } from "react-icons/fa"; 3 | import { HiOutlineExternalLink } from "react-icons/hi"; 4 | import { scrollClass } from "../constants/scrollClass"; 5 | 6 | const PopularPR = (props) => { 7 | const { popularPrName, popularPrState, popularPrCreationDate, popularPrURL } = props; 8 | return ( 9 | <> 10 | {popularPrName === "undefined" ? null : ( 11 |
12 |
13 |
14 | 15 |
16 |
17 |
18 |

19 | Top Pull Request 20 |

21 |
    22 |
  • 23 | Title: 24 | 25 | {popularPrName} 26 | 27 |
  • 28 |
  • 29 | State: 30 | 31 | {popularPrState} 32 | 33 |
  • 34 |
  • 35 | Created On: 36 | 37 | {popularPrCreationDate} 38 | 39 |
  • 40 |
  • 41 | PR Link: 42 | 48 | 49 | 50 |
  • 51 |
52 |
53 |
54 | )} 55 | 56 | ); 57 | }; 58 | 59 | export default PopularPR; 60 | -------------------------------------------------------------------------------- /src/components/Quote.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import { LuGitPullRequestClosed, LuGitFork } from "react-icons/lu"; 3 | import { scrollClass } from "../constants/scrollClass"; 4 | 5 | const Quote = ({ quote, author }) => { 6 | return ( 7 |
8 | 9 | 10 |

11 | Quote of the Day 12 |

13 |
14 | {`"${quote ?? 'Hope is a good thing my friend.'}"`}
15 | 16 | {`-${author ?? 'Master Mickey'}`} 17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default Quote; 24 | -------------------------------------------------------------------------------- /src/components/TopRepos.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import { FaGithub, FaGitlab } from "react-icons/fa"; 3 | import { scrollClass } from "../constants/scrollClass"; 4 | 5 | const TopRepos = ({ topReposArray }) => { 6 | return ( 7 | <> 8 | {topReposArray.length > 2 ? ( 9 |
10 |
11 |

12 | Repositories 13 |

14 |
    15 | {topReposArray?.slice(0,4)?.map((repo, index) => ( 16 |
  • 20 | 21 | {index + 1}. {`${repo.name}`} 22 | 23 | 29 | 30 | 31 |
  • 32 | ))} 33 |
34 |
35 |
36 |
37 | 38 |
39 |
40 |
41 | ) : null} 42 | 43 | ); 44 | }; 45 | 46 | export default TopRepos; 47 | -------------------------------------------------------------------------------- /src/components/UserDetails.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import avatar from "../assets/images/avatar.png"; 3 | import { FaGithub, FaQuestionCircle } from "react-icons/fa"; 4 | import { TbChristmasTree } from "react-icons/tb"; 5 | import getBadge from "../utils/getBadge"; 6 | 7 | const UserDetails = (props) => { 8 | const { userInfo, totalContributions } = props; 9 | const { userAvatar, githubURL, username, followers, location } = userInfo; 10 | 11 | return ( 12 |
13 |
14 |
15 | userAvatar 20 |
21 | 22 | 23 | 24 |
25 |
26 | 27 |

28 | User Details 29 |

30 |
    31 |
  • 32 | Username: 33 | {username} 34 |
  • 35 |
  • 36 | Followers: 37 | {followers} 38 |
  • 39 |
  • 40 | Location: 41 | {location} 42 |
  • 43 |
  • 44 | Badge: 45 | 46 | {getBadge(totalContributions)} 47 | {/* */} 51 |
    52 | 53 | 54 | {"Contributions > 1500 => Master"} 55 |
    56 | 57 | {"Contributions > 1000 && <= 1500 => Expert"} 58 | 59 |
    60 | 61 | { 62 | "Contributions > 500 && <= 1000 => Intermediate" 63 | } 64 | 65 |
    66 | 67 | {"Contributions > 250 && <= 500 => Skilled"} 68 | 69 |
    70 | {"Contributions <= 250 => Novice"} 71 |
    72 |
    73 |
    74 |
  • 75 |
76 |
77 |
78 | ); 79 | }; 80 | 81 | export default UserDetails; 82 | -------------------------------------------------------------------------------- /src/constants/scrollClass.js: -------------------------------------------------------------------------------- 1 | export const scrollClass = "reveal opacity-0 translate-y-[150px] transition ease duration-500"; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Borel&family=Inter:wght@400;500;600;700&display=swap'); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | 8 | :root { 9 | background-color: #212325; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | box-sizing: border-box; 16 | } 17 | 18 | /* for scroll animation */ 19 | .active{ 20 | transform: translateY(0px); 21 | opacity: 1; 22 | } -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/pages/Homepage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Link, useNavigate } from "react-router-dom"; 3 | import Modal from "../components/Modal"; 4 | import Loader from "../components/Loader"; 5 | import Canvas from "../components/Canvas"; 6 | import Cursor from "../components/Cursor"; 7 | import logo from "../assets/images/logo.png"; 8 | import octocatImage from "../assets/images/octocat.png"; 9 | import { FaRegArrowAltCircleRight } from "react-icons/fa"; 10 | import validateGithubUsername from "../utils/validateGithubUsername"; 11 | 12 | const Homepage = () => { 13 | const navigateTo = useNavigate(); 14 | const [showModal, setShowModal] = useState(false); 15 | const [ghUsername, setGhUsername] = useState(""); 16 | const [loader, setLoader] = useState(false); 17 | 18 | useEffect(() => localStorage.clear(), []); 19 | 20 | const handleInputChange = (e) => setGhUsername(e.target.value); 21 | 22 | const handleSubmit = async (e) => { 23 | if (e.type === "click" || e.key === "Enter") { 24 | e.preventDefault(); 25 | if (!ghUsername) return setShowModal(true); 26 | setLoader(true); 27 | const isValidUsername = await validateGithubUsername(ghUsername); 28 | setLoader(false); 29 | isValidUsername ? navigateTo("/stats") : setShowModal(true); 30 | } 31 | }; 32 | 33 | return ( 34 | <> 35 | {showModal && ( 36 | setShowModal(false)} 40 | /> 41 | )} 42 | {loader && } 43 | 44 | 45 |
46 | logo 47 |
48 | octocat 49 |
50 | 57 | 58 | 62 | 63 |
64 |
65 |
66 | 67 | ); 68 | }; 69 | 70 | export default Homepage; 71 | -------------------------------------------------------------------------------- /src/pages/StatsPage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | // import Components 4 | import Loader from "../components/Loader"; 5 | import Canvas from "../components/Canvas"; 6 | import Cursor from "../components/Cursor"; 7 | import UserDetails from "../components/UserDetails"; 8 | import ContributionGraph from "../components/ContributionGraph"; 9 | import ContributionSummary from "../components/ContributionSummary"; 10 | import ActivityStats from "../components/ActivityStats"; 11 | import TopRepos from "../components/TopRepos"; 12 | import PopularPR from "../components/PopularPR"; 13 | import ChartAndProductiveDay from "../components/ChartAndProductiveDay"; 14 | import Quote from "../components/Quote"; 15 | import Footer from "../components/Footer"; 16 | 17 | // import icons from react-icons 18 | import { FaStar } from "react-icons/fa"; 19 | 20 | // import utility functions 21 | import getQuote from "../utils/getQuote"; 22 | import getScrollAnimation from "../utils/getScrollAnimation"; 23 | import getContributionData from "../utils/getContributionData"; 24 | import getMostUsedLanguages from "../utils/getMostUsedLanguages"; 25 | 26 | const StatsPage = () => { 27 | getScrollAnimation(); 28 | // Loading state 29 | const [loader, setLoader] = useState(true); 30 | 31 | // User info states 32 | const [userDetails, setUserDetails] = useState({ 33 | userAvatar: "", 34 | username: "", 35 | followers: "", 36 | location: "unknown", 37 | githubURL: "", 38 | }); 39 | 40 | // User contribution data states 41 | const [totalContributions, setTotalContributions] = useState(""); 42 | const [openIssues, setOpenIssues] = useState(""); 43 | const [closedIssues, setClosedIssues] = useState(""); 44 | const [totalIssueContributions, setTotalIssueContributions] = useState(""); 45 | const [totalCommitContributions, setTotalCommitContributions] = useState(""); 46 | const [totalPullRequestContributions, setTotalPullRequestContributions] = 47 | useState(""); 48 | 49 | // User's Popular PR data 50 | const [popularPrName, setPopularPrName] = useState(""); 51 | const [popularPrState, setPopularPrState] = useState(""); 52 | const [popularPrCreationDate, setPopularPrCreationDate] = useState(""); 53 | const [popularPrURL, setPopularPrURL] = useState(""); 54 | 55 | // User Top Repos data 56 | const [totalRepositoryContributions, setTotalRepositoryContributions] = 57 | useState(""); 58 | const [topReposCount, setTopReposCount] = useState(""); 59 | const [topReposArray, setTopReposArray] = useState([]); 60 | 61 | // User activity data states 62 | const [mostProductiveDate, setMostProductiveDate] = useState(""); 63 | const [maxContributionCount, setMaxContributionCount] = useState(""); 64 | const [maxStreak, setMaxStreak] = useState(""); 65 | const [activeDays, setActiveDays] = useState(""); 66 | 67 | // Quotes states 68 | const [quote, setQuote] = useState( 69 | "You may say I'm a dreamer, but I'm not the only one. I hope someday you'll join us. And the world will live as one." 70 | ); 71 | const [author, setAuthor] = useState("John Lennon"); 72 | 73 | // Language-related states 74 | const [langButton, setLangButton] = useState(true); 75 | const [langArray, setLangArray] = useState([]); 76 | 77 | // Handle language button click 78 | const handleLangButton = async () => { 79 | setLoader(true); 80 | const resArray = await getMostUsedLanguages(userDetails.username); 81 | setLangArray(resArray); 82 | setLangButton(false); 83 | setLoader(false); 84 | }; 85 | 86 | // Effect to fetch user data when the username changes 87 | useEffect(() => { 88 | const fetchData = async () => { 89 | if (userDetails.username) { 90 | try { 91 | await fetchQuote() 92 | await getContributionData(userDetails.username) 93 | updateState(); 94 | } catch (error) { 95 | console.error("Error fetching data:", error); 96 | } finally { 97 | setLoader(false); 98 | } 99 | } 100 | }; 101 | fetchData(); 102 | }, [userDetails.username]); 103 | 104 | // Function to fetch a random quote 105 | async function fetchQuote() { 106 | const res = await getQuote(); 107 | setQuote(res[0].quote); 108 | setAuthor(res[0].author); 109 | } 110 | 111 | // Effect to update user-related states from local storage 112 | useEffect(() => { 113 | setUserDetails({ 114 | userAvatar: localStorage.getItem("avatar") || "", 115 | username: localStorage.getItem("username") || "errorGettingUsername", 116 | followers: localStorage.getItem("followers") || "-1", 117 | location: 118 | localStorage.getItem("location") !== "null" 119 | ? localStorage.getItem("location") || "errorGettingLocation" 120 | : "unknown", 121 | githubURL: localStorage.getItem("githubUrl") || "errorGettingGithubURL", 122 | }); 123 | }, []); 124 | 125 | // Function to update contribution-related states 126 | function updateState() { 127 | // Contribution data 128 | setTotalContributions(localStorage.getItem("totalContributions") || ""); 129 | setOpenIssues(localStorage.getItem("openIssues") || ""); 130 | setClosedIssues(localStorage.getItem("closedIssues") || ""); 131 | setTotalIssueContributions( 132 | localStorage.getItem("totalIssueContributions") || "" 133 | ); 134 | setTotalCommitContributions( 135 | localStorage.getItem("totalCommitContributions") || "" 136 | ); 137 | setTotalPullRequestContributions( 138 | localStorage.getItem("totalPullRequestContributions") || "" 139 | ); 140 | setTotalRepositoryContributions( 141 | localStorage.getItem("totalRepositoryContributions") || "" 142 | ); 143 | 144 | // User's Popular PR's data 145 | setPopularPrName(localStorage.getItem("popularPrName") || ""); 146 | setPopularPrState(localStorage.getItem("popularPrState") || ""); 147 | setPopularPrCreationDate( 148 | localStorage.getItem("popularPrCreationDate") || "" 149 | ); 150 | setPopularPrURL(localStorage.getItem("popularPrURL") || ""); 151 | 152 | // User Top Repos 153 | setTopReposCount(localStorage.getItem("topReposCount") || "0"); 154 | 155 | // User Activity Details 156 | setMostProductiveDate(localStorage.getItem("mostProductiveDate") || ""); 157 | setMaxContributionCount(localStorage.getItem("maxContributionCount") || ""); 158 | setMaxStreak(localStorage.getItem("maxStreak") || ""); 159 | setActiveDays(localStorage.getItem("activeDays") || ""); 160 | } 161 | 162 | // Effect to update top repos array 163 | useEffect(() => { 164 | if (topReposCount > 2) { 165 | setTopReposArray((prevTopReposArray) => { 166 | let updatedArray = [...prevTopReposArray]; 167 | 168 | for (let i = 0; i < topReposCount; i++) { 169 | let repoObj = { 170 | name: localStorage.getItem(`topRepoName${i}`), 171 | url: localStorage.getItem(`topRepoURL${i}`), 172 | }; 173 | updatedArray.push(repoObj); 174 | } 175 | 176 | return updatedArray; 177 | }); 178 | } 179 | }, [topReposCount]); 180 | 181 | return ( 182 | <> 183 | {loader && } 184 | 185 | 186 |
187 |
188 | {/* User Details Section */} 189 | 193 | 194 | {/* Contribution Graph Section */} 195 | 196 | 197 | {/* GitHub Contribution Summary Section */} 198 | {totalContributions && ( 199 | 209 | )} 210 | 211 | {/* Activity Stats Section */} 212 | {activeDays && ( 213 | 219 | )} 220 | 221 | {/* Top Repos Section */} 222 | 223 | 224 | {/* Popular PR Section */} 225 | 231 | 232 | {/* Chart & Most Productive Day Section */} 233 | 239 | 240 | {/* Quote of the Day Section */} 241 | 242 | 243 | {/* Star on Github */} 244 | 249 | 254 | 255 | {/* Footer Section */} 256 |
258 |
259 | 260 | ); 261 | }; 262 | 263 | export default StatsPage; 264 | -------------------------------------------------------------------------------- /src/utils/getActiveDays.js: -------------------------------------------------------------------------------- 1 | export default async function getActiveDays(weeksArray) { 2 | // Initialize variables to store results 3 | let maxContributionCount = 0; 4 | let mostProductiveDate = null; 5 | let activeDays = 0; 6 | let maxStreak = 0; 7 | 8 | // Initialize variables for tracking streak 9 | let hasContributedLastWeek = false; 10 | let streakDays = 0; 11 | 12 | try { 13 | // Loop through each week in the provided array 14 | weeksArray.forEach((week) => { 15 | // Initialize streak variables 16 | let currStreak = 0; 17 | 18 | // Check if there was a contribution streak from the last week 19 | hasContributedLastWeek ? (currStreak += streakDays) : (currStreak = 0); 20 | 21 | // Check if there was a contribution yesterday in the current week 22 | let hasContributedYesterday = 23 | week.contributionDays[0].contributionCount > 0; 24 | 25 | // Loop through each day in the contribution days of the week 26 | week.contributionDays.map((day) => { 27 | // Get the number of contributions for the day 28 | let count = day.contributionCount; 29 | 30 | if (count > 0) { 31 | activeDays += 1; // Increment total active days 32 | 33 | // Update max contribution count and most productive date if needed 34 | if (count > maxContributionCount) { 35 | maxContributionCount = count; 36 | mostProductiveDate = day.date; 37 | } 38 | 39 | // Update streak if there was a contribution yesterday 40 | if (hasContributedYesterday) { 41 | currStreak++; 42 | } else { 43 | currStreak = 1; 44 | hasContributedYesterday = true; 45 | } 46 | } else { 47 | hasContributedYesterday = false; 48 | } 49 | }); 50 | 51 | // Update max streak if the current streak is greater 52 | if (currStreak > maxStreak) { 53 | maxStreak = currStreak; 54 | } 55 | 56 | // Check if there was a contribution on the last day of the week 57 | week.contributionDays[6].contributionCount 58 | ? ((streakDays = currStreak), (hasContributedLastWeek = true)) 59 | : (streakDays = 0); 60 | }); 61 | } catch (err) { 62 | storeData(mostProductiveDate, maxContributionCount, maxStreak, activeDays); 63 | // Use a try-catch block to handle potential errors, The error is related to using forEach loop on an object which is behaving like an array returning the total active days in case of an error 64 | return activeDays; 65 | } 66 | // Store data to localStorage and return; 67 | storeData(mostProductiveDate, maxContributionCount, maxStreak, activeDays); 68 | return activeDays; 69 | } 70 | 71 | function storeData(mostProductiveDate, maxContributionCount, maxStreak, activeDays) { 72 | // Store the most productive date and max contribution count in local storage 73 | localStorage.setItem("mostProductiveDate", mostProductiveDate); 74 | localStorage.setItem("maxContributionCount", maxContributionCount); 75 | localStorage.setItem("maxStreak", maxStreak); 76 | localStorage.setItem("activeDays", activeDays); 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/getAllRepos.js: -------------------------------------------------------------------------------- 1 | import { octokit } from "./getOctokit"; 2 | 3 | export default async function getAllRepos(username){ 4 | try { 5 | let page = 1; 6 | let repos = []; 7 | 8 | // eslint-disable-next-line no-constant-condition 9 | while (true) { 10 | const response = await octokit.request(`GET https://api.github.com/users/${username}/repos?page=${page}`); 11 | const data = await response.data; 12 | 13 | if (data.length === 0) { 14 | // No more repositories, break the loop 15 | break; 16 | } 17 | 18 | // Add the current page's repositories to the overall list 19 | repos = [...repos, ...data]; 20 | 21 | // Move to the next page 22 | page++; 23 | } 24 | return repos; 25 | } catch (error) { 26 | console.error('Error fetching user repositories:', error); 27 | } 28 | } -------------------------------------------------------------------------------- /src/utils/getBadge.js: -------------------------------------------------------------------------------- 1 | export default function getBadge(contributionCount) { 2 | if (contributionCount > 1500) { 3 | return "Master"; 4 | } else if (contributionCount > 1000) { 5 | return "Expert"; 6 | } else if (contributionCount > 500) { 7 | return "Intermediate"; 8 | } else if (contributionCount > 250) { 9 | return "Skilled"; 10 | } else if (contributionCount < 250) { 11 | return "Novice"; 12 | } else { 13 | return "No Badge"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/getCanvas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to set up and animate circles on a canvas. 3 | */ 4 | 5 | let TOTAL_CIRCLES = 2000; 6 | let width = window.innerWidth; 7 | if(width < 400 ){ 8 | TOTAL_CIRCLES = 300; 9 | }else if(width < 500){ 10 | TOTAL_CIRCLES = 500; 11 | }else if(width < 700){ 12 | TOTAL_CIRCLES = 700; 13 | }else if(width < 1000){ 14 | TOTAL_CIRCLES = 1000; 15 | }else{ 16 | TOTAL_CIRCLES = 1500; 17 | } 18 | 19 | export default function getCanvas() { 20 | // Get canvas element by ID and set its dimensions 21 | const canvas = document.getElementById("myCanvas"); 22 | canvas.width = window.innerWidth; 23 | canvas.height = window.innerHeight; 24 | 25 | // Get 2D rendering context 26 | const ctx = canvas.getContext("2d"); 27 | 28 | // Event listener to adjust canvas dimensions on window resize 29 | window.addEventListener("resize", () => { 30 | let width = window.innerWidth; 31 | let height = window.innerHeight; 32 | if(width < 400 ){ 33 | TOTAL_CIRCLES = 300; 34 | }else if(width < 500){ 35 | TOTAL_CIRCLES = 500; 36 | }else if(width < 700){ 37 | TOTAL_CIRCLES = 700; 38 | }else if(width < 1000){ 39 | TOTAL_CIRCLES = 1000; 40 | }else{ 41 | TOTAL_CIRCLES = 1500; 42 | } 43 | 44 | canvas.width = width; 45 | canvas.height = height; 46 | init(); 47 | }); 48 | 49 | // Array of colors for circles 50 | const colorsArray = [ 51 | "#191717", 52 | "#CCC8AA", 53 | "#7D7C7C", 54 | "#191717", 55 | "#2C3333", 56 | "#1B2430", 57 | ]; 58 | 59 | // Interaction Part 60 | const mousePosition = { 61 | x: undefined, 62 | y: undefined, 63 | }; 64 | 65 | // Event listener to track mouse position 66 | window.addEventListener("mousemove", (e) => { 67 | mousePosition.x = e.x; 68 | mousePosition.y = e.y; 69 | }); 70 | 71 | // Circle class to create and update circles 72 | class Circle { 73 | constructor(x, y, radius, dx, dy) { 74 | this.x = x; 75 | this.y = y; 76 | this.radius = radius; 77 | this.dx = dx; 78 | this.dy = dy; 79 | this.minRadius = radius; 80 | this.maxRadius = 50; 81 | this.color = colorsArray[Math.floor(Math.random() * colorsArray.length)]; 82 | } 83 | 84 | /** 85 | * Method to draw the circle on the canvas. 86 | */ 87 | draw = () => { 88 | ctx.beginPath(); 89 | ctx.arc(this.x, this.y, this.radius, Math.PI * 2, false); 90 | ctx.strokeStyle = "black"; 91 | ctx.stroke(); 92 | ctx.fillStyle = this.color; 93 | ctx.fill(); 94 | }; 95 | 96 | /** 97 | * Method to update the circle's position and size. 98 | */ 99 | update = () => { 100 | // Bounce off walls 101 | if (this.x > innerWidth - this.radius || this.x < this.radius) { 102 | this.dx = -this.dx; 103 | } else if (this.y > innerHeight - this.radius || this.y < this.radius) { 104 | this.dy = -this.dy; 105 | } 106 | 107 | // Interaction: Expand on mouse proximity, shrink otherwise 108 | if ( 109 | mousePosition.x - this.x < 50 && 110 | mousePosition.x - this.x > -50 && 111 | mousePosition.y - this.y < 50 && 112 | mousePosition.y - this.y > -50 113 | ) { 114 | if (this.radius < this.maxRadius) this.radius += 1; 115 | } else { 116 | if (this.radius > this.minRadius) { 117 | this.radius -= 1; 118 | } 119 | } 120 | 121 | // Move circle 122 | this.x += this.dx; 123 | this.y += this.dy; 124 | 125 | // Draw updated circle 126 | this.draw(); 127 | }; 128 | } 129 | 130 | // Array to store circle objects 131 | let circleArray = []; 132 | 133 | /** 134 | * Initialize the canvas with a set of circles. 135 | */ 136 | function init() { 137 | circleArray = []; 138 | for (let i = 0; i < TOTAL_CIRCLES; i++) { 139 | // Generate random parameters for each circle 140 | let radius = Math.floor(Math.random() * 3) + 2; 141 | let x = Math.random() * (window.innerWidth - 2 * radius) + radius; 142 | let y = Math.random() * (window.innerHeight - 2 * radius) + radius; 143 | let dx = Math.random() - 0.5; 144 | let dy = Math.random() - 0.5; 145 | 146 | // Create and add a new circle to the array 147 | circleArray.push(new Circle(x, y, radius, dx, dy)); 148 | } 149 | } 150 | 151 | /** 152 | * Animate the circles on the canvas. 153 | */ 154 | function animate() { 155 | // Request animation frame for continuous rendering 156 | requestAnimationFrame(animate); 157 | 158 | // Clear canvas 159 | ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); 160 | 161 | // Update and draw each circle in the array 162 | for (let i = 0; i < circleArray.length; i++) circleArray[i].update(); 163 | } 164 | 165 | // Initialize and start animation loop 166 | init(); 167 | animate(); 168 | } 169 | -------------------------------------------------------------------------------- /src/utils/getContributionData.js: -------------------------------------------------------------------------------- 1 | import getActiveDays from "./getActiveDays"; 2 | import { octokit } from "./getOctokit"; 3 | 4 | 5 | export default async function getContributionData(username) { 6 | const YEAR = 2023; 7 | 8 | const query = ` 9 | query { 10 | user(login: "${username}") { 11 | 12 | openIssues: issues(filterBy: {since: "${YEAR}-01-01T00:00:00.000Z", states: OPEN}) { 13 | totalCount 14 | } 15 | closedIssues: issues(filterBy: {since: "${YEAR}-01-01T00:00:00.000Z", states: CLOSED}) { 16 | totalCount 17 | } 18 | avatarUrl 19 | login 20 | contributionsCollection(from: "${YEAR}-01-01T00:00:00.000Z", to: "${YEAR + 1}-01-01T00:00:00.000Z") { 21 | restrictedContributionsCount 22 | totalIssueContributions 23 | totalCommitContributions 24 | totalRepositoryContributions 25 | totalPullRequestContributions 26 | totalPullRequestReviewContributions 27 | popularPullRequestContribution { 28 | pullRequest { 29 | title 30 | repository { 31 | name 32 | } 33 | createdAt 34 | updatedAt 35 | totalCommentsCount 36 | state 37 | url 38 | } 39 | } 40 | contributionCalendar { 41 | totalContributions 42 | weeks { 43 | contributionDays { 44 | contributionCount 45 | date 46 | } 47 | } 48 | } 49 | commitContributionsByRepository { 50 | contributions { 51 | totalCount 52 | } 53 | repository { 54 | name 55 | url 56 | languages(first: 3, orderBy: {field: SIZE, direction: DESC}) { 57 | edges { 58 | size 59 | node { 60 | color 61 | name 62 | id 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | `; 72 | 73 | const res = await octokit.graphql(query); 74 | const weeksArray = await res.user.contributionsCollection.contributionCalendar.weeks; 75 | await getActiveDays(weeksArray); 76 | storeData(res.user); 77 | } 78 | 79 | 80 | function storeData(userData){ 81 | const contributionsCollection = userData.contributionsCollection; 82 | const contributionCalendar = userData.contributionsCollection.contributionCalendar; 83 | 84 | localStorage.setItem("totalContributions", contributionCalendar?.totalContributions); 85 | localStorage.setItem("openIssues", userData?.openIssues?.totalCount); 86 | localStorage.setItem("closedIssues", userData?.closedIssues?.totalCount); 87 | localStorage.setItem("totalIssueContributions", contributionsCollection?.totalIssueContributions); 88 | localStorage.setItem("totalCommitContributions", contributionsCollection?.totalCommitContributions); 89 | localStorage.setItem("totalPullRequestContributions", contributionsCollection?.totalPullRequestContributions); 90 | localStorage.setItem("totalPullRequestReviewContributions", contributionsCollection?.totalPullRequestReviewContributions); 91 | localStorage.setItem("totalRepositoryContributions", contributionsCollection?.totalRepositoryContributions); 92 | localStorage.setItem("popularPrName", contributionsCollection?.popularPullRequestContribution?.pullRequest?.title); 93 | localStorage.setItem("popularPrState", contributionsCollection?.popularPullRequestContribution?.pullRequest?.state); 94 | localStorage.setItem("popularPrCreationDate", contributionsCollection?.popularPullRequestContribution?.pullRequest?.createdAt?.slice(0, 10)); 95 | localStorage.setItem("popularPrURL", contributionsCollection?.popularPullRequestContribution?.pullRequest?.url); 96 | 97 | 98 | storeTopRepos(contributionsCollection.commitContributionsByRepository); 99 | } 100 | 101 | function storeTopRepos(topReposObj){ 102 | const topReposCount = Math.min(topReposObj.length, 4); 103 | localStorage.setItem("topReposCount", topReposCount); 104 | for(let i = 0; i < topReposCount;i++){ 105 | localStorage.setItem(`topRepoName${i}`, topReposObj[i].repository.name); 106 | localStorage.setItem(`topRepoURL${i}`, topReposObj[i].repository.url); 107 | } 108 | } -------------------------------------------------------------------------------- /src/utils/getDayIndex.js: -------------------------------------------------------------------------------- 1 | export default function getDayIndex(dateString) { 2 | const date = new Date(dateString); 3 | const dayIndex = date.getDay(); 4 | return dayIndex; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/getMostUsedLanguages.js: -------------------------------------------------------------------------------- 1 | import getAllRepos from "./getAllRepos"; 2 | import { octokit } from "./getOctokit"; 3 | 4 | // Main function to fetch and process repository languages 5 | export default async function (username) { 6 | // Get the list of repositories for the given username 7 | const reposArray = await getAllRepos(username); 8 | // Object to store the aggregated languages across all repositories 9 | const langObj = {}; 10 | 11 | // Iterate over each repository 12 | for (const repo of reposArray) { 13 | const repoName = repo.full_name; 14 | 15 | try { 16 | // GitHub API token for authentication 17 | const token = import.meta.env.VITE_GITHUB_TOKEN; 18 | // Fetch the languages used in the repository 19 | const response = await octokit.request( 20 | `GET https://api.github.com/repos/${repoName}/languages`, 21 | { 22 | owner: username, 23 | repo: repoName, 24 | headers: { 25 | Authorization: `Bearer ${token}`, 26 | }, 27 | } 28 | ); 29 | const languages = response.data; 30 | 31 | // Process languages and update langObj 32 | if (Object.keys(languages).length !== 0) { 33 | Object.keys(languages).forEach((lang) => { 34 | // Check if langObj already has the language key 35 | if (Object.prototype.hasOwnProperty.call(langObj, lang)) { 36 | // If it exists, add the value 37 | langObj[lang] += languages[lang]; 38 | } else { 39 | // If it doesn't exist, create the key with the value 40 | langObj[lang] = languages[lang]; 41 | } 42 | }); 43 | } 44 | } catch (error) { 45 | console.error("Error fetching repository languages:", error); 46 | } 47 | } 48 | 49 | // Get the top 5 languages and display the result 50 | const topLanguagesArray = sortAndTrim(langObj); 51 | const transformedArray = transformArrayToObject(topLanguagesArray); 52 | return transformedArray; 53 | } 54 | 55 | // Function to sort the languages object and return a languages array and then trim the languages array 56 | function sortAndTrim(obj) { 57 | const langArray = Object.entries(obj).map(([key, value]) => [ 58 | key.trim(), 59 | value, 60 | ]); 61 | const sortedLangArray = langArray.sort((a, b) => b[1] - a[1]); 62 | const trimmedLangArray = sortedLangArray.slice( 63 | 0, 64 | Math.min(sortedLangArray.length, 3) 65 | ); 66 | localStorage.setItem("languagesFetched", "true"); 67 | return trimmedLangArray; 68 | } 69 | 70 | function transformArrayToObject(array){ 71 | const transformedArray = [ 72 | { 73 | name: array[0][0], 74 | count: array[0][1], 75 | },{ 76 | name: array[1][0], 77 | count: array[1][1], 78 | },{ 79 | name: array[2][0], 80 | count: array[2][1], 81 | } 82 | ] 83 | return transformedArray; 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/utils/getOctokit.js: -------------------------------------------------------------------------------- 1 | import { Octokit } from "octokit"; 2 | 3 | const githubToken = import.meta.env.VITE_GITHUB_TOKEN; 4 | const octokit = new Octokit({auth: githubToken}); 5 | 6 | export {octokit}; -------------------------------------------------------------------------------- /src/utils/getQuote.js: -------------------------------------------------------------------------------- 1 | const apiKey = import.meta.env.VITE_API_NINJAS_KEY; 2 | 3 | export default async function getQuote(){ 4 | const requestURL = "https://api.api-ninjas.com/v1/quotes" 5 | const header = { 6 | "X-Api-Key": apiKey, 7 | }; 8 | try { 9 | const response = await fetch(requestURL,{headers:header}); 10 | if (!response.ok) { 11 | throw new Error("Network response was not ok"); 12 | } 13 | const res = await response.json(); 14 | return res; 15 | } catch (error) { 16 | console.error("Error = ", error); 17 | } 18 | } -------------------------------------------------------------------------------- /src/utils/getScrollAnimation.js: -------------------------------------------------------------------------------- 1 | export default function getScrollAnimation() { 2 | window.addEventListener("scroll", reveal); 3 | 4 | function reveal() { 5 | const revealPoint = window.innerHeight - 150; 6 | 7 | const revealDivs = document.querySelectorAll(".reveal"); 8 | 9 | revealDivs.forEach((revealDiv) => { 10 | const revealDivTop = revealDiv.getBoundingClientRect().top; 11 | if (revealDivTop < revealPoint) { 12 | revealDiv.classList.add("active"); 13 | } else { 14 | revealDiv.classList.remove("active"); 15 | } 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/moveCursor.js: -------------------------------------------------------------------------------- 1 | export default function moveCursor(){ 2 | const cursorDot = document.getElementById('cursorDot'); 3 | const cursorOutline = document.getElementById('cursorOutline'); 4 | window.addEventListener('mousemove', function(e){ 5 | const posX = e.clientX; 6 | const posY = e.clientY; 7 | 8 | cursorDot.style.left = `${posX}px`; 9 | cursorDot.style.top = `${posY}px`; 10 | 11 | cursorOutline.style.left = `${posX}px`; 12 | cursorOutline.style.top = `${posY}px`; 13 | 14 | cursorOutline.animate({ 15 | left: `${posX}px`, 16 | top: `${posY}px` 17 | }, { duration: 500 , fill : "forwards"}); 18 | }) 19 | } -------------------------------------------------------------------------------- /src/utils/validateGithubUsername.js: -------------------------------------------------------------------------------- 1 | import { octokit } from "./getOctokit"; 2 | 3 | export default async function validateGithubUsername(ghUsername) { 4 | try { 5 | const res = await octokit.rest.users.getByUsername({ 6 | username: ghUsername, 7 | }); 8 | //Storing data to local storage before returning 9 | storeDataToLocalStorage(res); 10 | return true; 11 | } catch (error) { 12 | console.clear(); 13 | return false; 14 | } 15 | } 16 | 17 | //Store user data to local storage 18 | function storeDataToLocalStorage(res) { 19 | localStorage.setItem("username", res.data.login); 20 | localStorage.setItem("followers", res.data.followers); 21 | localStorage.setItem("location", res.data.location); 22 | localStorage.setItem("avatar", res.data.avatar_url); 23 | localStorage.setItem("githubUrl", res.data.html_url); 24 | } 25 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{js,jsx}", "./*.html"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | bgBlack: "#212325", 8 | lightBlue: "#9CDAF1", 9 | darkBlue: "#368186", 10 | lightRed: "#F4CBB2", 11 | lightGrey: "#ABABAB", 12 | darkGrey: "#2E3136", 13 | }, 14 | fontFamily: { 15 | primary: "Inter", 16 | secondary: "Borel" 17 | }, 18 | screens: { 19 | xxl: "1751px", 20 | mmd: "851px", 21 | gsm: "571px", 22 | msm: "491px", 23 | vsm: "441px", 24 | vvsm: "351px", 25 | }, 26 | }, 27 | }, 28 | plugins: [], 29 | }; 30 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }] 3 | } 4 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------