├── .eslintrc.json
├── public
├── favicon.ico
├── vercel.svg
└── next.svg
├── postcss.config.js
├── src
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── api
│ │ └── stats.ts
│ └── index.tsx
└── styles
│ └── globals.css
├── next.config.js
├── .gitignore
├── tailwind.config.ts
├── tsconfig.json
├── package.json
├── LICENSE
└── README.md
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabriel-pineda/git-wrapped-api/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css'
2 | import type { AppProps } from 'next/app'
3 |
4 | export default function App({ Component, pageProps }: AppProps) {
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | images: {
5 | domains: ["ph-files.imgix.net"],
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "paths": {
17 | "@/*": ["./src/*"]
18 | }
19 | },
20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
21 | "exclude": ["node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "git-wrapped",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@octokit/rest": "^21.0.2",
13 | "next": "13.5.7",
14 | "react": "^18",
15 | "react-dom": "^18",
16 | "react-json-view": "^1.21.3"
17 | },
18 | "devDependencies": {
19 | "@types/node": "^20",
20 | "@types/react": "^18",
21 | "@types/react-dom": "^18",
22 | "autoprefixer": "^10",
23 | "eslint": "^8",
24 | "eslint-config-next": "13.5.7",
25 | "postcss": "^8",
26 | "tailwindcss": "^3",
27 | "typescript": "^5"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 gabriel-pineda
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Git Wrapped API
2 |
3 | Git Wrapped is the API that powers [git-wrapped.com](https://git-wrapped.com) (Github Wrapped), a service that generates beautiful visualizations of your GitHub activity.
4 |
5 | 
6 |
7 | ## 🚀 Quick Start
8 |
9 | ### Prerequisites
10 |
11 | 1. Node.js and npm/yarn
12 | 2. GitHub Personal Access Token (Classic)
13 | - [How to create a GitHub Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
14 | - Required permissions: `public_repo`, `read:user`
15 |
16 | ### Environment Setup
17 |
18 | Create a `.env.local` file in the root directory with:
19 |
20 | ```bash
21 | GITHUB_TOKEN=ghp_
22 | ```
23 |
24 | ### Installation
25 |
26 | ```bash
27 | # Using yarn
28 | yarn install
29 |
30 | # Using npm
31 | npm install
32 | ```
33 |
34 | ### Development
35 |
36 | Run the development server:
37 |
38 | ```bash
39 | # Using yarn
40 | yarn dev
41 |
42 | # Using npm
43 | npm run dev
44 | ```
45 |
46 | The application will be available at [http://localhost:3000](http://localhost:3000).
47 |
48 | ## 🛠 Technical Stack
49 |
50 | - Next.js 13
51 | - TypeScript
52 | - Main API file: `api/stats.ts`
53 |
54 | ## 🤝 Contributing
55 |
56 | We welcome contributions from the community! Here's how you can help:
57 |
58 | 1. **Open an Issue**
59 |
60 | - Found a bug? Have a feature request? Open an issue describing what you'd like to change.
61 |
62 | 2. **Submit a Pull Request**
63 | - Fork the repository
64 | - Create a new branch for your feature
65 | - Make your changes
66 | - Submit a PR with:
67 | - Clear description of the changes
68 | - Link to the related issue
69 | - Sample result/screenshot if applicable
70 | - Wait for review
71 |
72 | ## 📝 License
73 |
74 | The code in this repository is licensed under the MIT License.
75 |
76 | ---
77 |
78 |
79 | Additional Next.js Resources
80 |
81 | - [Next.js Documentation](https://nextjs.org/docs)
82 | - [Learn Next.js](https://nextjs.org/learn)
83 | - [Next.js GitHub Repository](https://github.com/vercel/next.js/)
84 | - [Next.js Deployment Documentation](https://nextjs.org/docs/deployment)
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/pages/api/stats.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { Octokit } from "@octokit/rest";
3 |
4 | export const runtime = "edge";
5 |
6 | const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
7 | if (!GITHUB_TOKEN) {
8 | throw new Error("Missing GITHUB_TOKEN environment variable");
9 | }
10 |
11 | const octokit = new Octokit({
12 | auth: GITHUB_TOKEN,
13 | });
14 |
15 | interface ContributionDay {
16 | contributionCount: number;
17 | date: string;
18 | weekday: number;
19 | }
20 |
21 | interface GitHubStats {
22 | longestStreak: number;
23 | totalCommits: number;
24 | commitRank: string;
25 | calendarData: ContributionDay[];
26 | mostActiveDay: {
27 | name: string;
28 | commits: number;
29 | };
30 | mostActiveMonth: {
31 | name: string;
32 | commits: number;
33 | };
34 | starsEarned: number;
35 | topLanguages: string[];
36 | }
37 |
38 | /**
39 | * Determines the user's commit rank based on their total number of contributions
40 | * These thresholds are approximations based on general GitHub activity patterns
41 | */
42 | function getCommitRank(totalCommits: number): string {
43 | if (totalCommits >= 5000) return "Top 0.5%-1%";
44 | if (totalCommits >= 2000) return "Top 1%-3%";
45 | if (totalCommits >= 1000) return "Top 5%-10%";
46 | if (totalCommits >= 500) return "Top 10%-15%";
47 | if (totalCommits >= 200) return "Top 25%-30%";
48 | if (totalCommits >= 50) return "Median 50%";
49 | return "Bottom 30%";
50 | }
51 |
52 | // Constants for date formatting
53 | const WEEKDAY_NAMES = [
54 | "Sunday",
55 | "Monday",
56 | "Tuesday",
57 | "Wednesday",
58 | "Thursday",
59 | "Friday",
60 | "Saturday",
61 | ] as const;
62 |
63 | const MONTH_NAMES = [
64 | "January",
65 | "February",
66 | "March",
67 | "April",
68 | "May",
69 | "June",
70 | "July",
71 | "August",
72 | "September",
73 | "October",
74 | "November",
75 | "December",
76 | ] as const;
77 |
78 | /**
79 | * GitHub Stats API endpoint
80 | * Fetches and processes a user's GitHub statistics including:
81 | * - Contribution data
82 | * - Commit patterns
83 | * - Repository stars
84 | * - Programming languages
85 | *
86 | * @param request - Incoming HTTP request with 'username' query parameter
87 | * @returns JSON response with processed GitHub statistics
88 | */
89 | export default async function GET(request: Request): Promise {
90 | try {
91 | // Extract username from query parameters
92 | const { searchParams } = new URL(request.url);
93 | const username = searchParams.get("username");
94 |
95 | if (!username) {
96 | return NextResponse.json(
97 | { error: "Username parameter is required" },
98 | { status: 400 }
99 | );
100 | }
101 |
102 | // GraphQL query to fetch user's GitHub data
103 | const query = `
104 | query($username: String!) {
105 | user(login: $username) {
106 | contributionsCollection {
107 | contributionCalendar {
108 | totalContributions
109 | weeks {
110 | contributionDays {
111 | contributionCount
112 | date
113 | weekday
114 | }
115 | }
116 | }
117 | }
118 | repositories(first: 100, orderBy: {field: STARGAZERS, direction: DESC}) {
119 | nodes {
120 | stargazerCount
121 | primaryLanguage {
122 | name
123 | }
124 | }
125 | }
126 | }
127 | }
128 | `;
129 |
130 | const graphqlResponse = (await octokit.graphql(query, { username })) as any;
131 | const userData = graphqlResponse.user;
132 |
133 | // Process contribution data for the current year
134 | const contributionDays =
135 | userData.contributionsCollection.contributionCalendar.weeks
136 | .flatMap((week: any) => week.contributionDays)
137 | .filter((day: any) => new Date(day.date) >= new Date("2024-01-01"));
138 |
139 | // Calculate monthly contribution statistics
140 | const monthlyCommits: Record = {};
141 | contributionDays.forEach((day: ContributionDay) => {
142 | const month = new Date(day.date).getMonth() + 1;
143 | const monthKey = month.toString().padStart(2, "0");
144 | monthlyCommits[monthKey] =
145 | (monthlyCommits[monthKey] || 0) + day.contributionCount;
146 | });
147 |
148 | // Calculate daily contribution patterns
149 | const dailyCommits: Record = {};
150 | contributionDays.forEach((day: ContributionDay) => {
151 | dailyCommits[day.weekday] =
152 | (dailyCommits[day.weekday] || 0) + day.contributionCount;
153 | });
154 |
155 | // Find peak activity periods
156 | const [mostActiveMonth] = Object.entries(monthlyCommits).sort(
157 | ([, a], [, b]) => b - a
158 | );
159 |
160 | const [mostActiveDay] = Object.entries(dailyCommits).sort(
161 | ([, a], [, b]) => b - a
162 | );
163 |
164 | // Calculate repository statistics
165 | const totalStars = userData.repositories.nodes.reduce(
166 | (acc: number, repo: any) => acc + repo.stargazerCount,
167 | 0
168 | );
169 |
170 | // Process programming language statistics
171 | const languages = userData.repositories.nodes.reduce(
172 | (acc: Record, repo: any) => {
173 | if (repo.primaryLanguage?.name) {
174 | acc[repo.primaryLanguage.name] =
175 | (acc[repo.primaryLanguage.name] || 0) + 1;
176 | }
177 | return acc;
178 | },
179 | {}
180 | );
181 |
182 | const topLanguages = Object.entries(languages)
183 | .sort(([, a], [, b]): number => (b as number) - (a as number))
184 | .slice(0, 3)
185 | .map(([lang]) => lang);
186 |
187 | // Calculate contribution streaks
188 | let currentStreak = 0;
189 | let maxStreak = 0;
190 | for (const day of contributionDays) {
191 | if (day.contributionCount > 0) {
192 | currentStreak++;
193 | maxStreak = Math.max(maxStreak, currentStreak);
194 | } else {
195 | currentStreak = 0;
196 | }
197 | }
198 |
199 | const totalCommits =
200 | userData.contributionsCollection.contributionCalendar.totalContributions;
201 |
202 | // Prepare and return the final statistics
203 | const stats: GitHubStats = {
204 | longestStreak: maxStreak,
205 | totalCommits,
206 | commitRank: getCommitRank(totalCommits),
207 | calendarData: contributionDays,
208 | mostActiveDay: {
209 | name: WEEKDAY_NAMES[parseInt(mostActiveDay[0])],
210 | commits: Math.round(mostActiveDay[1] / (contributionDays.length / 7)), // Average per day
211 | },
212 | mostActiveMonth: {
213 | name: MONTH_NAMES[parseInt(mostActiveMonth[0]) - 1],
214 | commits: mostActiveMonth[1],
215 | },
216 | starsEarned: totalStars,
217 | topLanguages,
218 | };
219 |
220 | return NextResponse.json(stats);
221 | } catch (error: any) {
222 | console.error("Error fetching GitHub stats:", error);
223 | return NextResponse.json(
224 | { error: error.message || "Failed to fetch GitHub statistics" },
225 | { status: 500 }
226 | );
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { Inter } from "next/font/google";
3 | import { useState } from "react";
4 | import dynamic from "next/dynamic";
5 |
6 | const ReactJson = dynamic(() => import("react-json-view"), { ssr: false });
7 | const inter = Inter({ subsets: ["latin"] });
8 |
9 | export default function Home() {
10 | const [username, setUsername] = useState("");
11 | const [stats, setStats] = useState(null);
12 | const [loading, setLoading] = useState(false);
13 | const [error, setError] = useState("");
14 |
15 | const handleSubmit = async (e: React.FormEvent) => {
16 | e.preventDefault();
17 | setLoading(true);
18 | setError("");
19 | setStats(null);
20 |
21 | try {
22 | const response = await fetch(`/api/stats?username=${username}`);
23 | const data = await response.json();
24 |
25 | if (!response.ok) {
26 | throw new Error(data.error || "Failed to fetch stats");
27 | }
28 |
29 | setStats(data);
30 | } catch (err: any) {
31 | setError(err.message);
32 | } finally {
33 | setLoading(false);
34 | }
35 | };
36 |
37 | return (
38 |
41 |
77 |
78 | {/* Subtle Pattern Overlay */}
79 |
85 |
86 | {/* Header */}
87 |
99 |
100 | {/* Main Content */}
101 |
102 |
103 |
104 | {/* Left Column - Input Form */}
105 |
106 |
157 |
158 | {/* Error Message */}
159 | {error && (
160 |
161 | {error}
162 |
163 | )}
164 |
165 |
166 | {/* Right Column - Results */}
167 |
168 | {!stats && !loading && (
169 |
170 |
171 |
187 |
188 |
189 | No Stats Found
190 |
191 |
192 | Enter a GitHub username to view API response
193 |
194 |
195 |
196 |
197 | )}
198 | {stats && (
199 |
200 |
201 |
202 | Results for @{username}
203 |
204 |
205 |
206 |
221 |
222 |
223 | )}
224 |
225 |
226 |
227 |
228 |
229 | );
230 | }
231 |
--------------------------------------------------------------------------------