Skip to content

Commit 2b904d0

Browse files
committed
more fun stats
1 parent 22fdac8 commit 2b904d0

27 files changed

Lines changed: 945 additions & 223 deletions

app/globals.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,18 @@ body {
1515
@layer base {
1616
:root {
1717
--radius: 0.5rem;
18+
--background: 0 0% 100%;
19+
--foreground: 222 47% 11%;
20+
}
21+
22+
.dark {
23+
--background: 222 47% 11%;
24+
--foreground: 0 0% 100%;
25+
}
26+
}
27+
28+
@layer base {
29+
body {
30+
@apply bg-background text-foreground;
1831
}
1932
}

app/layout.tsx

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "./globals.css";
33
import { Separator } from "@/components/ui/separator";
44
import { Github, Twitter } from "lucide-react";
55
import Link from "next/link";
6+
import { ThemeProvider } from "@/components/theme-provider";
67

78
export const metadata: Metadata = {
89
title: process.env.NEXT_PUBLIC_APP_NAME || "6 Degrees Github Stats",
@@ -15,29 +16,31 @@ export default function RootLayout({
1516
children: React.ReactNode;
1617
}>) {
1718
return (
18-
<html lang="en">
19-
<body className="bg-gray-50">
20-
{children}
19+
<html lang="en" suppressHydrationWarning>
20+
<body className="bg-gray-100 text-gray-900 dark:bg-[#0c175c] dark:text-gray-50">
21+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
22+
{children}
2123

22-
<footer className="text-gray-500 container mx-auto p-4 ">
23-
<Separator className="my-4" />
24-
<div className="grid lg:grid-cols-2">
25-
<span>
26-
Developed with ❤️ by{" "}
27-
<Link className="underline" target="_blank" href="https://www.6degrees.com.sa">
28-
6 Degrees
29-
</Link>
30-
</span>
31-
<span className="flex gap-4 justify-end">
32-
<Link target="_blank" href="https://www.github.com/mo9a7i/github_stats">
33-
<Github />
34-
</Link>
35-
<Link target="_blank" href="https://x.com/6degrees_sa">
36-
<Twitter />
37-
</Link>
38-
</span>
39-
</div>
40-
</footer>
24+
<footer className="text-gray-500 container mx-auto p-4 ">
25+
<Separator className="my-4" />
26+
<div className="grid lg:grid-cols-2">
27+
<span>
28+
Developed with ❤️ by{" "}
29+
<Link className="underline" target="_blank" href="https://www.6degrees.com.sa">
30+
6 Degrees
31+
</Link>
32+
</span>
33+
<span className="flex gap-4 justify-end">
34+
<Link target="_blank" href="https://www.github.com/mo9a7i/github_stats">
35+
<Github />
36+
</Link>
37+
<Link target="_blank" href="https://x.com/6degrees_sa">
38+
<Twitter />
39+
</Link>
40+
</span>
41+
</div>
42+
</footer>
43+
</ThemeProvider>
4144
</body>
4245
</html>
4346
);

app/metadata.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ const defaultMetadata: Metadata = {
4141
}
4242
},
4343
manifest: `${getBasePath()}/site.webmanifest`,
44-
themeColor: '#ffffff',
45-
viewport: 'width=device-width, initial-scale=1',
4644
applicationName: process.env.NEXT_PUBLIC_APP_NAME
4745
};
4846

app/page.tsx

Lines changed: 146 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,23 @@ import { GitHubOrgList } from '../components/github-org-list';
22
import { getCache, setCache } from '@/lib/cache';
33
import { Suspense } from 'react';
44
import { StatsCards } from '@/components/stats-cards';
5-
import { GitHubRepo } from './types/github';
5+
import { GitHubRepo, LanguageLinesStats } from './types/github';
66
import { Metadata } from 'next';
77
import defaultMetadata from './metadata';
8+
import { ThemeToggle } from '@/components/theme-toggle';
9+
10+
const LANGUAGE_COLORS: Record<string, string> = {
11+
TypeScript: "#3178c6",
12+
JavaScript: "#f1e05a",
13+
Python: "#3572A5",
14+
HTML: "#e34c26",
15+
Vue: "#41b883",
16+
Astro: "#f0f0f0",
17+
PHP: "#4F5D95",
18+
Java: "#b07219",
19+
"C#": "#178600",
20+
Other: "#6e7681"
21+
};
822

923
// Add configuration type
1024
interface GitHubConfig {
@@ -165,9 +179,9 @@ export default async function Home() {
165179
// For users, we need to fetch all repos (including private ones if token has access)
166180
const reposUrl = isUser
167181
? userData.login === org
168-
? `${config.baseUrl}/user/repos?per_page=100&type=owner&sort=updated` // For authenticated user
182+
? `${config.baseUrl}/user/repos?per_page=100&affiliation=owner&sort=updated` // For authenticated user
169183
: `${config.baseUrl}/users/${org}/repos?per_page=100&type=owner&sort=updated` // For other users
170-
: `${config.baseUrl}/orgs/${org}/repos?per_page=100`;
184+
: `${config.baseUrl}/orgs/${org}/repos?per_page=100&type=all`;
171185

172186
const [orgResponse, reposResponse] = await Promise.all([
173187
isUser
@@ -181,6 +195,12 @@ export default async function Home() {
181195
reposResponse.json()
182196
]);
183197

198+
// Add error handling for reposData
199+
if (!Array.isArray(reposData)) {
200+
console.error(`Invalid repos data for ${org}:`, reposData);
201+
return { ...orgData, repos: [] };
202+
}
203+
184204
console.log(`Fetched ${reposData.length} repositories for ${isUser ? 'user' : 'org'} ${org}`);
185205

186206
// Enhance each repository with additional details
@@ -194,12 +214,17 @@ export default async function Home() {
194214
return cachedRepoData;
195215
}
196216

197-
const [contributorsResponse, commitsResponse] = await Promise.all([
217+
const [contributorsResponse, commitsResponse, languagesResponse] = await Promise.all([
198218
fetch(repo.contributors_url, { headers: config.headers }),
199-
fetch(`${repo.url}/commits?per_page=1`, { headers: config.headers })
219+
fetch(`${repo.url}/commits?per_page=1`, { headers: config.headers }),
220+
fetch(repo.languages_url, { headers: config.headers })
221+
]);
222+
223+
const [contributorsData, languagesData] = await Promise.all([
224+
contributorsResponse.json(),
225+
languagesResponse.json()
200226
]);
201227

202-
const contributorsData = await contributorsResponse.json();
203228
const commitsLinkHeader = commitsResponse.headers.get("Link");
204229
let commitsCount = null;
205230

@@ -214,6 +239,7 @@ export default async function Home() {
214239
...repo,
215240
contributors: Array.isArray(contributorsData) ? contributorsData.slice(0, 5) : [],
216241
commits_count: commitsCount,
242+
languages_stats: languagesData
217243
};
218244

219245
// Cache individual repo data
@@ -266,46 +292,124 @@ export default async function Home() {
266292
const totalPrivateRepos = orgsData.reduce((sum, org) => sum + (org.total_private_repos || 0), 0);
267293
const totalRepos = totalPublicRepos + totalPrivateRepos;
268294

269-
// Calculate total stats
270-
const totalStats = {
271-
totalRepos,
272-
publicRepos: totalPublicRepos,
273-
privateRepos: totalPrivateRepos,
274-
totalStars: orgsData.reduce((sum, org) =>
275-
sum + org.repos.reduce((repoSum: number, repo: GitHubRepo) => repoSum + repo.stargazers_count, 0), 0
276-
),
277-
totalForks: orgsData.reduce((sum, org) =>
278-
sum + org.repos.reduce((repoSum: number, repo: GitHubRepo) => repoSum + repo.forks_count, 0), 0
279-
),
280-
organizations: orgsData.map(org => ({
281-
login: org.login,
282-
avatar_url: org.avatar_url,
283-
name: org.name || org.login
284-
})),
285-
contributors: Array.from(new Set(
286-
orgsData.flatMap(org =>
287-
org.repos.flatMap((repo: GitHubRepo) => repo.contributors)
288-
).map(c => JSON.stringify({ login: c.login, avatar_url: c.avatar_url }))
289-
)).map(str => JSON.parse(str)),
290-
totalContributors: new Set(
291-
orgsData.flatMap((org: any) =>
292-
org.repos.flatMap((repo: GitHubRepo) => repo.contributors.map(c => c.login))
295+
// Calculate total lines stats
296+
const languageBytesStats = orgsData.reduce((stats: Record<string, number>, org) => {
297+
org.repos.forEach((repo: GitHubRepo) => {
298+
if (repo.languages_stats) {
299+
Object.entries(repo.languages_stats).forEach(([lang, bytes]) => {
300+
stats[lang] = (stats[lang] || 0) + bytes;
301+
});
302+
}
303+
});
304+
return stats;
305+
}, {});
306+
307+
const totalBytes = Object.values(languageBytesStats).reduce((sum, bytes) => sum + bytes, 0);
308+
const BYTES_PER_LINE = 120; // Average line length estimation
309+
const LINES_PER_HOUR = 50 / 8; // 50 lines per day (8 hours)
310+
const totalLines = Math.round(totalBytes / BYTES_PER_LINE);
311+
const totalHours = Math.round(totalLines / LINES_PER_HOUR);
312+
313+
function createLanguageStats(
314+
name: string,
315+
bytes: number,
316+
percentage: number,
317+
color?: string
318+
): LanguageLinesStats {
319+
return {
320+
name,
321+
bytes,
322+
lines: Math.round(bytes / BYTES_PER_LINE),
323+
percentage,
324+
color
325+
};
326+
}
327+
328+
// Get top 5 languages by bytes and combine rest into "Others"
329+
const topLanguagesByBytes = Object.entries(languageBytesStats)
330+
.sort(([, a], [, b]) => b - a)
331+
.reduce<LanguageLinesStats[]>((acc, [name, bytes], index) => {
332+
if (index < 5) {
333+
acc.push(createLanguageStats(name, bytes, (bytes / totalBytes) * 100, LANGUAGE_COLORS[name] || LANGUAGE_COLORS.Other));
334+
} else if (index === 5) {
335+
const otherBytes = Object.entries(languageBytesStats).slice(5).reduce((sum, [, bytes]) => sum + bytes, 0);
336+
acc.push(createLanguageStats('Others', otherBytes, (otherBytes / totalBytes) * 100, LANGUAGE_COLORS.Other));
337+
}
338+
return acc;
339+
}, []);
340+
341+
// Calculate total stats
342+
const totalStats = {
343+
totalRepos,
344+
publicRepos: totalPublicRepos,
345+
privateRepos: totalPrivateRepos,
346+
totalStars: orgsData.reduce((sum, org) =>
347+
sum + org.repos.reduce((repoSum: number, repo: GitHubRepo) => repoSum + repo.stargazers_count, 0), 0
348+
),
349+
totalForks: orgsData.reduce((sum, org) =>
350+
sum + org.repos.reduce((repoSum: number, repo: GitHubRepo) => repoSum + repo.forks_count, 0), 0
351+
),
352+
totalIssues: orgsData.reduce((sum, org) =>
353+
sum + org.repos.reduce((repoSum: number, repo: GitHubRepo) => repoSum + repo.open_issues || 0, 0), 0
354+
),
355+
organizations: orgsData.map(org => ({
356+
login: org.login,
357+
avatar_url: org.avatar_url,
358+
name: org.name || org.login
359+
})),
360+
contributors: Array.from(new Set(
361+
orgsData.flatMap(org =>
362+
org.repos.flatMap((repo: GitHubRepo) => repo.contributors)
363+
).map(c => JSON.stringify({ login: c.login, avatar_url: c.avatar_url }))
364+
)).map(str => JSON.parse(str)),
365+
totalContributors: new Set(
366+
orgsData.flatMap((org: any) =>
367+
org.repos.flatMap((repo: GitHubRepo) => repo.contributors.map(c => c.login))
368+
)
369+
).size,
370+
totalCommits: orgsData.reduce((sum, org) =>
371+
sum + org.repos.reduce((repoSum: number, repo: GitHubRepo) => repoSum + (repo.commits_count || 0), 0), 0
372+
),
373+
lastUpdated: new Date().toISOString(),
374+
languages: Object.entries(
375+
orgsData.reduce((langs: Record<string, number>, org) => {
376+
org.repos.forEach((repo: GitHubRepo) => {
377+
if (repo.language) {
378+
langs[repo.language] = (langs[repo.language] || 0) + 1;
379+
}
380+
});
381+
return langs;
382+
}, {})
293383
)
294-
).size,
295-
totalCommits: orgsData.reduce((sum, org) =>
296-
sum + org.repos.reduce((repoSum: number, repo: GitHubRepo) => repoSum + (repo.commits_count || 0), 0), 0
297-
),
298-
lastUpdated: new Date().toISOString(),
299-
};
384+
.map(([name, count]) => ({
385+
name,
386+
count,
387+
lines: Math.round(count / BYTES_PER_LINE),
388+
percentage: (count / totalRepos) * 100
389+
}))
390+
.sort((a, b) => b.count - a.count)
391+
.slice(0, 5),
392+
languageBytes: {
393+
languages: topLanguagesByBytes,
394+
totalBytes
395+
},
396+
developmentStats: {
397+
totalLines,
398+
totalHours
399+
}
400+
};
300401

301402
return (
302403
<main className="container mx-auto p-4">
303-
<h1 className="text-4xl font-bold mb-8">
304-
{process.env.NEXT_PUBLIC_APP_NAME}
305-
<small className="block font-thin text-base text-gray-700">
306-
{process.env.NEXT_PUBLIC_APP_DESCRIPTION}
307-
</small>
308-
</h1>
404+
<div className="flex justify-between items-center mb-8">
405+
<h1 className="text-4xl font-bold">
406+
{process.env.NEXT_PUBLIC_APP_NAME}
407+
<small className="block font-thin text-base text-gray-700 dark:text-gray-300">
408+
{process.env.NEXT_PUBLIC_APP_DESCRIPTION}
409+
</small>
410+
</h1>
411+
<ThemeToggle />
412+
</div>
309413
<StatsCards stats={totalStats} />
310414
<Suspense fallback={<GitHubOrgList orgsData={[]} loading={true} />}>
311415
<GitHubOrgList orgsData={sortedOrgsData} />

app/types/github.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,26 @@ export interface GitHubRepo {
1313
name: string;
1414
description: string | null;
1515
html_url: string;
16-
contributors_url: string;
17-
commits_count: number | null;
18-
open_issues_count: number;
19-
subscribers_count: number;
16+
private: boolean;
17+
fork: boolean;
2018
stargazers_count: number;
21-
open_issues: number;
19+
watchers_count: number;
2220
forks_count: number;
23-
contributors: GitHubContributor[];
24-
private: boolean;
21+
open_issues: number;
22+
subscribers_count: number;
2523
language: string | null;
2624
topics: string[];
2725
pushed_at: string;
26+
contributors_url: string;
27+
contributors: Array<{
28+
id: number;
29+
login: string;
30+
avatar_url: string;
31+
html_url: string;
32+
}>;
33+
commits_count: number | null;
34+
languages_url: string;
35+
languages_stats?: Record<string, number>; // bytes of code per language
2836
}
2937

3038
export interface GitHubContributor {
@@ -40,4 +48,21 @@ export interface GitHubApiUser {
4048
avatar_url: string;
4149
public_repos: number;
4250
type: 'User';
43-
}
51+
}
52+
53+
export interface LanguageStats {
54+
name: string;
55+
count: number;
56+
lines: number;
57+
percentage: number;
58+
color?: string; // GitHub language colors
59+
}
60+
61+
export interface LanguageLinesStats {
62+
name: string;
63+
bytes: number;
64+
lines?: number;
65+
percentage: number;
66+
color?: string;
67+
}
68+

app/viewport.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const viewport = {
2+
themeColor: '#ffffff',
3+
width: 'device-width',
4+
initialScale: 1
5+
}

0 commit comments

Comments
 (0)