diff --git a/src/app/api/metrics/profile-stats/route.ts b/src/app/api/metrics/profile-stats/route.ts new file mode 100644 index 0000000..a8ecfac --- /dev/null +++ b/src/app/api/metrics/profile-stats/route.ts @@ -0,0 +1,74 @@ +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; + +export const dynamic = "force-dynamic"; + +const GITHUB_API = "https://api.github.com"; + +type GithubRepo = { + stargazers_count: number; + forks_count: number; +}; + +export async function GET() { + const session = await getServerSession(authOptions); + + if (!session?.accessToken || !session.githubLogin) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userRes = await fetch(`${GITHUB_API}/user`, { + headers: { + Authorization: `Bearer ${session.accessToken}`, + Accept: "application/vnd.github+json", + }, + cache: "no-store", + }); + + if (!userRes.ok) { + return Response.json( + { error: "Failed to fetch profile stats" }, + { status: 502 } + ); + } + + const user = await userRes.json(); + + const reposRes = await fetch( + `${GITHUB_API}/user/repos?per_page=100&type=owner&sort=updated`, + { + headers: { + Authorization: `Bearer ${session.accessToken}`, + Accept: "application/vnd.github+json", + }, + cache: "no-store", + } + ); + + if (!reposRes.ok) { + return Response.json( + { error: "Failed to fetch repo stats" }, + { status: 502 } + ); + } + + const repos = (await reposRes.json()) as GithubRepo[]; + + const totalStars = repos.reduce( + (sum, repo) => sum + (repo.stargazers_count ?? 0), + 0 + ); + + const totalForks = repos.reduce( + (sum, repo) => sum + (repo.forks_count ?? 0), + 0 + ); + + return Response.json({ + memberSince: user.created_at, + publicRepos: user.public_repos, + totalStars, + totalForks, + followers: user.followers, + }); +} \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 23f9881..2fbe9cd 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -17,6 +17,7 @@ import WeeklySummaryCard from "@/components/WeeklySummaryCard"; import ExportButton from "@/components/ExportButton"; import Link from "next/link"; import PersonalRecords from "@/components/PersonalRecords"; +import ProfileStats from "@/components/ProfileStats"; import { authOptions } from "@/lib/auth"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; @@ -31,27 +32,32 @@ export default async function DashboardPage() { return (
-
+ +
Settings
+
+
+ +
+
- {/* Row 1: Contribution graph + Streak + Friend Comparison */} -
+
@@ -65,32 +71,28 @@ export default async function DashboardPage() {
- {/* Row 2: PR metrics, PR breakdown & Time Chart */} -
+
- {/* Row 3: Issue metrics + CI analytics */} -
+
- {/* Row 4: Pinned repositories */}
- {/* Row 5: Top repos + Language breakdown + Goal tracker */} -
+
); -} +} \ No newline at end of file diff --git a/src/components/ProfileStats.tsx b/src/components/ProfileStats.tsx new file mode 100644 index 0000000..6031804 --- /dev/null +++ b/src/components/ProfileStats.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +type ProfileStatsData = { + memberSince: string; + publicRepos: number; + totalStars: number; + totalForks: number; + followers: number; +}; + +type StatCard = { + label: string; + value: string; + icon: string; +}; + +function formatCompactNumber(value: number) { + return new Intl.NumberFormat("en", { + notation: "compact", + maximumFractionDigits: 1, + }).format(value); +} + +function formatMemberSince(dateString: string) { + const date = new Date(dateString); + + return new Intl.DateTimeFormat("en", { + month: "short", + year: "numeric", + }).format(date); +} + +export default function ProfileStats() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch("/api/metrics/profile-stats") + .then(async (r) => { + const res = await r.json(); + + if (!r.ok) { + throw new Error(res.error || "Failed to load profile stats"); + } + + return res as ProfileStatsData; + }) + .then((res) => setData(res)) + .catch((error) => { + console.error(error); + setData(null); + }) + .finally(() => setLoading(false)); + }, []); + + const stats: StatCard[] = useMemo(() => { + if (!data) return []; + + return [ + { + label: "Member Since", + value: formatMemberSince(data.memberSince), + icon: "📅", + }, + { + label: "Public Repos", + value: formatCompactNumber(data.publicRepos), + icon: "📦", + }, + { + label: "Total Stars", + value: formatCompactNumber(data.totalStars), + icon: "⭐", + }, + { + label: "Total Forks", + value: formatCompactNumber(data.totalForks), + icon: "🍴", + }, + { + label: "Followers", + value: formatCompactNumber(data.followers), + icon: "👥", + }, + ]; + }, [data]); + + return ( +
+
+

+ GitHub Profile Stats +

+
+ +
+ {loading + ? Array.from({ length: 5 }).map((_, i) => ( +
+
+
+
+ )) + : stats.map((stat) => ( +
+
+ {stat.icon} + {stat.label} +
+ +
+ {stat.value} +
+
+ ))} +
+
+ ); +}