├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── app ├── Container.tsx ├── Error.tsx ├── favicon.ico ├── globals.css ├── home │ ├── Builder.tsx │ ├── Card.tsx │ ├── Config.tsx │ ├── Header.tsx │ ├── Hero.tsx │ └── Themes.tsx ├── layout.tsx ├── page.tsx ├── recent │ ├── Recents.tsx │ └── route.tsx ├── repo │ ├── Repo.tsx │ └── route.tsx ├── top │ ├── Bar.tsx │ ├── Compact.tsx │ ├── Normal.tsx │ └── route.tsx ├── user │ ├── User.tsx │ └── route.tsx └── wakatime │ ├── Bar.tsx │ ├── Compact.tsx │ ├── Normal.tsx │ └── route.tsx ├── assets ├── Urbanist-Bold.ttf ├── Urbanist-SemiBold.ttf └── gitmystat.png ├── bun.lockb ├── example.env ├── helpers ├── animate.ts ├── calculateLanguage.ts ├── calculateRank.ts ├── emoji.ts ├── formatNum.ts ├── generateSvg.ts ├── getColor.ts ├── getData.ts └── send.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── providers └── DeviceProvider.tsx ├── public ├── banner.png ├── fine.jpg └── gitmystat.png ├── tailwind.config.ts ├── themes └── index.ts ├── tsconfig.json ├── types ├── AnimateOptions.ts ├── Error.ts ├── Languages.ts ├── Recents.ts ├── Repo.ts ├── Theme.ts ├── UserStats.ts └── Waka.ts └── utils ├── colors.ts ├── languages.ts ├── repo.ts ├── repositories.ts ├── users.ts └── wakatime.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "always", 13 | "rangeStart": 0, 14 | "requirePragma": false, 15 | "insertPragma": false, 16 | "proseWrap": "preserve", 17 | "htmlWhitespaceSensitivity": "css", 18 | "vueIndentScriptAndStyle": false, 19 | "endOfLine": "auto", 20 | "embeddedLanguageFormatting": "auto", 21 | "plugins": ["prettier-plugin-tailwindcss"] 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![gitmystat](/assets/gitmystat.png) 2 | 3 | # gitMyStat! 4 | 5 | ~~Get~~ Git my stat! 🎨 Turn your GitHub activity into sleek stats and cool visuals 6 | 7 | > Inspired by a great project [github-readme-stats](https://github.com/anuraghazra/github-readme-stats/) 8 | 9 | > [!TIP] 10 | > Lazy to read alot just for readme? Me too! 11 | > Thats why I made a [Builder](https://gitmystat.vercel.app#builder) which generates the cards for your parameters. Give it a try! 12 | 13 | ## Table of Contents 14 | 15 | - [Parameters](#parameters) 16 | 17 | - [Cards](https://gitmystat.vercel.app#config) 18 | - [User](#user) 19 | - [Recent repos](#recent-repo) 20 | - [Repo card](#repo-card) 21 | - [Top languages](#top-languages) 22 | - [Wakatime](#wakatime) 23 | 24 | - [Themes](https://gitmystat.vercel.app#themes) 25 | 26 | --- 27 | 28 | ## Parameters 29 | 30 | All cards are built equally. Which means these apply to every card possible 31 | 32 | | Parameters | Description | Default | 33 | | ---------- | --------------------------------- | ------- | 34 | | `username` | The username to get the data from | | 35 | | `theme` | The theme to apply on the cards | dark | 36 | 37 | ### Customizations 38 | 39 | > [!IMPORTANT] 40 | > As url encoding messes up the # tag character, we wanted you to provide a hexadecimal code instead. 41 | > 42 | > ### `What's that?!` 43 | > 44 | > Nothing actually. instead of `#` we use `0x`. 45 | > 46 | > For example 47 | > 48 | > `#000000` is the same as `0x000000` > `#075fff` is the same as `0x075fff` 49 | 50 | | Parameters | Description | 51 | | ------------ | -------------------------------------------------- | 52 | | `color` | The hexadecimal color of the text color | 53 | | `accent` | The hexadecimal color of the accent color | 54 | | `background` | The hexadecimal color of the background | 55 | | `border` | The hexadecimal color of the border | 56 | | `tip` | The hexadecimal color of the tertiary (tips) color | 57 | | `radius` | The radius of the border | 58 | | `padding` | The padding of the border | 59 | 60 | - Totally decked out example 61 | 62 | ``` 63 | ![image](https://gitmystat.vercel.app/recent?username=rahuletto&color=0xaeaeae&accent=0x075fff&background=0x000000&border=0x075fff&tip=0x075fff&radius=12&padding=24) 64 | ``` 65 | 66 | ![image](https://gitmystat.vercel.app/recent?username=rahuletto&color=0xaeaeae&accent=0x075fff&background=0x000000&border=0x075fff&tip=0x075fff&radius=12&padding=24) 67 | 68 | > [!TIP] 69 | > 70 | > Adding `theme={theme}` in the url would change the theme of any card 71 | > 72 | > For example 73 | > 74 | > - `https://gitmystat.vercel.app/recent?username=rahuletto&theme=gold` 75 | > 76 | > ![recent theme](https://gitmystat.vercel.app/recent?username=rahuletto&theme=gold) 77 | 78 | --- 79 | 80 | ## Cards 81 | - [User](https://gitmystat.vercel.app#config) 82 | - [Recent repos](https://gitmystat.vercel.app#config) 83 | - [Repo card](https://gitmystat.vercel.app#config) 84 | - [Top languages](https://gitmystat.vercel.app#config) 85 | - [Wakatime](https://gitmystat.vercel.app#config) 86 | 87 | > [!WARNING] 88 | > For wakatime, We are using WakaTime api, which only displays for profiles that are public. Make sure the `Display code time publicly` and `Display languages, editors, os, categories publicly` are enabled. 89 | 90 | --- 91 | 92 | ## Themes 93 | Visit the [themes page](https://gitmystat.vercel.app#themes)! 94 | 95 | > #### You like a theme, but you just don't like that particular color? 96 | > 97 | > It's alright! the parameters can override particular colors of the theme while keeping the other colors the same from the theme. 98 | 99 | ### Extend it. 100 | 101 | These themes are not hard coded. You can add your own! 102 | You have a great color palette? **PLEASE** do a pull request on our repo and make it a reality. 103 | 104 | > [!NOTE] 105 | > If you are making a theme, please make a light mode version of it too. This is not mandatory to do, but it's a good thing to have more flavors. 106 | 107 | #### `How?` 108 | 109 | - Go to `/themes/index.ts` 110 | 111 | ```ts 112 | themename: { 113 | color: '#hex', 114 | accent: '#hex', 115 | background: '#hex', 116 | border: '#hex', 117 | tip: '#hex', 118 | radius: 24, 119 | padding: 24, 120 | }, 121 | ``` 122 | 123 | - Change the `themename` to the name you prefer 124 | - Change the color hex codes of the things, localhost would be great to test it 125 | - Push it, Do a pull request. 126 | 127 | It's that simple. 128 | -------------------------------------------------------------------------------- /app/Container.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeData } from "@/types/Theme"; 2 | import { ReactNode } from "react"; 3 | 4 | export default function Container({ 5 | children, 6 | theme, 7 | }: { 8 | children: ReactNode; 9 | theme: ThemeData; 10 | }) { 11 | return ( 12 |
19 | {children} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/Error.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeData } from "@/types/Theme"; 2 | 3 | export default function Error(theme: ThemeData, error: { message: string, code: string }) { 4 | return ( 5 |
12 |
13 |
17 | Error 18 |
19 |
20 | {error.message || "There is no error? Wait what"} 21 |
22 |
23 |
{error.code || "Uhm?"}
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/gitMyStat/095895765bea3409e36c721d790d491b8cfbdc39/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #fefefe; 7 | --color: #000000; 8 | --accent: #343941; 9 | --border: #C2C9D1; 10 | --tip:#D0D7DE; 11 | } 12 | 13 | @media (prefers-color-scheme: dark) { 14 | :root { 15 | --background: #0D1116; 16 | --color: #E6EDF3; 17 | --accent: #8D96A0; 18 | --border: #30363D; 19 | --tip: var(--border); 20 | } 21 | } 22 | 23 | body, html { 24 | background: radial-gradient(circle, var(--border), 0%, var(--background) 100%); 25 | color: var(--color); 26 | scroll-behavior: smooth; 27 | } 28 | 29 | * { 30 | transition: all 0.1s ease-out; 31 | -webkit-transition: all 0.1s ease-out; 32 | -moz-transition: all 0.1s ease-out; 33 | -ms-transition: all 0.1s ease-out; 34 | -o-transition: all 0.1s ease-out; 35 | } 36 | 37 | #header { 38 | -webkit-backdrop-filter: blur(10px) !important; 39 | backdrop-filter: blur(10px); 40 | } 41 | 42 | @layer utilities { 43 | .text-balance { 44 | text-wrap: balance; 45 | } 46 | } 47 | 48 | @keyframes fadeIn { 49 | 0% { 50 | opacity: 0; 51 | } 52 | 100% { 53 | opacity: 1; 54 | } 55 | } -------------------------------------------------------------------------------- /app/home/Builder.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import { Themes } from "@/themes"; 3 | import { ThemeType } from "@/types/Theme"; 4 | import { useEffect, useRef, useState } from "react"; 5 | import { HexColorInput, HexAlphaColorPicker } from "react-colorful"; 6 | 7 | export default function Builder() { 8 | const userInput = useRef(null); 9 | const [url, setUrl] = useState(""); 10 | const [theme, setTheme] = useState(""); 11 | const [layout, setLayout] = useState("default"); 12 | const [username, setUsername] = useState(""); 13 | const [repo, setRepo] = useState(""); 14 | const [card, setCard] = useState(""); 15 | const [themeColors, setThemeColors] = useState(Themes["dark"]); 16 | const [error, setError] = useState(0); 17 | 18 | const handleColorChange = (key: keyof typeof themeColors, value: string) => { 19 | setThemeColors((prevThemeColors) => ({ 20 | ...prevThemeColors, 21 | [key]: value, 22 | })); 23 | }; 24 | 25 | const renderColorPickers = () => { 26 | if (themeColors) 27 | return Object.keys(themeColors).map((key) => 28 | key == "radius" || key == "padding" ? ( 29 |
33 | 34 | 38 | handleColorChange( 39 | key as keyof typeof themeColors, 40 | e.target.value 41 | ) 42 | } 43 | className="max-w-[120px] appearance-none rounded-xl border-paper-border px-4 py-2 text-lg text-paper-color dark:bg-moonlight-background dark:text-moonlight-accent" 44 | /> 45 |
46 | ) : ( 47 |
51 | 52 |
53 | { 56 | handleColorChange(key as keyof typeof themeColors, e); 57 | }} 58 | /> 59 | { 63 | handleColorChange(key as keyof typeof themeColors, e); 64 | }} 65 | /> 66 |
67 |
68 | ) 69 | ); 70 | }; 71 | 72 | useEffect(() => { 73 | if (theme) { 74 | setThemeColors(Themes[theme]); 75 | } 76 | }, [theme]); 77 | 78 | useEffect(() => { 79 | setUrl(""); 80 | }, [card]); 81 | 82 | function generate() { 83 | setError(0); 84 | if (username) { 85 | setUrl( 86 | generateURL({ 87 | card, 88 | theme, 89 | username, 90 | repo, 91 | layout: card == "top" || card == "wakatime" ? layout : "", 92 | custom: themeColors as any, 93 | }) 94 | ); 95 | } else { 96 | userInput.current?.focus(); 97 | setError(1); 98 | } 99 | } 100 | 101 | const btnStyle = 102 | "active:scale-90 hover:scale-105 rounded-full lg:py-4 lg:px-12 py-2 px-2 lg:text-xl text-lg lg:font-semibold font-medium hover:dark:bg-moonlight-background hover:dark:text-moonlight-color hover:bg-paper-accent hover:text-paper-color"; 103 | return ( 104 |
109 |
110 |

Builder

111 |

Build your card without messing with the url

112 |
113 | 114 |
115 | 121 | 127 | 133 | 139 | 145 |
146 | 147 | {card ? ( 148 |
151 |
152 |

153 | {card.charAt(0).toUpperCase() + card.slice(1)} 154 |

155 |
156 | Parameters 157 |
158 |
159 | 160 | 175 |
176 |
177 | 178 | setUsername(e.target.value)} 183 | className={`${error == 1 ? "border-4 border-red-400" : ""} appearance-none rounded-xl border-paper-border px-4 py-2 text-lg text-paper-color dark:bg-moonlight-background dark:text-moonlight-accent`} 184 | /> 185 |
186 | {card == "top" || 187 | (card == "wakatime" && ( 188 |
189 | 190 | 205 |
206 | ))} 207 | 208 | {card === "repo" && ( 209 |
210 | 211 | setRepo(e.target.value)} 215 | className="appearance-none rounded-xl border-paper-border px-4 py-2 text-lg text-paper-color dark:bg-moonlight-background dark:text-moonlight-accent" 216 | /> 217 |
218 | )} 219 |
220 |
221 | 222 |
223 | Customization 224 |
225 | {renderColorPickers()} 226 |
227 |
228 | 229 | 235 |
236 | 237 | {url && ( 238 |
239 | {card} 240 | 241 | https://gitmystat.vercel.app{url} 242 | 243 |
244 | )} 245 |
246 | ) : ( 247 |

248 | Select a type 249 |

250 | )} 251 |
252 | ); 253 | } 254 | 255 | function generateURL({ 256 | card, 257 | theme, 258 | username, 259 | repo, 260 | layout, 261 | custom, 262 | }: { 263 | card: string; 264 | theme: string; 265 | username: string; 266 | repo?: string; 267 | layout?: string; 268 | custom: ThemeType; 269 | }) { 270 | const params = new URLSearchParams(); 271 | 272 | params.append("theme", theme); 273 | params.append("username", username); 274 | 275 | if (card === "repo" && repo) { 276 | params.append("repo", repo); 277 | } 278 | 279 | if (layout) { 280 | params.append("layout", layout); 281 | } 282 | 283 | const defaultTheme = Themes[theme || "dark"]; 284 | 285 | Object.keys(defaultTheme).forEach((key) => { 286 | if (custom[key] !== (defaultTheme as any)[key]) { 287 | params.append(key, (custom as any)[key]); 288 | } 289 | }); 290 | 291 | return `/${card}?${params.toString().replaceAll("%23", "0x").replaceAll("#", "0x")}`; 292 | } 293 | -------------------------------------------------------------------------------- /app/home/Card.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { Themes } from "@/themes"; 3 | 4 | export default function Card({ 5 | theme = "dark", 6 | url = "/repo?username=rahuletto&repo=gitMyStat", 7 | text, 8 | width, 9 | }: { 10 | theme?: string; 11 | url?: string; 12 | text?: string; 13 | width?: number; 14 | }) { 15 | return ( 16 |
17 | 18 | 22 | GitHub 30 | 31 | 39 | {text 40 | ? text.split(new RegExp(`(rahuletto|gitmystat)`, "gi")).map((part, index) => 41 | ['rahuletto', 'gitmystat'].includes(part.toLowerCase()) ? ( 42 | 43 | {part} 44 | 45 | ) : ( 46 | part 47 | ) 48 | ) 49 | : theme} 50 | 51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/home/Config.tsx: -------------------------------------------------------------------------------- 1 | import { useDevice } from "@/providers/DeviceProvider"; 2 | import dynamic from "next/dynamic"; 3 | 4 | const Card = dynamic(() => import("./Card"), { 5 | ssr: false, 6 | }); 7 | 8 | export default function Config() { 9 | const device = useDevice() 10 | return ( 11 | <> 12 |
17 |

Config

18 |
19 |

20 | Base URL:{" "} 21 | 22 | https://gitmystat.vercel.app/ 23 | 24 |

25 |
26 |
27 |

Cards

28 |

29 | These are the list of card component to display your GitHub profile, 30 | repository, and more. The card component is a dynamic component that 31 | can be configured. 32 |

33 |
34 | 35 |
36 |

User

37 |
42 | 47 |
48 |
49 | 50 |
51 |

Recent

52 |
57 | 62 |
63 |
64 | 65 |
66 |

Repo

67 |
72 | 77 |
78 |
79 | 80 |
81 |

Top Languages

82 |
89 | 94 | 99 | 104 |
105 |
106 | 107 |
108 |

Wakatime

109 |
116 | 121 | 126 | 131 |
132 |
133 |
134 | 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /app/home/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export default function Header() { 4 | return ( 5 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/home/Hero.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Image from 'next/image' 3 | import Link from 'next/link' 4 | 5 | export default function Hero() { 6 | return ( 7 | <> 8 |
13 | Logo 14 |

gitMyStat!

15 |

16 | Turn your GitHub activity into sleek stats and cool visuals 17 |

18 |
19 |
24 |
25 |
26 |

27 | Turn your stats into eyecandy 28 |

29 |

30 | gitMyStat! is a helpful project that generates an image svg with 31 | your GitHub stats and activity in a sleek and modern way. 32 |

33 |
34 | 35 | 39 | Top languages 46 | 47 |
48 | 49 |
50 |
51 |

52 | Modernized 53 |

54 |

55 | My mission is to modernize the README visuals of github with 56 | stunning statistics of other{"'"}s work and progress. 57 |

58 |
59 | 60 | 64 | User 70 | 71 |
72 |
73 |
77 |
78 |
79 |

80 | Awesome flavors 81 |

82 |

83 | Give a little touch of your personal taste to the stats with the 84 | themes available. Heavily inspired from{" "} 85 | 89 | MonkeyType 90 | {" "} 91 | themes. But it doesn{"'"}t stop there, you can customize 92 | everything for your liking. 93 |

94 |
95 | 96 | 100 | Recent 107 | 108 |
109 |
110 | 111 |
115 |
116 |
117 |

118 | Soothing Animations 119 |

120 |

121 | What{"'"}s better than a static sticker? An animated one! With 122 | lazy-loading, you can acheive an animated sticker with ease. 123 |

124 |
125 | 126 | 130 | Recent 137 | 138 |
139 | 140 |
141 |
142 |

143 | Well optimized 144 |

145 |

146 | Using edge networks, the stats are cached for a longer period of 147 | 1 hour, with benefits of lower latency and improved performance. 148 |

149 |
150 | 151 | 155 | Top compact 162 | 163 |
164 |
165 |
169 |
170 |
171 |

172 | AAAAAAAAA *crash* 173 |

174 |

175 | Imagine, what if these stats are crutial for your README? I got 176 | you covered with amazing error handling, which tells you what 177 | {"'"}s wrong instead of{" "} 178 | 179 | silence 180 | 181 | . 182 |

183 |
184 | 185 | 189 | Error 196 | 197 |
198 | This is fine 199 |
200 | 201 |
205 |
206 |
207 |

208 | Open Source 209 |

210 |

211 | gitMyStat! is an open-source project that is free to use and 212 | contribute to. Built with Next.js and TailwindCSS, with safety 213 | of Typescript. 214 |

215 |
216 | 217 | 221 | GitHub 228 | 229 |
230 |
231 | 232 | 233 | ) 234 | } 235 | -------------------------------------------------------------------------------- /app/home/Themes.tsx: -------------------------------------------------------------------------------- 1 | import { useDevice } from '@/providers/DeviceProvider'; 2 | import { Themes } from '@/themes'; 3 | import dynamic from 'next/dynamic'; 4 | 5 | const Card = dynamic(() => import('./Card'), { 6 | ssr: false, 7 | }); 8 | 9 | 10 | export default function Theme() { 11 | const device = useDevice(); 12 | return ( 13 |
18 |

Themes

19 |
26 | {Object.keys(Themes).map((theme, i) => { 27 | return ( 28 | 29 | ); 30 | })} 31 |
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next"; 2 | import { Urbanist } from "next/font/google"; 3 | import "./globals.css"; 4 | import { DeviceProvider } from "@/providers/DeviceProvider"; 5 | 6 | const urbanist = Urbanist({ 7 | subsets: ["latin"], 8 | weight: ["500", "700"], 9 | display: "swap", 10 | variable: "--urbanist", 11 | }); 12 | 13 | export const viewport: Viewport = { 14 | themeColor: "#0D1116", 15 | colorScheme: "dark", 16 | }; 17 | 18 | export const metadata: Metadata = { 19 | title: "gitMyStat!", 20 | description: "Turn your GitHub activity into sleek stats and cool visuals", 21 | icons: { 22 | icon: "/favicon.ico", 23 | }, 24 | category: "tool", 25 | openGraph: { 26 | siteName: "gitMyStat!", 27 | type: "website", 28 | locale: "en_US", 29 | images: [ 30 | { 31 | url: "/banner.png", 32 | alt: "gitMyStat!", 33 | }, 34 | ], 35 | }, 36 | twitter: { 37 | card: "summary_large_image", 38 | images: [ 39 | { 40 | url: "/banner.png", 41 | alt: "gitMyStat!", 42 | }, 43 | ], 44 | }, 45 | }; 46 | 47 | export default function RootLayout({ 48 | children, 49 | }: Readonly<{ 50 | children: React.ReactNode; 51 | }>) { 52 | return ( 53 | 54 | 55 | {children} 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | "use client"; 3 | 4 | import Hero from "./home/Hero"; 5 | import Header from "./home/Header"; 6 | import dynamic from "next/dynamic"; 7 | import Builder from "./home/Builder"; 8 | 9 | const Config = dynamic(() => import("./home/Config"), { 10 | ssr: false, 11 | }); 12 | const Theme = dynamic(() => import("./home/Themes"), { 13 | ssr: false, 14 | }); 15 | 16 | export default function Home() { 17 | return ( 18 | <> 19 |
20 |
21 | 22 | 23 | 24 | 25 |
26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/recent/Recents.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeData } from "@/types/Theme"; 2 | import { RecentData } from "@/types/Recents"; 3 | import { GoRepo } from "react-icons/go"; 4 | 5 | export default function Recents(data: RecentData, theme: ThemeData) { 6 | return ( 7 |
14 |

15 | @{theme.user} has recently worked on 16 |

17 |
18 | 19 |

{data.name}

20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/recent/route.tsx: -------------------------------------------------------------------------------- 1 | import RepoList from "@/utils/repositories"; 2 | import Recents from "./Recents"; 3 | import generateSvg from "@/helpers/generateSvg"; 4 | import Send from "@/helpers/send"; 5 | import { getData } from "@/helpers/getData"; 6 | import { ThemeData } from "@/types/Theme"; 7 | import Error from "../Error"; 8 | 9 | // /recent?username=rahuletto 10 | export async function GET(request: Request) { 11 | const { searchParams } = new URL(request.url); 12 | const { user, color, accent, background, border, radius, padding } = 13 | getData(searchParams); 14 | 15 | const theme: ThemeData = { 16 | user: user ?? "rahuletto", 17 | color: color ?? "#E6EDF3", 18 | accent: accent ?? "#8D96A0", 19 | background: background ?? "#0D1116", 20 | border: border ?? "#30363D", 21 | radius: radius ?? 24, 22 | padding: padding ?? 24, 23 | }; 24 | try { 25 | const rawdata = await RepoList(user || "rahuletto"); 26 | 27 | if(!rawdata.data.user?.repositories) { 28 | const image = await generateSvg( 29 | Error(theme, { 30 | message: rawdata.errors 31 | ? rawdata.errors[0]?.message 32 | : `There are user with username "${user}"`, 33 | code: rawdata.errors ? rawdata.errors[0]?.type : "NO_USER", 34 | }), 35 | { 36 | width: 500, 37 | height: 170, 38 | } 39 | ); 40 | 41 | return Send(image, {error: true}); 42 | } 43 | if ( 44 | rawdata.data.user.repositories.edges.length == 0 || 45 | (rawdata.errors && rawdata.errors[0]) 46 | ) { 47 | const image = await generateSvg( 48 | Error(theme, { 49 | message: rawdata.errors 50 | ? rawdata.errors[0]?.message 51 | : `There are no repositories for "${user}"`, 52 | code: rawdata.errors ? rawdata.errors[0]?.type : "NO_REPO", 53 | }), 54 | { 55 | width: 500, 56 | height: 170, 57 | } 58 | ); 59 | 60 | return Send(image, {error: true}); 61 | } 62 | 63 | const recent = { 64 | name: rawdata.data.user.repositories.edges[0].node.name as string, 65 | url: rawdata.data.user.repositories.edges[0].node.url as string, 66 | }; 67 | 68 | const image = await generateSvg(Recents(recent, theme), { 69 | width: 410, 70 | height: 110, 71 | }); 72 | 73 | return Send(image); 74 | } catch (err: any) { 75 | console.warn(err); 76 | const image = await generateSvg( 77 | Error(theme, { 78 | message: (err as Error).message, 79 | code: (err as Error).name, 80 | }), 81 | { 82 | width: 500, 83 | height: 170, 84 | } 85 | ); 86 | 87 | return Send(image, {error: true}); 88 | } 89 | } 90 | 91 | export const runtime = "edge"; 92 | -------------------------------------------------------------------------------- /app/repo/Repo.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeData } from "@/types/Theme"; 2 | import { Repo } from "@/types/Repo"; 3 | import { GoRepo, GoRepoForked, GoStar } from "react-icons/go"; 4 | import Container from "../Container"; 5 | 6 | export default function RepoComp(data: Repo, theme: ThemeData) { 7 | const sliced = data.description.slice(0, 110); 8 | return ( 9 | 10 |
11 |
12 | 13 |
14 | {data.name} 15 |
16 |
17 |
21 | {sliced.length === data.description.length 22 | ? data.description 23 | : sliced + "..."} 24 |
25 |
26 | 27 |
28 |
29 |
30 |
33 | 34 | {data.primaryLanguage.name} 35 | 36 |
37 |
38 | 39 |
{data.stargazerCount}
40 |
41 | 42 |
43 | 44 |
{data.forkCount}
45 |
46 |
47 |
48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /app/repo/route.tsx: -------------------------------------------------------------------------------- 1 | import generateSvg from "@/helpers/generateSvg"; 2 | import Send from "@/helpers/send"; 3 | import { getData } from "@/helpers/getData"; 4 | import RepoComp from "./Repo"; 5 | import { ThemeData } from "@/types/Theme"; 6 | import Repository from "@/utils/repo"; 7 | import { Repo } from "@/types/Repo"; 8 | import Error from "../Error"; 9 | 10 | // /repo?user=rahuletto&repo=AcademiaPro 11 | export async function GET(request: Request) { 12 | const { searchParams } = new URL(request.url); 13 | 14 | const { user, color, accent, background, border, radius, padding, tip } = 15 | getData(searchParams); 16 | 17 | const theme: ThemeData = { 18 | user: user ?? "rahuletto", 19 | color: color ?? "#E6EDF3", 20 | accent: accent ?? "#8D96A0", 21 | background: background ?? "#0D1116", 22 | border: border ?? "#30363D", 23 | radius: radius ?? 24, 24 | padding: padding ?? 24, 25 | tip: tip ?? "#F6C655", 26 | }; 27 | 28 | let repo = searchParams.get("repo"); 29 | if (!repo) { 30 | const image = await generateSvg( 31 | Error(theme, { 32 | message: "You didn't provide a repository name", 33 | code: "MISSING_FIELD", 34 | }), 35 | { 36 | width: 500, 37 | height: 170, 38 | } 39 | ); 40 | 41 | return Send(image, {error: true}); 42 | } 43 | 44 | try { 45 | const rawdata = await Repository( 46 | user || "rahuletto", 47 | repo || "AcademiaPro" 48 | ); 49 | 50 | if (rawdata.errors && rawdata.errors[0]) { 51 | const image = await generateSvg( 52 | Error(theme, { 53 | message: rawdata.errors[0].message, 54 | code: rawdata.errors[0].type, 55 | }), 56 | { 57 | width: 500, 58 | height: 170, 59 | } 60 | ); 61 | 62 | return Send(image, {error: true}); 63 | } 64 | 65 | const data: Repo = { 66 | name: rawdata.data.repositoryOwner.repository.name, 67 | description: rawdata.data.repositoryOwner.repository.description, 68 | primaryLanguage: rawdata.data.repositoryOwner.repository.primaryLanguage, 69 | stargazerCount: rawdata.data.repositoryOwner.repository.stargazerCount, 70 | forkCount: rawdata.data.repositoryOwner.repository.forkCount, 71 | }; 72 | 73 | const image = await generateSvg(RepoComp(data, theme), { 74 | width: 500, 75 | height: 170, 76 | }); 77 | 78 | return Send(image); 79 | } catch (err: any) { 80 | console.warn(err); 81 | 82 | const image = await generateSvg( 83 | Error(theme, { 84 | message: (err as Error).message, 85 | code: (err as Error).name, 86 | }), 87 | { 88 | width: 500, 89 | height: 170, 90 | } 91 | ); 92 | 93 | return Send(image, {error: true}); 94 | } 95 | } 96 | 97 | export const runtime = "edge"; 98 | -------------------------------------------------------------------------------- /app/top/Bar.tsx: -------------------------------------------------------------------------------- 1 | import { LanguageStat } from "@/types/Languages"; 2 | import { ThemeData } from "@/types/Theme"; 3 | import Container from "../Container"; 4 | 5 | export default function BarTop(data: LanguageStat[], theme: ThemeData) { 6 | return ( 7 | 8 |
9 | Most used languages 10 |
11 | 12 |
13 | {data.map((language, index) => ( 14 |
23 | ))} 24 |
25 |
26 | {data.slice(0, 6).map((language) => ( 27 | 34 | ))} 35 |
36 | 37 | ); 38 | } 39 | 40 | function Percent({ 41 | language, 42 | percent, 43 | color, 44 | theme, 45 | }: { 46 | language: string; 47 | percent: number; 48 | color: string; 49 | theme: ThemeData; 50 | }) { 51 | return ( 52 |
53 |
54 |
55 | {language} 56 |
57 |
{percent.toFixed(2)}%
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /app/top/Compact.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeData } from "@/types/Theme"; 2 | import { LanguageStat } from "@/types/Languages"; 3 | import Container from "../Container"; 4 | 5 | export default function CompactTop(data: LanguageStat[], theme: ThemeData) { 6 | return ( 7 | 8 |
9 | @{theme.user} has worked alot with 10 |
11 |
12 |
15 | {data[0].name} 16 |
17 |
18 | {`(${data[0].percent}%)`} 19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/top/Normal.tsx: -------------------------------------------------------------------------------- 1 | import { LanguageStat } from "@/types/Languages"; 2 | import { ThemeData } from "@/types/Theme"; 3 | import Container from "../Container"; 4 | 5 | export default function NormalTop(data: LanguageStat[], theme: ThemeData) { 6 | return ( 7 | 8 |
9 | {data.slice(0, 6).map((language) => ( 10 | 17 | ))} 18 |
19 |
20 | ); 21 | } 22 | 23 | function Percent({ 24 | language, 25 | percent, 26 | color, 27 | theme, 28 | }: { 29 | language: string; 30 | percent: number; 31 | color: string; 32 | theme: ThemeData; 33 | }) { 34 | return ( 35 |
36 |
37 |
38 | 39 | {language} 40 | 41 |
42 | {percent.toFixed(2)}% 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/top/route.tsx: -------------------------------------------------------------------------------- 1 | import Error from "../Error"; 2 | import generateSvg from "@/helpers/generateSvg"; 3 | import Send from "@/helpers/send"; 4 | import { getData } from "@/helpers/getData"; 5 | import NormalTop from "./Normal"; 6 | import LangData from "@/utils/languages"; 7 | import { ThemeData } from "@/types/Theme"; 8 | import BarTop from "./Bar"; 9 | import calculateLanguageStats from "@/helpers/calculateLanguage"; 10 | import CompactTop from "./Compact"; 11 | 12 | // /top?username=rahuletto&layout=bar 13 | // /top?username=rahuletto&layout=normal 14 | // /top?username=rahuletto&layout=compact 15 | 16 | export async function GET(request: Request) { 17 | const { searchParams } = new URL(request.url); 18 | const { user, color, accent, background, border, radius, padding } = 19 | getData(searchParams); 20 | 21 | const theme: ThemeData = { 22 | user: user ?? "rahuletto", 23 | color: color ?? "#E6EDF3", 24 | accent: accent ?? "#8D96A0", 25 | background: background ?? "#0D1116", 26 | border: border ?? "#30363D", 27 | radius: radius ?? 24, 28 | padding: padding ?? 24, 29 | }; 30 | 31 | const hasLayout = searchParams.has("layout"); 32 | const layout = hasLayout ? searchParams.get("layout") : "normal"; 33 | 34 | try { 35 | const rawdata = await LangData(user || "rahuletto"); 36 | 37 | if ( 38 | rawdata.data.user.repositories.edges.length == 0 || 39 | (rawdata.errors && rawdata.errors[0]) 40 | ) { 41 | const image = await generateSvg( 42 | Error(theme, { 43 | message: rawdata.errors 44 | ? rawdata.errors[0]?.message 45 | : `There is no user with username "${user}"`, 46 | code: rawdata.errors ? rawdata.errors[0]?.type : "NOT_FOUND", 47 | }), 48 | { 49 | width: 500, 50 | height: 170, 51 | } 52 | ); 53 | 54 | return Send(image, {error: true}); 55 | } 56 | 57 | const result = calculateLanguageStats(rawdata); 58 | 59 | switch (layout) { 60 | case "bar": { 61 | const image = await generateSvg(BarTop(result, theme), { 62 | width: 300, 63 | height: 327, 64 | }); 65 | 66 | return Send(image, {delay:0.1, bar: true}); 67 | } 68 | case "compact": 69 | const image = await generateSvg(CompactTop(result, theme), { 70 | width: 480, 71 | height: 130, 72 | }); 73 | 74 | return Send(image); 75 | case "normal": 76 | default: { 77 | const image = await generateSvg(NormalTop(result, theme), { 78 | width: 300, 79 | height: 260, 80 | }); 81 | 82 | return Send(image, {delay: 0.1}); 83 | } 84 | } 85 | } catch (err: any) { 86 | console.warn(err); 87 | const image = await generateSvg( 88 | Error(theme, { 89 | message: (err as Error).message, 90 | code: (err as Error).name, 91 | }), 92 | { 93 | width: 500, 94 | height: 170, 95 | } 96 | ); 97 | 98 | return Send(image, {error: true}); 99 | } 100 | } 101 | 102 | export const runtime = "edge"; 103 | -------------------------------------------------------------------------------- /app/user/User.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeData } from "@/types/Theme"; 2 | 3 | import { UserStats } from "@/types/UserStats"; 4 | import Container from "../Container"; 5 | import { calculateRank } from "@/helpers/calculateRank"; 6 | import { GoCommit, GoGitPullRequest, GoIssueOpened, GoStar } from "react-icons/go"; 7 | import { formatNumber } from "@/helpers/formatNum"; 8 | 9 | export default function UserComp(data: UserStats, theme: ThemeData) { 10 | return ( 11 | 12 |
13 |
14 |
15 | {calculateRank(data)} 16 |
17 |
18 | @{theme.user} 19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 | Stars 27 |
28 |
29 |
30 | {formatNumber(data.stars)} 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 | Issues 39 |
40 |
41 |
42 | {formatNumber(data.issues)} 43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 | Commits 51 |
52 |
53 |
54 | {formatNumber(data.commits)} 55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 | Pull Requests 63 |
64 |
65 |
66 | {formatNumber(data.pullRequests)} 67 |
68 |
69 |
70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /app/user/route.tsx: -------------------------------------------------------------------------------- 1 | import generateSvg from "@/helpers/generateSvg"; 2 | import Send from "@/helpers/send"; 3 | import { getData } from "@/helpers/getData"; 4 | import { ThemeData } from "@/types/Theme"; 5 | import Error from "../Error"; 6 | import UserData from "@/utils/users"; 7 | import { parseGitHubData } from "@/helpers/calculateRank"; 8 | import UserComp from "./User"; 9 | 10 | // /user?user=rahuletto 11 | export async function GET(request: Request) { 12 | const { searchParams } = new URL(request.url); 13 | 14 | const { user, color, accent, background, border, radius, padding, tip } = 15 | getData(searchParams); 16 | 17 | const theme: ThemeData = { 18 | user: user ?? "rahuletto", 19 | color: color ?? "#E6EDF3", 20 | accent: accent ?? "#8D96A0", 21 | background: background ?? "#0D1116", 22 | border: border ?? "#30363D", 23 | radius: radius ?? 24, 24 | padding: padding ?? 24, 25 | tip: tip ?? "#F6C655", 26 | }; 27 | 28 | try { 29 | const rawdata = await UserData( 30 | user || "rahuletto" 31 | ); 32 | 33 | if (rawdata.errors && rawdata.errors[0]) { 34 | const image = await generateSvg( 35 | Error(theme, { 36 | message: rawdata.errors[0].message, 37 | code: rawdata.errors[0].type, 38 | }), 39 | { 40 | width: 500, 41 | height: 170, 42 | } 43 | ); 44 | 45 | return Send(image, {error: true}); 46 | } 47 | 48 | const data = parseGitHubData(rawdata) 49 | 50 | const image = await generateSvg(UserComp(data, theme), { 51 | width: 285, 52 | height: 340, 53 | }); 54 | 55 | return Send(image, {delay: 0.2}); 56 | } catch (err: any) { 57 | console.warn(err); 58 | 59 | const image = await generateSvg( 60 | Error(theme, { 61 | message: (err as Error).message, 62 | code: (err as Error).name, 63 | }), 64 | { 65 | width: 500, 66 | height: 170, 67 | } 68 | ); 69 | 70 | return Send(image, {error: true}); 71 | } 72 | } 73 | 74 | export const runtime = "edge"; 75 | -------------------------------------------------------------------------------- /app/wakatime/Bar.tsx: -------------------------------------------------------------------------------- 1 | import { generateColor } from "@/helpers/getColor"; 2 | import { ThemeData } from "@/types/Theme"; 3 | import { WakaData } from "@/types/Waka"; 4 | import { colors } from "@/utils/colors"; 5 | import Container from "../Container"; 6 | 7 | export default function BarWaka(data: WakaData, theme: ThemeData) { 8 | return ( 9 | 10 |
11 | Most used languages 12 |
13 | 14 |
15 | {data.languages.map((language, index) => ( 16 |
25 | ))} 26 |
27 |
28 | {data.languages.slice(0, 6).map((language) => ( 29 | 36 | ))} 37 |
38 |
39 | wakatime 40 |
41 | 42 | ); 43 | } 44 | 45 | function Percent({ 46 | language, 47 | percent, 48 | color, 49 | theme, 50 | }: { 51 | language: string; 52 | percent: number; 53 | color: string; 54 | theme: ThemeData; 55 | }) { 56 | return ( 57 |
58 |
59 |
60 | {language} 61 |
62 |
63 | {percent.toFixed(2)}% 64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /app/wakatime/Compact.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeData } from "@/types/Theme"; 2 | import { colors } from "@/utils/colors"; 3 | import { WakaData } from "@/types/Waka"; 4 | import Container from "../Container"; 5 | 6 | export default function CompactWaka(data: WakaData, theme: ThemeData) { 7 | return ( 8 | 9 |
10 | @{theme.user} has worked with 11 |
12 |
13 |
16 | {data.languages[0].name} 17 |
18 |
19 | for {data.languages[0].hours} hours 20 |
21 |
22 | 23 |
24 | wakatime 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/wakatime/Normal.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeData } from "@/types/Theme"; 2 | import { WakaData } from "@/types/Waka"; 3 | import Container from "../Container"; 4 | 5 | // /wakatime?username=rahuletto&layout=compact 6 | // /wakatime?username=rahuletto&layout=normal 7 | export default function NormalWaka(data: WakaData, theme: ThemeData) { 8 | return ( 9 | 10 |
11 | @{data.user} 12 |
13 |
14 | {data.languages.slice(0, 6).map((language) => ( 15 |
23 |
24 | wakatime 25 |
26 |
27 | ); 28 | } 29 | 30 | function Time({ 31 | language, 32 | seconds, 33 | theme, 34 | }: { 35 | language: string; 36 | seconds: number; 37 | theme: ThemeData; 38 | }) { 39 | const hours = Math.floor(seconds / 3600); 40 | const minutes = Math.floor((seconds % 3600) / 60); 41 | 42 | return ( 43 |
44 |
45 | {language} 46 |
47 | 48 | {hours}h {minutes}m 49 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/wakatime/route.tsx: -------------------------------------------------------------------------------- 1 | import generateSvg from "@/helpers/generateSvg"; 2 | import Send from "@/helpers/send"; 3 | import { getData } from "@/helpers/getData"; 4 | import CompactWaka from "./Compact"; 5 | import NormalWaka from "./Normal"; 6 | import { ThemeData } from "@/types/Theme"; 7 | import Error from "../Error"; 8 | import Wakatime from "@/utils/wakatime"; 9 | import { WakaData } from "@/types/Waka"; 10 | import BarWaka from "./Bar"; 11 | 12 | export async function GET(request: Request) { 13 | const { searchParams } = new URL(request.url); 14 | 15 | const hasLayout = searchParams.has("layout"); 16 | const layout = hasLayout ? searchParams.get("layout") : "normal"; 17 | 18 | const { user, color, accent, background, border, radius, padding, tip } = 19 | getData(searchParams); 20 | 21 | const theme: ThemeData = { 22 | user: user ?? "rahuletto", 23 | color: color ?? "#E6EDF3", 24 | accent: accent ?? "#8D96A0", 25 | background: background ?? "#0D1116", 26 | border: border ?? "#30363D", 27 | radius: radius ?? 24, 28 | padding: padding ?? 24, 29 | tip: tip ?? "#F6C655", 30 | }; 31 | 32 | try { 33 | const rawdata = await Wakatime(user || "rahuletto"); 34 | 35 | if (!rawdata || rawdata.error) { 36 | const image = await generateSvg( 37 | Error(theme, { 38 | message: rawdata?.error ?? "", 39 | code: "", 40 | }), 41 | { 42 | width: 500, 43 | height: 170, 44 | } 45 | ); 46 | 47 | return Send(image, {error: true}); 48 | } 49 | 50 | const data: WakaData = { 51 | user: rawdata.data.username, 52 | languages: rawdata.data.languages.sort( 53 | (a, b) => b.total_seconds - a.total_seconds 54 | ), 55 | }; 56 | 57 | switch (layout) { 58 | case "bar": { 59 | const image = await generateSvg(BarWaka(data, theme), { 60 | width: 300, 61 | height: 337, 62 | }); 63 | 64 | return Send(image, {delay: 0.1, bar: true}); 65 | } 66 | case "compact": 67 | const image = await generateSvg(CompactWaka(data, theme), { 68 | width: 480, 69 | height: 130, 70 | }); 71 | 72 | return Send(image); 73 | case "normal": 74 | default: { 75 | const image = await generateSvg(NormalWaka(data, theme), { 76 | width: 320, 77 | height: 337, 78 | }); 79 | 80 | return Send(image, { delay: 0.1 }); 81 | } 82 | } 83 | 84 | } catch (err: any) { 85 | console.warn(err); 86 | 87 | const image = await generateSvg( 88 | Error(theme, { 89 | message: (err as Error).message, 90 | code: (err as Error).name, 91 | }), 92 | { 93 | width: 500, 94 | height: 170, 95 | } 96 | ); 97 | 98 | return Send(image, {error: true}); 99 | } 100 | } 101 | 102 | export const runtime = "edge"; 103 | -------------------------------------------------------------------------------- /assets/Urbanist-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/gitMyStat/095895765bea3409e36c721d790d491b8cfbdc39/assets/Urbanist-Bold.ttf -------------------------------------------------------------------------------- /assets/Urbanist-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/gitMyStat/095895765bea3409e36c721d790d491b8cfbdc39/assets/Urbanist-SemiBold.ttf -------------------------------------------------------------------------------- /assets/gitmystat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/gitMyStat/095895765bea3409e36c721d790d491b8cfbdc39/assets/gitmystat.png -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/gitMyStat/095895765bea3409e36c721d790d491b8cfbdc39/bun.lockb -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN= -------------------------------------------------------------------------------- /helpers/animate.ts: -------------------------------------------------------------------------------- 1 | import { Options } from "@/types/AnimateOptions"; 2 | 3 | export function animate(svgString: string, options?: Options): string { 4 | if (options?.error) return svgString; 5 | 6 | const pathRegex = /]*\bd="([^"]*)"/gi; 7 | let match; 8 | let index = 0; 9 | while ((match = pathRegex.exec(svgString)) !== null) { 10 | if (match[1].length > 150) { 11 | svgString = svgString.replace( 12 | match[0], 13 | `${match[0]} class="long-path" style="animation: fadein 0.5s ${index * (options?.delay || 0.4) + 0.2}s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards; opacity: 0;"` 14 | ); 15 | index++; 16 | } 17 | } 18 | 19 | if (options?.bar) { 20 | const clip = /]*\bclip-path="([^"]*)"/gi; 21 | match = ""; 22 | index = 0; 23 | while ((match = clip.exec(svgString)) !== null) { 24 | if (match[1].includes("cp-id-1")) { 25 | svgString = svgString.replace( 26 | match[0], 27 | `${match[0]} class="barchart" style="animation: fadein 0.7s ${index * (options?.delay || 0.4) + 0.1}s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards; opacity: 0;"` 28 | ); 29 | index++; 30 | } 31 | } 32 | } 33 | const style = ` 34 | 57 | 58 | 82 | `; 83 | const splitter = svgString.split('/svg">'); 84 | return splitter[0] + '/svg">' + style + splitter[1]; 85 | } 86 | -------------------------------------------------------------------------------- /helpers/calculateLanguage.ts: -------------------------------------------------------------------------------- 1 | import { RawLanguageData, LanguageStat } from "@/types/Languages"; 2 | 3 | export default function calculateLanguageStats(languageData: RawLanguageData): LanguageStat[] { 4 | const repositories = languageData.data.user.repositories.edges; 5 | const languageMap = new Map(); 6 | let totalSize = 0; 7 | 8 | repositories.forEach(repo => { 9 | const languages = repo.node.languages.edges; 10 | if (languages) { 11 | languages.forEach(lang => { 12 | const name = lang.node.name; 13 | const size = lang.size; 14 | const color = lang.node.color; 15 | 16 | if (languageMap.has(name)) { 17 | languageMap.get(name)!.size += size; 18 | } else { 19 | languageMap.set(name, { size, color }); 20 | } 21 | 22 | totalSize += size; 23 | }); 24 | } 25 | }); 26 | 27 | const languageStats: LanguageStat[] = []; 28 | languageMap.forEach((value, key) => { 29 | const { size, color } = value; 30 | const percent = (size / totalSize) * 100; 31 | const stat: LanguageStat = { 32 | name: key, 33 | color: color, 34 | count: size, 35 | percent: parseFloat(percent.toFixed(2)) 36 | }; 37 | languageStats.push(stat); 38 | }); 39 | 40 | languageStats.sort((a, b) => b.percent - a.percent); 41 | 42 | return languageStats; 43 | } -------------------------------------------------------------------------------- /helpers/calculateRank.ts: -------------------------------------------------------------------------------- 1 | import { RawUserData, UserStats } from "@/types/UserStats"; 2 | 3 | function calculatePercentiles(stats: UserStats): number { 4 | const { commits, pullRequests, reviews, issues, stars, followers } = stats; 5 | 6 | const commitPercentile = 1 - Math.exp(-commits / 100); 7 | const prPercentile = 1 - Math.exp(-pullRequests / 100); 8 | const reviewPercentile = 1 - Math.exp(-reviews / 100); 9 | const issuePercentile = 1 - Math.exp(-issues / 100); 10 | const starPercentile = (1 + erf(Math.log(stars) / Math.sqrt(2))) / 2; 11 | const followerPercentile = (1 + erf(Math.log(followers) / Math.sqrt(2))) / 2; 12 | 13 | const globalPercentile = 14 | (commitPercentile + 15 | prPercentile + 16 | reviewPercentile + 17 | issuePercentile + 18 | starPercentile + 19 | followerPercentile) / 20 | 6; 21 | 22 | return globalPercentile * 100; 23 | } 24 | 25 | function mapPercentileToRank(percent: number): string { 26 | const percentile = 100 - percent; 27 | if (percentile <= 1) return "S"; 28 | if (percentile <= 12.5) return "A+"; 29 | if (percentile <= 25) return "A"; 30 | if (percentile <= 37.5) return "A-"; 31 | if (percentile <= 50) return "B+"; 32 | if (percentile <= 62.5) return "B"; 33 | if (percentile <= 75) return "B-"; 34 | if (percentile <= 87.5) return "C+"; 35 | if (percentile <= 91) return "C"; 36 | return "D"; 37 | } 38 | 39 | export function calculateRank(stats: UserStats): string { 40 | const percentile = calculatePercentiles(stats); 41 | console.log(percentile); 42 | const rank = mapPercentileToRank(percentile); 43 | return rank; 44 | } 45 | 46 | function erf(x: number): number { 47 | const a1 = 0.254829592; 48 | const a2 = -0.284496736; 49 | const a3 = 1.421413741; 50 | const a4 = -1.453152027; 51 | const a5 = 1.061405429; 52 | const p = 0.3275911; 53 | 54 | const sign = x >= 0 ? 1 : -1; 55 | x = Math.abs(x); 56 | 57 | const t = 1.0 / (1.0 + p * x); 58 | const y = ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t; 59 | return sign * (1.0 - y * Math.exp(-x * x)); 60 | } 61 | 62 | export function parseGitHubData(data: RawUserData): UserStats { 63 | const userData = data.data.user; 64 | const contributions = userData.contributionsCollection; 65 | const repositories = userData.repositories.nodes; 66 | const followers = userData.followers.totalCount; 67 | 68 | const stars = repositories.reduce( 69 | (acc, repo) => acc + repo.stargazers.totalCount, 70 | 0 71 | ); 72 | 73 | const stats: UserStats = { 74 | commits: contributions.contributionCalendar.totalContributions, 75 | pullRequests: userData.pullRequests.totalCount, 76 | reviews: contributions.totalCommitContributions, 77 | issues: userData.issues.totalCount, 78 | stars, 79 | followers, 80 | }; 81 | 82 | return stats; 83 | } 84 | -------------------------------------------------------------------------------- /helpers/emoji.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js. 3 | */ 4 | 5 | /* ! Copyright Twitter Inc. and other contributors. Licensed under MIT */ 6 | 7 | const U200D = String.fromCharCode(8205); 8 | const UFE0Fg = /\uFE0F/g; 9 | 10 | export function getIconCode(char: string) { 11 | return toCodePoint(!char.includes(U200D) ? char.replace(UFE0Fg, "") : char); 12 | } 13 | 14 | function toCodePoint(unicodeSurrogates: string) { 15 | const r = []; 16 | let c = 0; 17 | let i = 0; 18 | let p = 0; 19 | 20 | while (i < unicodeSurrogates.length) { 21 | c = unicodeSurrogates.charCodeAt(i++); 22 | if (p) { 23 | r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16)); 24 | p = 0; 25 | } else if (55296 <= c && c <= 56319) { 26 | p = c; 27 | } else { 28 | r.push(c.toString(16)); 29 | } 30 | } 31 | return r.join("-"); 32 | } 33 | 34 | const emojiCache: Record> = {}; 35 | 36 | export async function loadEmoji(code: string) { 37 | const key = `${code}`; 38 | 39 | if (key in emojiCache) { 40 | return emojiCache[key]; 41 | } 42 | 43 | const api = "https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/"; 44 | 45 | return (emojiCache[key] = fetch(`${api}${code.toUpperCase()}.svg`).then( 46 | async (r) => r.text() 47 | )); 48 | } 49 | -------------------------------------------------------------------------------- /helpers/formatNum.ts: -------------------------------------------------------------------------------- 1 | export function formatNumber(num: number): string { 2 | if (num >= 1000 && num < 1000000) { 3 | return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; 4 | } else if (num >= 1000000) { 5 | return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'; 6 | } else { 7 | return num.toString(); 8 | } 9 | } -------------------------------------------------------------------------------- /helpers/generateSvg.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import satori from "satori"; 3 | import { getIconCode, loadEmoji } from "./emoji"; 4 | 5 | const urbanistFetch = fetch( 6 | new URL("../assets/Urbanist-SemiBold.ttf", import.meta.url), 7 | { 8 | cache: "force-cache", 9 | } 10 | ).then((res) => res.arrayBuffer()); 11 | 12 | const boldFetch = fetch( 13 | new URL("../assets/Urbanist-Bold.ttf", import.meta.url), 14 | { 15 | cache: "force-cache", 16 | } 17 | ).then((res) => res.arrayBuffer()); 18 | 19 | export default async function generateSvg( 20 | fn: ReactNode, 21 | options: { width: number; height: number } 22 | ) { 23 | const urbanist = await urbanistFetch; 24 | const urbanistBold = await boldFetch; 25 | 26 | const data = await satori(fn, { 27 | width: options.width, 28 | height: options.height, 29 | fonts: [ 30 | { 31 | weight: 500, 32 | style: "normal", 33 | data: urbanist, 34 | name: "Urbanist", 35 | }, 36 | { 37 | weight: 600, 38 | style: "normal", 39 | data: urbanistBold, 40 | name: "Urbanist", 41 | }, 42 | ], 43 | loadAdditionalAsset: async (code: string, segment: string) => { 44 | if (code === "emoji") { 45 | 46 | return `data:image/svg+xml;base64,${btoa(await loadEmoji(getIconCode(segment)))}`; 47 | } 48 | 49 | 50 | // if segment is normal text 51 | return code; 52 | } 53 | 54 | }); 55 | 56 | return data; 57 | } 58 | -------------------------------------------------------------------------------- /helpers/getColor.ts: -------------------------------------------------------------------------------- 1 | export function generateColor(str: string): string { 2 | let hash = 0; 3 | for (let i = 0; i < str.length; i++) { 4 | const char = str.charCodeAt(i); 5 | hash = (hash << 5) - hash + char; 6 | hash |= 0; 7 | } 8 | 9 | const color = (hash & 0x00FFFFFF).toString(16).toUpperCase().padStart(6, '0'); 10 | 11 | return `#${color}`; 12 | } -------------------------------------------------------------------------------- /helpers/getData.ts: -------------------------------------------------------------------------------- 1 | import { Themes } from "@/themes"; 2 | import { ThemeData } from "@/types/Theme"; 3 | 4 | export function getData(searchParams: URLSearchParams): ThemeData { 5 | const username = searchParams.has("username"); 6 | const user = (username ? searchParams.get("username") : "rahuletto") as string; 7 | 8 | const hasTheme = searchParams.has("theme"); 9 | let theme = hasTheme ? searchParams.get("theme") : "dark"; 10 | if (!theme || !Themes[theme as string]?.accent) theme = "dark"; 11 | 12 | const hasColor = searchParams.has("color"); 13 | const color = hasColor 14 | ? decodeURIComponent(searchParams.get("color") as string).replaceAll( 15 | "0x", 16 | "#" 17 | ) 18 | : hasTheme 19 | ? Themes[theme as string].color 20 | : "#E6EDF3"; 21 | 22 | const hasAccent = searchParams.has("accent"); 23 | const accent = hasAccent 24 | ? decodeURIComponent(searchParams.get("accent") as string).replaceAll( 25 | "0x", 26 | "#" 27 | ) 28 | : hasTheme 29 | ? Themes[theme as string].accent 30 | : "#8D96A0"; 31 | 32 | const hasBg = searchParams.has("background"); 33 | const background = hasBg 34 | ? decodeURIComponent(searchParams.get("background") as string).replaceAll( 35 | "0x", 36 | "#" 37 | ) 38 | : hasTheme 39 | ? Themes[theme as string].background 40 | : "#0D1116"; 41 | 42 | const hasBorder = searchParams.has("border"); 43 | const border = hasBorder 44 | ? decodeURIComponent(searchParams.get("border") as string).replaceAll( 45 | "0x", 46 | "#" 47 | ) 48 | : hasTheme 49 | ? Themes[theme as string].border 50 | : "#30363D"; 51 | 52 | const hasTip = searchParams.has("tip"); 53 | const tip = hasTip 54 | ? decodeURIComponent(searchParams.get("tip") as string).replaceAll( 55 | "0x", 56 | "#" 57 | ) 58 | : hasTheme 59 | ? Themes[theme as string].tip 60 | : "#30363D"; 61 | 62 | const hasRad = searchParams.has("radius"); 63 | const radius = hasRad 64 | ? Number(searchParams.get("radius")) 65 | : hasTheme 66 | ? Themes[theme as string].radius 67 | : 24; 68 | 69 | const hasPad = searchParams.has("padding"); 70 | const padding = hasPad 71 | ? Number(searchParams.get("padding")) 72 | : hasTheme 73 | ? Themes[theme as string].padding 74 | : 24; 75 | 76 | return { 77 | theme, 78 | user, 79 | color, 80 | accent, 81 | background, 82 | border, 83 | tip, 84 | radius, 85 | padding, 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /helpers/send.ts: -------------------------------------------------------------------------------- 1 | import { Options } from "@/types/AnimateOptions"; 2 | import { animate } from "./animate"; 3 | 4 | 5 | export default function Send(image: string, options?: Options) { 6 | return new Response(animate(image, options), { 7 | headers: { 8 | "Accept-Encoding": "gzip, deflate, br, zstd", 9 | "cache-control": "private, max-age=43200, stale-if-error=3600, stale-while-revalidate=21600", 10 | "Content-Type": "image/svg+xml", 11 | }, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | compress: true, 4 | reactStrictMode: true, 5 | swcMinify: true, 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mystats", 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 | "@vercel/og": "^0.6.2", 13 | "next": "14.2.4", 14 | "react": "18.3.1", 15 | "react-colorful": "^5.6.1", 16 | "react-dom": "18.3.1", 17 | "react-icons": "^5.2.1", 18 | "satori": "^0.10.13" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^20", 22 | "@types/react": "^18", 23 | "@types/react-dom": "^18", 24 | "eslint": "^8", 25 | "eslint-config-next": "14.2.4", 26 | "postcss": "^8", 27 | "prettier": "^3.3.2", 28 | "prettier-plugin-tailwindcss": "^0.6.5", 29 | "tailwindcss": "^3.4.1", 30 | "typescript": "^5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /providers/DeviceProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | createContext, 5 | ReactNode, 6 | useContext, 7 | useEffect, 8 | useState, 9 | } from "react"; 10 | 11 | 12 | export const DeviceContext = createContext<{ 13 | mode: "desktop" | "mobile" | "tablet" 14 | }>({ mode: "desktop" }); 15 | 16 | 17 | export function useDevice() { 18 | const { mode } = useContext(DeviceContext); 19 | return mode; 20 | } 21 | export const DeviceProvider = ({ children }: { children: ReactNode }) => { 22 | const [mode, setMode] = useState<"desktop" | "mobile" | "tablet">("desktop"); 23 | 24 | useEffect(() => { 25 | setMode(device()); 26 | 27 | setInterval(() => { 28 | setMode(device()); 29 | }, 2000); 30 | 31 | function device() { 32 | const screenWidth = document.body.clientWidth; 33 | 34 | const userAgent = navigator.userAgent.toLowerCase(); 35 | const isMobile = 36 | /iphone|ipod|android|windows phone/g.test(userAgent); 37 | const isTablet = 38 | /(ipad|tablet|playbook|silk)|(android(?!.*mobile))/g.test(userAgent); 39 | 40 | if (screenWidth < 768 || isMobile) { 41 | return "mobile"; 42 | } else if ((screenWidth >= 768 && screenWidth <= 1024) || isTablet) { 43 | return "tablet"; 44 | } else { 45 | return "desktop"; 46 | } 47 | } 48 | const handleResize = () => { 49 | const newDeviceType = device(); 50 | if (mode != newDeviceType) setMode(newDeviceType); 51 | }; 52 | 53 | window.addEventListener("resize", handleResize); 54 | return () => { 55 | window.removeEventListener("resize", handleResize); 56 | }; 57 | }, [mode]); 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | ); 64 | }; -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/gitMyStat/095895765bea3409e36c721d790d491b8cfbdc39/public/banner.png -------------------------------------------------------------------------------- /public/fine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/gitMyStat/095895765bea3409e36c721d790d491b8cfbdc39/public/fine.jpg -------------------------------------------------------------------------------- /public/gitmystat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/gitMyStat/095895765bea3409e36c721d790d491b8cfbdc39/public/gitmystat.png -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import { Themes } from "./themes"; 3 | 4 | const config: Config = { 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | darkMode: "media", // or 'class' 11 | theme: { 12 | extend: { 13 | colors: { 14 | background: "var(--background)", 15 | color: "var(--color)", 16 | accent: "var(--accent)", 17 | tip: "var(--tip)", 18 | ...Themes 19 | }, 20 | transitionTimingFunction: { 21 | 'bouncy': 'cubic-bezier(0.68, -0.55, 0.27, 1.55)', 22 | }, 23 | }, 24 | }, 25 | plugins: [], 26 | }; 27 | export default config; 28 | -------------------------------------------------------------------------------- /themes/index.ts: -------------------------------------------------------------------------------- 1 | import { ThemeType } from "@/types/Theme"; 2 | 3 | export const Themes: ThemeType = { 4 | // Github Themes 5 | dark: { 6 | color: "#E6EDF3", 7 | accent: "#8D96A0", 8 | background: "#0D1116", 9 | border: "#30363D", 10 | tip: "#30363D", 11 | radius: 24, 12 | padding: 24, 13 | }, 14 | light: { 15 | color: "#000000", 16 | accent: "#343941", 17 | background: "#fefefe", 18 | border: "#C2C9D1", 19 | tip: "#D0D7DE", 20 | radius: 24, 21 | padding: 24, 22 | }, 23 | // My Portfolio themes 24 | gold: { 25 | color: "#D6BFB2", 26 | accent: "#A6A6A6", 27 | background: "#141414", 28 | border: "#1B1A1A", 29 | tip: "#5c5c5c", 30 | radius: 24, 31 | padding: 24, 32 | }, 33 | "gold-light": { 34 | color: "#967C6E", 35 | accent: "#898888", 36 | background: "#EDEDED", 37 | border: "#D6CDCD", 38 | tip: "#967C6E", 39 | radius: 24, 40 | padding: 24, 41 | }, 42 | 43 | // Transparent 44 | transparent: { 45 | color: "#fefefe", 46 | accent: "#fefefe", 47 | background: "#00000000", 48 | border: "#00000000", 49 | tip: "#e4e4e4", 50 | radius: 24, 51 | padding: 24, 52 | }, 53 | "transparent-light": { 54 | color: "#000000", 55 | accent: "#000000", 56 | background: "#00000000", 57 | border: "#00000000", 58 | tip: "#1d1d1d", 59 | radius: 24, 60 | padding: 24, 61 | }, 62 | // Tokyonight 63 | tokyonight: { 64 | color: "#B4BDE6", 65 | accent: "#6CCBBE", 66 | background: "#24283A", 67 | border: "#17B99A", 68 | tip: "#9274C9", 69 | radius: 24, 70 | padding: 24, 71 | }, 72 | tokyoday: { 73 | color: "#5476C6", 74 | accent: "#3F756D", 75 | background: "#E1E2E6", 76 | border: "#50A3B5", 77 | tip: "#8C64C5", 78 | radius: 24, 79 | padding: 24, 80 | }, 81 | // gruvbox 82 | gruvbox: { 83 | color: "#EBD9B4", 84 | accent: "#FABD2E", 85 | background: "#282828", 86 | border: "#3C3836", 87 | tip: "#928275", 88 | radius: 24, 89 | padding: 24, 90 | }, 91 | "gruvbox-light": { 92 | color: "#3C3836", 93 | accent: "#B47524", 94 | background: "#FBF0C9", 95 | border: "#EBD9B4", 96 | tip: "#958375", 97 | radius: 24, 98 | padding: 24, 99 | }, 100 | // our theme 101 | 'our-theme': { 102 | color: "#FCD116", 103 | accent: "#FFF", 104 | background: "#CE1226", 105 | border: "#FCD116", 106 | tip: "#9F1120", 107 | radius: 24, 108 | padding: 24, 109 | }, 110 | // Catppuccin 111 | catppuccin: { 112 | color: "#CBA6F7", 113 | accent: "#CDD6F4", 114 | background: "#1E1E2E", 115 | border: "#181825", 116 | tip: "#CDD6F4", 117 | radius: 24, 118 | padding: 24, 119 | }, 120 | "catppuccin-light": { 121 | color: "#130F1A", 122 | accent: "#130F1A", 123 | background: "#cacffa", 124 | border: "#706C91", 125 | tip: "#706C91", 126 | radius: 24, 127 | padding: 24, 128 | }, 129 | 130 | // blueberry 131 | blueberry: { 132 | color: "#91B4D5", 133 | accent: "#ACD7FF", 134 | background: "#212B42", 135 | border: "#1B2334", 136 | tip: "#5C7DA5", 137 | radius: 24, 138 | padding: 24, 139 | }, 140 | "blueberry-light": { 141 | color: "#678198", 142 | accent: "#506477", 143 | background: "#DAE0F5", 144 | border: "#C1C7DF", 145 | tip: "#92A4BE", 146 | radius: 24, 147 | padding: 24, 148 | }, 149 | 150 | // dino 151 | dino: { 152 | color: "#FFF", 153 | accent: "#CAFAD9", 154 | background: "#1D221F", 155 | border: "#40D672", 156 | tip: "#D5D5D5", 157 | radius: 24, 158 | padding: 24, 159 | }, 160 | "dino-light": { 161 | color: "#1D221F", 162 | accent: "#40D672", 163 | background: "#FFF", 164 | border: "#CAFAD9", 165 | tip: "#D5D5D5", 166 | radius: 24, 167 | padding: 24, 168 | }, 169 | 170 | // bw 171 | stealth: { 172 | color: "#5E676E", 173 | accent: "#5E676E", 174 | background: "#010203", 175 | border: "#383E42", 176 | tip: "#121212", 177 | radius: 24, 178 | padding: 24, 179 | }, 180 | paper: { 181 | color: "#444", 182 | accent: "#444", 183 | background: "#EEE", 184 | border: "#DDD", 185 | tip: "#B2B2B2", 186 | radius: 24, 187 | padding: 24, 188 | }, 189 | // drowning 190 | drowning: { 191 | color: "#4A6FB5", 192 | accent: "#9393A7", 193 | background: "#191826", 194 | border: "#1E1F2F", 195 | tip: "#50688C", 196 | radius: 24, 197 | padding: 24, 198 | }, 199 | 200 | //moonlight 201 | moonlight: { 202 | color: "#C69E69", 203 | accent: "#CCCCB5", 204 | background: "#191F28", 205 | border: "#141A22", 206 | tip: "#4B5975", 207 | radius: 24, 208 | padding: 24, 209 | }, 210 | 211 | // vscode 212 | vsdark: { 213 | color: "#B5CEA8", 214 | accent: "#D4D4D4", 215 | background: "#1E1E1E", 216 | border: "#252526", 217 | tip: "#858585", 218 | radius: 24, 219 | padding: 24, 220 | }, 221 | vslight: { 222 | color: "#001080", 223 | accent: "#6D7681", 224 | background: "#FFF", 225 | border: "#F8F8F8", 226 | tip: "#6A9956", 227 | radius: 24, 228 | padding: 24, 229 | }, 230 | 231 | // cyred 232 | cyred: { 233 | color: "#E55150", 234 | accent: "#FAA", 235 | background: "#3F1516", 236 | border: "#6E2627", 237 | tip: "#FAA", 238 | radius: 24, 239 | padding: 24, 240 | }, 241 | "cyred-light": { 242 | color: "#E55150", 243 | accent: "#6E2627", 244 | background: "#fee", 245 | border: "#E55150", 246 | tip: "#3F1516", 247 | radius: 24, 248 | padding: 24, 249 | }, 250 | 251 | // One dark 252 | onedark: { 253 | color: "#ABB2BF", 254 | accent: "#98C379", 255 | background: "#23272E", 256 | border: "#3B4048", 257 | tip: "#E06C75", 258 | radius: 24, 259 | padding: 24, 260 | }, 261 | onelight: { 262 | color: "#393A43", 263 | accent: "#A81AA5", 264 | background: "#FAFAFA", 265 | border: "#565B7D", 266 | tip: "#A0A1A6", 267 | radius: 24, 268 | padding: 24, 269 | }, 270 | 271 | // discord 272 | discord: { 273 | color: "#5A65EA", 274 | accent: "#DCDEE2", 275 | background: "#111214", 276 | border: "#5A65EA", 277 | tip: "#2B2D31", 278 | radius: 24, 279 | padding: 24, 280 | }, 281 | "discord-light": { 282 | color: "#5664F0", 283 | accent: "#313338", 284 | background: "#FFF", 285 | border: "#E1E2E4", 286 | tip: "#313338", 287 | radius: 24, 288 | padding: 24, 289 | }, 290 | }; 291 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strictNullChecks": 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 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } -------------------------------------------------------------------------------- /types/AnimateOptions.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | delay?: number; 3 | bar?: boolean; 4 | error?: boolean; 5 | } -------------------------------------------------------------------------------- /types/Error.ts: -------------------------------------------------------------------------------- 1 | export interface QLError { 2 | type: string; 3 | path: string[]; 4 | locations: { 5 | line: number; 6 | column: number; 7 | }[]; 8 | message: string; 9 | } 10 | -------------------------------------------------------------------------------- /types/Languages.ts: -------------------------------------------------------------------------------- 1 | import { QLError } from "./Error"; 2 | 3 | export interface Repository { 4 | name: string; 5 | languages: { 6 | totalSize: number; 7 | edges: {size: number; node: { name: string; color: string }}[] | null; 8 | }; 9 | } 10 | 11 | export interface RawLanguageData { 12 | data: { 13 | user: { 14 | repositories: { 15 | edges: Array<{ node: Repository }>; 16 | }; 17 | }; 18 | }; 19 | errors?: QLError[]; 20 | } 21 | 22 | export interface LanguageStat { 23 | name: string; 24 | color: string; 25 | count: number; 26 | percent: number; 27 | } 28 | -------------------------------------------------------------------------------- /types/Recents.ts: -------------------------------------------------------------------------------- 1 | export interface RecentData { 2 | name: string; 3 | url: string; 4 | } 5 | -------------------------------------------------------------------------------- /types/Repo.ts: -------------------------------------------------------------------------------- 1 | import { QLError } from "./Error"; 2 | 3 | export interface RawRepoData { 4 | data: { 5 | repositoryOwner: { 6 | repository: Repo; 7 | }; 8 | }; 9 | errors?: QLError[]; 10 | } 11 | 12 | export interface Repo { 13 | name: string; 14 | description: string; 15 | stargazerCount: number; 16 | forkCount: number; 17 | primaryLanguage: { 18 | name: string; 19 | color: string; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /types/Theme.ts: -------------------------------------------------------------------------------- 1 | export interface ThemeType { 2 | [key: string]: { 3 | color: string; 4 | accent: string; 5 | background: string; 6 | border: string; 7 | radius: number; 8 | padding: number; 9 | tip: string; 10 | }; 11 | } 12 | 13 | export interface ThemeData { 14 | user: string; 15 | color: string; 16 | accent: string; 17 | background: string; 18 | border: string; 19 | radius: number; 20 | padding: number; 21 | tip?: string; 22 | theme?: string 23 | } 24 | -------------------------------------------------------------------------------- /types/UserStats.ts: -------------------------------------------------------------------------------- 1 | import { QLError } from "./Error"; 2 | 3 | export interface UserStats { 4 | commits: number; 5 | pullRequests: number; 6 | reviews: number; 7 | issues: number; 8 | stars: number; 9 | followers: number; 10 | } 11 | 12 | export interface RawUserData { 13 | data: { 14 | user: User; 15 | }; 16 | errors?: QLError[] 17 | } 18 | 19 | interface ContributeCollection { 20 | totalCommitContributions: number; 21 | contributionCalendar: { 22 | totalContributions: number; 23 | }; 24 | totalRepositoriesWithContributedCommits: number; 25 | totalPullRequestContributions: number; 26 | totalIssueContributions: number; 27 | } 28 | 29 | interface Repositories { 30 | nodes: { 31 | stargazers: { 32 | totalCount: number; 33 | }; 34 | }[]; 35 | } 36 | 37 | interface User { 38 | contributionsCollection: ContributeCollection; 39 | repositories: Repositories; 40 | issues: { 41 | totalCount: number; 42 | }; 43 | pullRequests: { 44 | totalCount: number; 45 | }; 46 | followers: { 47 | totalCount: number; 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /types/Waka.ts: -------------------------------------------------------------------------------- 1 | export interface RawWakaData { 2 | error?: string; 3 | data: { 4 | categories: WakatimeStat[]; 5 | languages: WakatimeStat[]; 6 | editors: WakatimeStat[]; 7 | operating_systems: WakatimeStat[]; 8 | daily_average: number; 9 | daily_average_including_other_language: number; 10 | days_including_holidays: number; 11 | days_minus_holidays: number; 12 | holidays: number; 13 | human_readable_daily_average: string; 14 | human_readable_daily_average_including_other_language: string; 15 | human_readable_range: string; 16 | human_readable_total: string; 17 | human_readable_total_including_other_language: string; 18 | id: string; 19 | is_already_updating: boolean; 20 | is_cached: boolean; 21 | is_coding_activity_visible: boolean; 22 | is_including_today: boolean; 23 | is_other_usage_visible: boolean; 24 | is_stuck: boolean; 25 | is_up_to_date: boolean; 26 | is_up_to_date_pending_future: boolean; 27 | percent_calculated: number; 28 | range: string; 29 | status: string; 30 | timeout: number; 31 | total_seconds: number; 32 | total_seconds_including_other_language: number; 33 | user_id: string; 34 | username: string; 35 | writes_only: boolean; 36 | }; 37 | message: string; 38 | } 39 | 40 | 41 | interface WakatimeStat { 42 | decimal: string; 43 | digital: string; 44 | hours: number; 45 | minutes: number; 46 | name: string; 47 | percent: number; 48 | text: string; 49 | total_seconds: number; 50 | } 51 | 52 | export interface WakaData { 53 | user: string; 54 | languages: WakatimeStat[]; 55 | 56 | } -------------------------------------------------------------------------------- /utils/colors.ts: -------------------------------------------------------------------------------- 1 | export interface ColorType { 2 | [key: string]: string | null; 3 | } 4 | 5 | export const colors: ColorType = { 6 | "1C Enterprise": "#814CCC", 7 | "2-Dimensional Array": "#38761D", 8 | "4D": "#004289", 9 | ABAP: "#E8274B", 10 | "ABAP CDS": "#555e25", 11 | ActionScript: "#882B0F", 12 | Ada: "#02f88c", 13 | "Adblock Filter List": "#800000", 14 | "Adobe Font Metrics": "#fa0f00", 15 | Agda: "#315665", 16 | "AGS Script": "#B9D9FF", 17 | AIDL: "#34EB6B", 18 | AL: "#3AA2B5", 19 | Alloy: "#64C800", 20 | "Alpine Abuild": "#0D597F", 21 | "Altium Designer": "#A89663", 22 | AMPL: "#E6EFBB", 23 | AngelScript: "#C7D7DC", 24 | "Ant Build System": "#A9157E", 25 | Antlers: "#ff269e", 26 | ANTLR: "#9DC3FF", 27 | ApacheConf: "#d12127", 28 | Apex: "#1797c0", 29 | "API Blueprint": "#2ACCA8", 30 | APL: "#5A8164", 31 | "Apollo Guidance Computer": "#0B3D91", 32 | AppleScript: "#101F1F", 33 | Arc: "#aa2afe", 34 | AsciiDoc: "#73a0c5", 35 | ASL: null, 36 | "ASP.NET": "#9400ff", 37 | AspectJ: "#a957b0", 38 | Assembly: "#6E4C13", 39 | Astro: "#ff5a03", 40 | Asymptote: "#ff0000", 41 | ATS: "#1ac620", 42 | Augeas: "#9CC134", 43 | AutoHotkey: "#6594b9", 44 | AutoIt: "#1C3552", 45 | "Avro IDL": "#0040FF", 46 | Awk: "#c30e9b", 47 | Ballerina: "#FF5000", 48 | BASIC: "#ff0000", 49 | Batchfile: "#C1F12E", 50 | Beef: "#a52f4e", 51 | Befunge: null, 52 | Berry: "#15A13C", 53 | BibTeX: "#778899", 54 | Bicep: "#519aba", 55 | Bikeshed: "#5562ac", 56 | Bison: "#6A463F", 57 | BitBake: "#00bce4", 58 | Blade: "#f7523f", 59 | BlitzBasic: "#00FFAE", 60 | BlitzMax: "#cd6400", 61 | Bluespec: "#12223c", 62 | "Bluespec BH": "#12223c", 63 | Boo: "#d4bec1", 64 | Boogie: "#c80fa0", 65 | BQN: "#2b7067", 66 | Brainfuck: "#2F2530", 67 | BrighterScript: "#66AABB", 68 | Brightscript: "#662D91", 69 | Browserslist: "#ffd539", 70 | C: "#555555", 71 | "C#": "#178600", 72 | "C++": "#f34b7d", 73 | "C2hs Haskell": null, 74 | "Cabal Config": "#483465", 75 | Caddyfile: "#22b638", 76 | Cadence: "#00ef8b", 77 | Cairo: "#ff4a48", 78 | CameLIGO: "#3be133", 79 | "CAP CDS": "#0092d1", 80 | "Cap'n Proto": "#c42727", 81 | CartoCSS: null, 82 | Ceylon: "#dfa535", 83 | Chapel: "#8dc63f", 84 | Charity: null, 85 | ChucK: "#3f8000", 86 | Circom: "#707575", 87 | Cirru: "#ccccff", 88 | Clarion: "#db901e", 89 | Clarity: "#5546ff", 90 | "Classic ASP": "#6a40fd", 91 | Clean: "#3F85AF", 92 | Click: "#E4E6F3", 93 | CLIPS: "#00A300", 94 | Clojure: "#db5855", 95 | "Closure Templates": "#0d948f", 96 | "Cloud Firestore Security Rules": "#FFA000", 97 | CMake: "#DA3434", 98 | COBOL: null, 99 | CodeQL: "#140f46", 100 | CoffeeScript: "#244776", 101 | ColdFusion: "#ed2cd6", 102 | "ColdFusion CFC": "#ed2cd6", 103 | COLLADA: "#F1A42B", 104 | "Common Lisp": "#3fb68b", 105 | "Common Workflow Language": "#B5314C", 106 | "Component Pascal": "#B0CE4E", 107 | Cool: null, 108 | Coq: "#d0b68c", 109 | crontab: "#ead7ac", 110 | Crystal: "#000100", 111 | CSON: "#244776", 112 | Csound: "#1a1a1a", 113 | "Csound Document": "#1a1a1a", 114 | "Csound Score": "#1a1a1a", 115 | CSS: "#563d7c", 116 | CSV: "#237346", 117 | Cuda: "#3A4E3A", 118 | CUE: "#5886E1", 119 | Curry: "#531242", 120 | CWeb: "#00007a", 121 | Cycript: null, 122 | Cypher: "#34c0eb", 123 | Cython: "#fedf5b", 124 | D: "#ba595e", 125 | D2: "#526ee8", 126 | Dafny: "#FFEC25", 127 | "Darcs Patch": "#8eff23", 128 | Dart: "#00B4AB", 129 | DataWeave: "#003a52", 130 | "Debian Package Control File": "#D70751", 131 | DenizenScript: "#FBEE96", 132 | Dhall: "#dfafff", 133 | "DIGITAL Command Language": null, 134 | "DirectX 3D File": "#aace60", 135 | DM: "#447265", 136 | Dockerfile: "#384d54", 137 | Dogescript: "#cca760", 138 | Dotenv: "#e5d559", 139 | DTrace: null, 140 | Dylan: "#6c616e", 141 | E: "#ccce35", 142 | Earthly: "#2af0ff", 143 | Easybuild: "#069406", 144 | eC: "#913960", 145 | "Ecere Projects": "#913960", 146 | ECL: "#8a1267", 147 | ECLiPSe: "#001d9d", 148 | Ecmarkup: "#eb8131", 149 | Edge: "#0dffe0", 150 | EdgeQL: "#31A7FF", 151 | EditorConfig: "#fff1f2", 152 | Eiffel: "#4d6977", 153 | EJS: "#a91e50", 154 | Elixir: "#6e4a7e", 155 | Elm: "#60B5CC", 156 | Elvish: "#55BB55", 157 | "Elvish Transcript": "#55BB55", 158 | "Emacs Lisp": "#c065db", 159 | EmberScript: "#FFF4F3", 160 | EQ: "#a78649", 161 | Erlang: "#B83998", 162 | Euphoria: "#FF790B", 163 | "F#": "#b845fc", 164 | "F*": "#572e30", 165 | Factor: "#636746", 166 | Fancy: "#7b9db4", 167 | Fantom: "#14253c", 168 | Faust: "#c37240", 169 | Fennel: "#fff3d7", 170 | "FIGlet Font": "#FFDDBB", 171 | "Filebench WML": "#F6B900", 172 | Filterscript: null, 173 | FIRRTL: "#2f632f", 174 | fish: "#4aae47", 175 | Fluent: "#ffcc33", 176 | FLUX: "#88ccff", 177 | Forth: "#341708", 178 | Fortran: "#4d41b1", 179 | "Fortran Free Form": "#4d41b1", 180 | FreeBasic: "#141AC9", 181 | FreeMarker: "#0050b2", 182 | Frege: "#00cafe", 183 | Futhark: "#5f021f", 184 | "G-code": "#D08CF2", 185 | "Game Maker Language": "#71b417", 186 | GAML: "#FFC766", 187 | GAMS: "#f49a22", 188 | GAP: "#0000cc", 189 | "GCC Machine Description": "#FFCFAB", 190 | GDB: null, 191 | GDScript: "#355570", 192 | GEDCOM: "#003058", 193 | "Gemfile.lock": "#701516", 194 | Gemini: "#ff6900", 195 | "Genero 4gl": "#63408e", 196 | "Genero per": "#d8df39", 197 | Genie: "#fb855d", 198 | Genshi: "#951531", 199 | "Gentoo Ebuild": "#9400ff", 200 | "Gentoo Eclass": "#9400ff", 201 | "Gerber Image": "#d20b00", 202 | Gherkin: "#5B2063", 203 | "Git Attributes": "#F44D27", 204 | "Git Config": "#F44D27", 205 | "Git Revision List": "#F44D27", 206 | Gleam: "#ffaff3", 207 | "Glimmer JS": "#F5835F", 208 | "Glimmer TS": "#3178c6", 209 | GLSL: "#5686a5", 210 | Glyph: "#c1ac7f", 211 | Gnuplot: "#f0a9f0", 212 | Go: "#00ADD8", 213 | "Go Checksums": "#00ADD8", 214 | "Go Module": "#00ADD8", 215 | "Go Workspace": "#00ADD8", 216 | "Godot Resource": "#355570", 217 | Golo: "#88562A", 218 | Gosu: "#82937f", 219 | Grace: "#615f8b", 220 | Gradle: "#02303a", 221 | "Gradle Kotlin DSL": "#02303a", 222 | "Grammatical Framework": "#ff0000", 223 | GraphQL: "#e10098", 224 | "Graphviz (DOT)": "#2596be", 225 | Groovy: "#4298b8", 226 | "Groovy Server Pages": "#4298b8", 227 | GSC: "#FF6800", 228 | Hack: "#878787", 229 | Haml: "#ece2a9", 230 | Handlebars: "#f7931e", 231 | HAProxy: "#106da9", 232 | Harbour: "#0e60e3", 233 | Haskell: "#5e5086", 234 | Haxe: "#df7900", 235 | HCL: "#844FBA", 236 | HiveQL: "#dce200", 237 | HLSL: "#aace60", 238 | HOCON: "#9ff8ee", 239 | HolyC: "#ffefaf", 240 | hoon: "#00b171", 241 | "Hosts File": "#308888", 242 | HTML: "#e34c26", 243 | "HTML+ECR": "#2e1052", 244 | "HTML+EEX": "#6e4a7e", 245 | "HTML+ERB": "#701516", 246 | "HTML+PHP": "#4f5d95", 247 | "HTML+Razor": "#512be4", 248 | HTTP: "#005C9C", 249 | HXML: "#f68712", 250 | Hy: "#7790B2", 251 | HyPhy: null, 252 | IDL: "#a3522f", 253 | Idris: "#b30000", 254 | "Ignore List": "#000000", 255 | "IGOR Pro": "#0000cc", 256 | "ImageJ Macro": "#99AAFF", 257 | Imba: "#16cec6", 258 | "Inform 7": null, 259 | INI: "#d1dbe0", 260 | Ink: null, 261 | "Inno Setup": "#264b99", 262 | Io: "#a9188d", 263 | Ioke: "#078193", 264 | Isabelle: "#FEFE00", 265 | "Isabelle ROOT": "#FEFE00", 266 | J: "#9EEDFF", 267 | Janet: "#0886a5", 268 | "JAR Manifest": "#b07219", 269 | Jasmin: "#d03600", 270 | Java: "#b07219", 271 | "Java Properties": "#2A6277", 272 | "Java Server Pages": "#2A6277", 273 | JavaScript: "#f1e05a", 274 | "JavaScript+ERB": "#f1e05a", 275 | JCL: "#d90e09", 276 | "Jest Snapshot": "#15c213", 277 | "JetBrains MPS": "#21D789", 278 | JFlex: "#DBCA00", 279 | Jinja: "#a52a22", 280 | Jison: "#56b3cb", 281 | "Jison Lex": "#56b3cb", 282 | Jolie: "#843179", 283 | jq: "#c7254e", 284 | JSON: "#292929", 285 | "JSON with Comments": "#292929", 286 | JSON5: "#267CB9", 287 | JSONiq: "#40d47e", 288 | JSONLD: "#0c479c", 289 | Jsonnet: "#0064bd", 290 | Julia: "#a270ba", 291 | "Julia REPL": "#a270ba", 292 | "Jupyter Notebook": "#DA5B0B", 293 | Just: "#384d54", 294 | "Kaitai Struct": "#773b37", 295 | KakouneScript: "#6f8042", 296 | KerboScript: "#41adf0", 297 | "KiCad Layout": "#2f4aab", 298 | "KiCad Legacy Layout": "#2f4aab", 299 | "KiCad Schematic": "#2f4aab", 300 | Kotlin: "#A97BFF", 301 | KRL: "#28430A", 302 | kvlang: "#1da6e0", 303 | LabVIEW: "#fede06", 304 | Lark: "#2980B9", 305 | Lasso: "#999999", 306 | Latte: "#f2a542", 307 | Lean: null, 308 | "Lean 4": null, 309 | Less: "#1d365d", 310 | Lex: "#DBCA00", 311 | LFE: "#4C3023", 312 | LigoLANG: "#0e74ff", 313 | LilyPond: "#9ccc7c", 314 | Limbo: null, 315 | Liquid: "#67b8de", 316 | "Literate Agda": "#315665", 317 | "Literate CoffeeScript": "#244776", 318 | "Literate Haskell": "#5e5086", 319 | LiveScript: "#499886", 320 | LLVM: "#185619", 321 | Logos: null, 322 | Logtalk: "#295b9a", 323 | LOLCODE: "#cc9900", 324 | LookML: "#652B81", 325 | LoomScript: null, 326 | LSL: "#3d9970", 327 | Lua: "#000080", 328 | Luau: "#00A2FF", 329 | M: null, 330 | M4: null, 331 | M4Sugar: null, 332 | Macaulay2: "#d8ffff", 333 | Makefile: "#427819", 334 | Mako: "#7e858d", 335 | Markdown: "#083fa1", 336 | Marko: "#42bff2", 337 | Mask: "#f97732", 338 | Mathematica: "#dd1100", 339 | MATLAB: "#e16737", 340 | Max: "#c4a79c", 341 | MAXScript: "#00a6a6", 342 | mcfunction: "#E22837", 343 | MDX: "#fcb32c", 344 | Mercury: "#ff2b2b", 345 | Mermaid: "#ff3670", 346 | Meson: "#007800", 347 | Metal: "#8f14e9", 348 | MiniD: null, 349 | MiniYAML: "#ff1111", 350 | Mint: "#02b046", 351 | Mirah: "#c7a938", 352 | "mIRC Script": "#3d57c3", 353 | MLIR: "#5EC8DB", 354 | Modelica: "#de1d31", 355 | "Modula-2": "#10253f", 356 | "Modula-3": "#223388", 357 | "Module Management System": null, 358 | Mojo: "#ff4c1f", 359 | Monkey: null, 360 | "Monkey C": "#8D6747", 361 | Moocode: null, 362 | MoonScript: "#ff4585", 363 | Motoko: "#fbb03b", 364 | "Motorola 68K Assembly": "#005daa", 365 | Move: "#4a137a", 366 | MQL4: "#62A8D6", 367 | MQL5: "#4A76B8", 368 | MTML: "#b7e1f4", 369 | MUF: null, 370 | mupad: "#244963", 371 | Mustache: "#724b3b", 372 | Myghty: null, 373 | nanorc: "#2d004d", 374 | Nasal: "#1d2c4e", 375 | NASL: null, 376 | NCL: "#28431f", 377 | Nearley: "#990000", 378 | Nemerle: "#3d3c6e", 379 | nesC: "#94B0C7", 380 | NetLinx: "#0aa0ff", 381 | "NetLinx+ERB": "#747faa", 382 | NetLogo: "#ff6375", 383 | NewLisp: "#87AED7", 384 | Nextflow: "#3ac486", 385 | Nginx: "#009639", 386 | Nim: "#ffc200", 387 | Nit: "#009917", 388 | Nix: "#7e7eff", 389 | NMODL: "#00356B", 390 | "NPM Config": "#cb3837", 391 | NSIS: null, 392 | Nu: "#c9df40", 393 | NumPy: "#9C8AF9", 394 | Nunjucks: "#3d8137", 395 | Nushell: "#4E9906", 396 | NWScript: "#111522", 397 | "OASv2-json": "#85ea2d", 398 | "OASv2-yaml": "#85ea2d", 399 | "OASv3-json": "#85ea2d", 400 | "OASv3-yaml": "#85ea2d", 401 | Oberon: null, 402 | "Objective-C": "#438eff", 403 | "Objective-C++": "#6866fb", 404 | "Objective-J": "#ff0c5a", 405 | ObjectScript: "#424893", 406 | OCaml: "#ef7a08", 407 | Odin: "#60AFFE", 408 | Omgrofl: "#cabbff", 409 | ooc: "#b0b77e", 410 | Opa: null, 411 | Opal: "#f7ede0", 412 | "Open Policy Agent": "#7d9199", 413 | "OpenAPI Specification v2": "#85ea2d", 414 | "OpenAPI Specification v3": "#85ea2d", 415 | OpenCL: "#ed2e2d", 416 | "OpenEdge ABL": "#5ce600", 417 | OpenQASM: "#AA70FF", 418 | "OpenRC runscript": null, 419 | OpenSCAD: "#e5cd45", 420 | "Option List": "#476732", 421 | Org: "#77aa99", 422 | Ox: null, 423 | Oxygene: "#cdd0e3", 424 | Oz: "#fab738", 425 | P4: "#7055b5", 426 | Pact: "#F7A8B8", 427 | Pan: "#cc0000", 428 | Papyrus: "#6600cc", 429 | Parrot: "#f3ca0a", 430 | "Parrot Assembly": null, 431 | "Parrot Internal Representation": null, 432 | Pascal: "#E3F171", 433 | Pawn: "#dbb284", 434 | PDDL: "#0d00ff", 435 | "PEG.js": "#234d6b", 436 | Pep8: "#C76F5B", 437 | Perl: "#0298c3", 438 | PHP: "#4F5D95", 439 | PicoLisp: "#6067af", 440 | PigLatin: "#fcd7de", 441 | Pike: "#005390", 442 | "Pip Requirements": "#FFD343", 443 | Pkl: "#6b9543", 444 | PlantUML: "#fbbd16", 445 | PLpgSQL: "#336790", 446 | PLSQL: "#dad8d8", 447 | PogoScript: "#d80074", 448 | Polar: "#ae81ff", 449 | Pony: null, 450 | Portugol: "#f8bd00", 451 | PostCSS: "#dc3a0c", 452 | PostScript: "#da291c", 453 | "POV-Ray SDL": "#6bac65", 454 | PowerBuilder: "#8f0f8d", 455 | PowerShell: "#012456", 456 | Praat: "#c8506d", 457 | Prisma: "#0c344b", 458 | Processing: "#0096D8", 459 | Procfile: "#3B2F63", 460 | Prolog: "#74283c", 461 | Promela: "#de0000", 462 | "Propeller Spin": "#7fa2a7", 463 | Pug: "#a86454", 464 | Puppet: "#302B6D", 465 | PureBasic: "#5a6986", 466 | PureScript: "#1D222D", 467 | Pyret: "#ee1e10", 468 | Python: "#3572A5", 469 | "Python console": "#3572A5", 470 | "Python traceback": "#3572A5", 471 | q: "#0040cd", 472 | "Q#": "#fed659", 473 | QMake: null, 474 | QML: "#44a51c", 475 | "Qt Script": "#00b841", 476 | Quake: "#882233", 477 | R: "#198CE7", 478 | Racket: "#3c5caa", 479 | Ragel: "#9d5200", 480 | Raku: "#0000fb", 481 | RAML: "#77d9fb", 482 | Rascal: "#fffaa0", 483 | RBS: "#701516", 484 | RDoc: "#701516", 485 | REALbasic: null, 486 | Reason: "#ff5847", 487 | ReasonLIGO: "#ff5847", 488 | Rebol: "#358a5b", 489 | "Record Jar": "#0673ba", 490 | Red: "#f50000", 491 | Redcode: null, 492 | "Regular Expression": "#009a00", 493 | "Ren'Py": "#ff7f7f", 494 | RenderScript: null, 495 | ReScript: "#ed5051", 496 | reStructuredText: "#141414", 497 | REXX: "#d90e09", 498 | Rez: "#FFDAB3", 499 | Ring: "#2D54CB", 500 | Riot: "#A71E49", 501 | RMarkdown: "#198ce7", 502 | RobotFramework: "#00c0b5", 503 | Roc: "#7c38f5", 504 | Roff: "#ecdebe", 505 | "Roff Manpage": "#ecdebe", 506 | RON: "#a62c00", 507 | Rouge: "#cc0088", 508 | "RouterOS Script": "#DE3941", 509 | RPC: null, 510 | RPGLE: "#2BDE21", 511 | Ruby: "#701516", 512 | RUNOFF: "#665a4e", 513 | Rust: "#dea584", 514 | Sage: null, 515 | SaltStack: "#646464", 516 | SAS: "#B34936", 517 | Sass: "#a53b70", 518 | Scala: "#c22d40", 519 | Scaml: "#bd181a", 520 | Scenic: "#fdc700", 521 | Scheme: "#1e4aec", 522 | Scilab: "#ca0f21", 523 | SCSS: "#c6538c", 524 | sed: "#64b970", 525 | Self: "#0579aa", 526 | ShaderLab: "#222c37", 527 | Shell: "#89e051", 528 | "ShellCheck Config": "#cecfcb", 529 | ShellSession: null, 530 | Shen: "#120F14", 531 | Sieve: null, 532 | "Simple File Verification": "#C9BFED", 533 | Singularity: "#64E6AD", 534 | Slash: "#007eff", 535 | Slice: "#003fa2", 536 | Slim: "#2b2b2b", 537 | Slint: "#2379F4", 538 | Smali: null, 539 | Smalltalk: "#596706", 540 | Smarty: "#f0c040", 541 | Smithy: "#c44536", 542 | SmPL: "#c94949", 543 | SMT: null, 544 | Snakemake: "#419179", 545 | Solidity: "#AA6746", 546 | SourcePawn: "#f69e1d", 547 | SPARQL: "#0C4597", 548 | SQF: "#3F3F3F", 549 | SQL: "#e38c00", 550 | SQLPL: "#e38c00", 551 | Squirrel: "#800000", 552 | "SRecode Template": "#348a34", 553 | Stan: "#b2011d", 554 | "Standard ML": "#dc566d", 555 | Starlark: "#76d275", 556 | Stata: "#1a5f91", 557 | STL: "#373b5e", 558 | StringTemplate: "#3fb34f", 559 | Stylus: "#ff6347", 560 | "SubRip Text": "#9e0101", 561 | SugarSS: "#2fcc9f", 562 | SuperCollider: "#46390b", 563 | Svelte: "#ff3e00", 564 | SVG: "#ff9900", 565 | Sway: "#00F58C", 566 | Sweave: "#198ce7", 567 | Swift: "#F05138", 568 | SWIG: null, 569 | SystemVerilog: "#DAE1C2", 570 | Talon: "#333333", 571 | Tcl: "#e4cc98", 572 | Tcsh: null, 573 | templ: "#66D0DD", 574 | Terra: "#00004c", 575 | "Terraform Template": "#7b42bb", 576 | TeX: "#3D6117", 577 | TextGrid: "#c8506d", 578 | Textile: "#ffe7ac", 579 | "TextMate Properties": "#df66e4", 580 | Thrift: "#D12127", 581 | "TI Program": "#A0AA87", 582 | "TL-Verilog": "#C40023", 583 | TLA: "#4b0079", 584 | Toit: "#c2c9fb", 585 | TOML: "#9c4221", 586 | TSQL: "#e38c00", 587 | TSV: "#237346", 588 | TSX: "#3178c6", 589 | Turing: "#cf142b", 590 | Twig: "#c1d026", 591 | TXL: "#0178b8", 592 | TypeScript: "#3178c6", 593 | Typst: "#239dad", 594 | "Unified Parallel C": "#4e3617", 595 | "Unity3D Asset": "#222c37", 596 | "Unix Assembly": null, 597 | Uno: "#9933cc", 598 | UnrealScript: "#a54c4d", 599 | UrWeb: "#ccccee", 600 | V: "#4f87c4", 601 | Vala: "#a56de2", 602 | "Valve Data Format": "#f26025", 603 | VBA: "#867db1", 604 | VBScript: "#15dcdc", 605 | VCL: "#148AA8", 606 | "Velocity Template Language": "#507cff", 607 | Verilog: "#b2b7f8", 608 | VHDL: "#adb2cb", 609 | "Vim Help File": "#199f4b", 610 | "Vim Script": "#199f4b", 611 | "Vim Snippet": "#199f4b", 612 | "Visual Basic .NET": "#945db7", 613 | "Visual Basic 6.0": "#2c6353", 614 | Volt: "#1F1F1F", 615 | Vue: "#41b883", 616 | Vyper: "#2980b9", 617 | WDL: "#42f1f4", 618 | "Web Ontology Language": "#5b70bd", 619 | WebAssembly: "#04133b", 620 | "WebAssembly Interface Type": "#6250e7", 621 | WebIDL: null, 622 | WGSL: "#1a5e9a", 623 | Whiley: "#d5c397", 624 | Wikitext: "#fc5757", 625 | "Windows Registry Entries": "#52d5ff", 626 | wisp: "#7582D1", 627 | "Witcher Script": "#ff0000", 628 | Wollok: "#a23738", 629 | "World of Warcraft Addon Data": "#f7e43f", 630 | Wren: "#383838", 631 | X10: "#4B6BEF", 632 | xBase: "#403a40", 633 | XC: "#99DA07", 634 | XML: "#0060ac", 635 | "XML Property List": "#0060ac", 636 | Xojo: "#81bd41", 637 | Xonsh: "#285EEF", 638 | XProc: null, 639 | XQuery: "#5232e7", 640 | XS: null, 641 | XSLT: "#EB8CEB", 642 | Xtend: "#24255d", 643 | Yacc: "#4B6C4B", 644 | YAML: "#cb171e", 645 | YARA: "#220000", 646 | YASnippet: "#32AB90", 647 | Yul: "#794932", 648 | ZAP: "#0d665e", 649 | Zeek: null, 650 | ZenScript: "#00BCD1", 651 | Zephir: "#118f9e", 652 | Zig: "#ec915c", 653 | ZIL: "#dc75e5", 654 | Zimpl: "#d67711", 655 | }; 656 | -------------------------------------------------------------------------------- /utils/languages.ts: -------------------------------------------------------------------------------- 1 | 2 | import { RawLanguageData } from "@/types/Languages"; 3 | 4 | export default async function LangData(user: string) { 5 | const graph = await fetch("https://api.github.com/graphql", { 6 | method: "POST", 7 | headers: { 8 | "Cache-Control": "private; stale-while-revalidate=3600", 9 | "Content-Type": "application/json", 10 | Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 11 | }, 12 | body: JSON.stringify({ 13 | query: ` 14 | query { 15 | user(login: "${user}") { 16 | repositories( 17 | first: 100 18 | ) { 19 | edges { 20 | node { 21 | languages(first: 10) { 22 | totalSize 23 | edges { 24 | size 25 | node { 26 | name 27 | color 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | `, 37 | variables: {}, 38 | }), 39 | }); 40 | 41 | const data: RawLanguageData = await graph.json(); 42 | 43 | return data 44 | } -------------------------------------------------------------------------------- /utils/repo.ts: -------------------------------------------------------------------------------- 1 | import { RawRepoData } from "@/types/Repo"; 2 | 3 | export default async function RepoData(user: string, repo: string) { 4 | const graph = await fetch("https://api.github.com/graphql", { 5 | method: "POST", 6 | headers: { 7 | "Content-Type": "application/json", 8 | "Cache-Control": "private; stale-while-revalidate=3600", 9 | Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 10 | }, 11 | body: JSON.stringify({ 12 | query: ` 13 | query { 14 | repositoryOwner(login:"${user}") { 15 | repository(name:"${repo}") { 16 | name 17 | description 18 | stargazerCount 19 | forkCount 20 | primaryLanguage { 21 | name 22 | color 23 | } 24 | } 25 | } 26 | } 27 | `, 28 | variables: {}, 29 | }), 30 | }); 31 | 32 | const data: RawRepoData = await graph.json(); 33 | return data; 34 | } 35 | -------------------------------------------------------------------------------- /utils/repositories.ts: -------------------------------------------------------------------------------- 1 | export default async function RepoList(user: string) { 2 | const graph = await fetch("https://api.github.com/graphql", { 3 | method: "POST", 4 | headers: { 5 | "Content-Type": "application/json", 6 | "Cache-Control": "private; stale-while-revalidate=3600", 7 | Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 8 | }, 9 | body: JSON.stringify({ 10 | query: ` 11 | query { 12 | user(login: "${user}") { 13 | repositories(first: 1, orderBy: {direction: DESC, field: PUSHED_AT}) { 14 | edges { 15 | node { 16 | id 17 | url 18 | name 19 | description 20 | primaryLanguage { 21 | name 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | variables: {}, 30 | }), 31 | }); 32 | 33 | const data = await graph.json(); 34 | return data; 35 | } 36 | -------------------------------------------------------------------------------- /utils/users.ts: -------------------------------------------------------------------------------- 1 | import { RawUserData } from "@/types/UserStats"; 2 | 3 | export default async function UserData(user: string) { 4 | const graph = await fetch("https://api.github.com/graphql", { 5 | method: "POST", 6 | headers: { 7 | "Content-Type": "application/json", 8 | "Cache-Control": "private; stale-while-revalidate=3600", 9 | Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 10 | }, 11 | body: JSON.stringify({ 12 | query: ` 13 | query { 14 | user(login: "${user}") { 15 | contributionsCollection { 16 | totalCommitContributions 17 | totalPullRequestContributions 18 | totalIssueContributions 19 | contributionCalendar { 20 | totalContributions 21 | } 22 | totalRepositoriesWithContributedCommits 23 | } 24 | repositories(first: 100) { 25 | nodes { 26 | stargazers { 27 | totalCount 28 | } 29 | } 30 | } 31 | issues { 32 | totalCount 33 | } 34 | pullRequests { 35 | totalCount 36 | } 37 | followers { 38 | totalCount 39 | } 40 | } 41 | } 42 | `, 43 | variables: {}, 44 | }), 45 | }); 46 | 47 | const data: RawUserData = await graph.json(); 48 | return data; 49 | } 50 | -------------------------------------------------------------------------------- /utils/wakatime.ts: -------------------------------------------------------------------------------- 1 | import { RawWakaData } from "@/types/Waka"; 2 | 3 | export default async function Wakatime(user: string) { 4 | const url = `https://api.wakatime.com/api/v1/users/${encodeURIComponent(user)}/stats?is_including_today=true&range=all_time`; 5 | 6 | const response = await fetch(url, { 7 | method: "GET" 8 | }); 9 | 10 | const data: RawWakaData = await response.json(); 11 | return data; 12 | } 13 | --------------------------------------------------------------------------------