Back to blogs
Blog
Writing in public
Longer posts — published here first, then cross-posted to Dev.to with a canonical URL back to this site.
Blog
Longer posts — published here first, then cross-posted to Dev.to with a canonical URL back to this site.
Back to blogs
Back to blogs
Postreact, portfolio, tailwindcss, nextjs
Are developers engineers? Maybe not in the strictest sense, but sometimes we like to geek out and...
Date published

Why You Might Still Pick Next.js Over TanStack Start From a TanStack Start...

A complete theme management system for React applications with SSR support! ✨ 🌟...

Modify your nextauth client and add a callbacks section that will get the access token from the...
Are developers engineers? Maybe not in the strictest sense, but sometimes we like to geek out and over-engineer things, especially when it comes to our own portfolios.
Portfolio websites are the perfect playground for unleashing our creativity and showcasing our skills without worrying about going overboard. In this mini-series, we'll be transforming our portfolio site into an expressive masterpiece that truly reflects who we are as developers.
To begin, we'll lay the groundwork with our signature color scheme and a hero section featuring a striking image of ourselves. Get ready to dive into the world of code and design as we breathe life into our digital canvas!

Next, we'll unveil a curated showcase of our most impressive projects, seamlessly integrated into the site. While hardcoding them is an option, we'll opt for a more dynamic approach by fetching them directly from our GitHub repository. This ensures our portfolio always reflects our latest and greatest creations.
But why stop at a simple project list when we can paint a richer portrait of our coding prowess? Let's infuse the site with captivating programming language statistics, revealing the languages we've mastered and the depths of our expertise. While pre-built solutions like github-readme-stats that we can embed directly into our markdown or html , It's limitation is that it'll only fetch the first 100 repos.
1const fetcher = (variables, token) => {2 return request(3 {4 query: `5 query userInfo($login: String!) {6 user(login: $login) {7 # fetch only owner repos & not forks8 repositories(ownerAffiliations: OWNER, isFork: false, first: 100) {9 nodes {10 name11 languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {12 edges {13 size14 node {15 color16 name17 }18 }19 }20 }21 }22 }23 }24 `,25 variables,26 },27 {28 Authorization: `token ${token}`,29 },30 );31};
This is good enough but in my case I have at least 200 (i plan to delete most of these as soon as I fix up another tool for that 😜)
So I made a recursive version of it to fetch all our repositories
1export async function getViewerRepos(2 viewer_token: string,3 cursor: string | null = null,4): Promise<{ data: ViewerRepos | null; error: BadDataGitHubError | null }> {56 const query = `7 query($first: Int!,$after: String) {8 viewer {9 repositories(10 first: $first11 after: $after12 isFork: false13 orderBy: {field: PUSHED_AT, direction: DESC}14 ) {1516 edges {17 node {18 id19 name20 nameWithOwner21 languages(first: 10) {22 edges {23 node {24 id25 name26 color27 }28 }29 }30 }31 }32 totalCount33 pageInfo {34 endCursor35 startCursor36 }37 }38 }39}40`;41 try {42 const response = await fetch("https://api.github.com/graphql", {43 method: "POST",44 headers: {45 "Authorization": `bearer ${viewer_token}`,46 "Content-Type": "application/json",47 "accept": "application/vnd.github.hawkgirl-preview+json",48 },49 body: JSON.stringify({50 query,51 variables: {52 first: 50,53 after: cursor,54 },55 // operationName,56 }),57 });58 const data = await response.json() as unknown as ViewerRepos;5960 if ("message" in data) {61 console.log("throw error fetching viewer repos ==> ", data);62 return { data: null, error: data as unknown as BadDataGitHubError };63 }64 // logSuccess("all user repositories ===== ", data);65 return { data, error: null };66 } catch (err) {67 console.log("catch error fetching viewer repos ==> ", err);68 return { data: null, error: err as BadDataGitHubError };69 }70}7172export interface ViewerRepos {73 data: Data;74}7576export interface Data {77 viewer: Viewer;78}7980export interface Viewer {81 repositories: Repositories;82}8384export interface Repositories {85 edges: Edge[];86 totalCount: number;87 pageInfo: PageInfo;88}8990export interface Edge {91 cursor: string;92 node: Node;93}9495export interface Node {96 id: string;97 name: string;98 nameWithOwner: string;99 languages: Languages;100}101102export interface Languages {103 edges: LanguageEdge[];104}105106export interface LanguageEdge {107 node: LanguageNode;108}109110export interface LanguageNode {111 id: string;112 name: string;113 color: string;114}115116export interface PageInfo {117 endCursor: string;118 startCursor: string;119}120121export interface BadDataGitHubError {122 message: string;123 documentation_url: string;124}125
recursively fetch all the repos
1async function fetchReposRecursivelyWithGQL({2 viewer_token,3 all_repos = [],4 cursor,5}: FetchRepoRecursivelyWithGQL) {6 try {7 const repos = await getViewerRepos(viewer_token, cursor);8 if (repos.data) {9 const fetched_repos = repos.data.data.viewer.repositories.edges;10 const totalCount = repos.data.data.viewer.repositories.totalCount;11 const next_cursor =12 repos.data.data.viewer.repositories.pageInfo.endCursor;13 const new_repos = all_repos.concat(fetched_repos);1415 console.log({16 fetched_repos_count: new_repos.length,17 totalCount,18 next_cursor,19 });2021 if (new_repos.length < totalCount) {22 return fetchReposRecursivelyWithGQL({23 viewer_token,24 cursor: next_cursor,25 all_repos: new_repos,26 });27 }28 return new_repos;29 }30 } catch (error) {31 logError(error);32 }33}34
then we'll loop over these with .reduce and output the languages stats and return an array of type
1const top_langs: {2 name: string;3 color: string;4 percentage: number;5}[] | undefined
12import { LanguagePercentage } from "./helpers";34interface GithubLangiagesProps {5 top_langs: LanguagePercentage[];67}89export function GithubLangiagesPercentage({top_langs}:GithubLangiagesProps){1011if(!top_langs){12 return null13}1415return (16 <div className="w-full h-full flex items-center py-2 ">17 <div className="w-full h-full">18 <ul className="w-full flex flex-wrap list-none m-0 px-5 gap-3 ">19 {top_langs.map(({color,name,percentage}, index: number) => {20 const percent = percentage21 const percetage = percent < 5 ? percent + 2 : percent;22 if(percentage<1){23 return null24 }25 return (26 <li27 key={name + index}28 className=" md:max-w-[70%] min-w-fit gap-1 flex flex-col justify-center "29 style={{30 width: `${percetage}%`,31 }}>32 <div33 className="min-w-[20%] rounded-xl w-full h-4"34 style={{35 backgroundColor: color ?? "",36 }}>3738 </div>39 <div className="pl-1 text-xs flex min-w-fit gap-2">40 <div>{name}</div>41 <div> {percentage}%</div>42 </div>43 </li>44 );45 })}46 </ul>47 </div>48 </div>49);50}
finally we display the stats bars
1export async function GithubLanguages({}: LanguagesProps) {2 const top_langs = await getGithubViewerLanguages();3 if (!top_langs) return;45 return (6 <div7 className=" w-full h-full p-58 flex flex-wrap items-center justify-center text-xs md:text-base9 gap-2 rounded-xl ">10 <SectionHeader heading="Languages Stats on Github" id="stats" />11 <GithubLangiagesPercentage top_langs={top_langs} />12 </div>13 );14}
 [](https://tigawanna-portfolio.vercel.app/#stats)
In the next part we fix react's biggest problem: the lack of a cool ".react" file extension like Vue or svelte , because how else are people going to know what all that html,CSS and JavaScript was being used for