diff --git a/src/app/api/metrics/prs/route.ts b/src/app/api/metrics/prs/route.ts index 1972c33..6194ac1 100644 --- a/src/app/api/metrics/prs/route.ts +++ b/src/app/api/metrics/prs/route.ts @@ -10,16 +10,23 @@ import { GITHUB_API } from "@/lib/github"; import { supabaseAdmin } from "@/lib/supabase"; export const dynamic = "force-dynamic"; - -interface PRMetricsBase { +interface PullRequest { + title: string; + created_at: string; + html_url: string; + state: string; +}interface PRMetricsBase { open: number; merged: number; total: number; avgReviewHours: number; mergeRate: number; + prs: PullRequest[]; } async function fetchPRMetrics(token: string): Promise { + + const searchRes = await fetch( `${GITHUB_API}/search/issues?q=type:pr+author:@me&per_page=100`, { @@ -34,15 +41,14 @@ async function fetchPRMetrics(token: string): Promise { const data = (await searchRes.json()) as { total_count: number; - items: Array<{ - state: string; - created_at: string; - closed_at: string | null; - // GitHub Search API includes a pull_request object on PR items. - // merged_at is non-null only when the PR was actually merged, as - // opposed to closed without merging. - pull_request?: { merged_at: string | null }; - }>; +items: Array<{ + title: string; + state: string; + created_at: string; + closed_at: string | null; + html_url: string; + pull_request?: { merged_at: string | null }; +}>; }; const open = data.items.filter((pr) => pr.state === "open").length; @@ -69,6 +75,12 @@ async function fetchPRMetrics(token: string): Promise { 0 ) / mergedPRs.length : 0; + const prs = data.items.map((pr) => ({ + title: pr.title, + created_at: pr.created_at, + html_url: pr.html_url, + state: pr.state, +})); // Use the number of fetched items as the denominator for mergeRate. // data.total_count is the all-time GitHub total (potentially thousands) @@ -77,13 +89,14 @@ async function fetchPRMetrics(token: string): Promise { // (open + merged + closed-without-merge) is the correct base. const sampleTotal = data.items.length; - return { - open, - merged, - total: data.total_count, - avgReviewHours: Math.round(avgReviewMs / 3600000), - mergeRate: sampleTotal > 0 ? merged / sampleTotal : 0, - }; + return { + open, + merged, + total: data.total_count, + avgReviewHours: Math.round(avgReviewMs / 3600000), + mergeRate: sampleTotal > 0 ? merged / sampleTotal : 0, + prs, +}; } function formatPRMetrics(metrics: PRMetricsBase) { @@ -96,6 +109,7 @@ function formatPRMetrics(metrics: PRMetricsBase) { metrics.total > 0 ? `${Math.round(metrics.mergeRate * 100)}%` : "0%", + prs: metrics.prs, }; } @@ -154,6 +168,7 @@ export async function GET(req: NextRequest) { return { open: a.open + b.open, + prs: [...a.prs, ...b.prs], merged: mergedCount, total, avgReviewHours: Math.round(avgReviewHours * 10) / 10, diff --git a/src/app/api/metrics/streak/route.ts b/src/app/api/metrics/streak/route.ts index fe01d11..943fc19 100644 --- a/src/app/api/metrics/streak/route.ts +++ b/src/app/api/metrics/streak/route.ts @@ -25,7 +25,6 @@ async function fetchActiveDates( since.setDate(since.getDate() - 90); const sinceStr = since.toISOString().slice(0, 10); - const activeDates = new Set(); // Paginate through all results. GitHub Search API caps responses at 100 // items per page and 1000 items total (10 pages). Without pagination, // active users with more than 100 commits in the window have their oldest @@ -52,6 +51,8 @@ async function fetchActiveDates( items: Array<{ commit: { author: { date: string } } }>; }; + + for (const item of data.items) { activeDates.add(item.commit.author.date.slice(0, 10)); } diff --git a/src/app/u/[username]/page.tsx b/src/app/u/[username]/page.tsx index c587a71..a15604f 100644 --- a/src/app/u/[username]/page.tsx +++ b/src/app/u/[username]/page.tsx @@ -3,6 +3,7 @@ import BadgeSection from "@/components/BadgeSection"; import ContributionGraph from "@/components/ContributionGraph"; import StreakTracker from "@/components/StreakTracker"; import TopRepos from "@/components/TopRepos"; + import StatsCard from "@/components/StatsCard"; interface PublicProfileData { @@ -87,26 +88,26 @@ export default async function PublicProfilePage({ const { username } = params; const profile = await fetchPublicProfile(username); - if (!profile) { - return ( -
-
-

- Profile Not Found -

-

- This profile is not available or is private. -

- - Back to Home - -
+if (!profile) { + return ( +
+
+

+ Profile Not Found +

+

+ This profile is not available or is private. +

+ + Back to Home +
- ); - } +
+ ); +} const avatarUrl = `https://avatars.githubusercontent.com/${profile.username}`; const topRepo = profile.repos[0]?.name ?? ""; diff --git a/src/components/BackToDashboard.tsx b/src/components/BackToDashboard.tsx new file mode 100644 index 0000000..a8f1518 --- /dev/null +++ b/src/components/BackToDashboard.tsx @@ -0,0 +1,27 @@ +"use client"; + +import Link from "next/link"; +import { useSession } from "next-auth/react"; + +interface Props { + username: string; +} + +export default function BackToDashboard({ username }: Props) { + const { data: session } = useSession(); + + const currentUser = session?.user?.name; + + const isOwner = currentUser === username; + + if (!isOwner) return null; + + return ( + + ← Back to dashboard + + ); +} \ No newline at end of file diff --git a/src/components/PRMetrics.tsx b/src/components/PRMetrics.tsx index 8202dec..1f75210 100644 --- a/src/components/PRMetrics.tsx +++ b/src/components/PRMetrics.tsx @@ -8,6 +8,13 @@ interface PRData { merged: number; avgReviewHours: number; mergeRate: string; + prs: PullRequest[]; +} +interface PullRequest { + title: string; + created_at: string; + html_url: string; + state: string; } export default function PRMetrics() { @@ -15,7 +22,7 @@ export default function PRMetrics() { const [metrics, setMetrics] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - +const [staleDays, setStaleDays] = useState(7); const fetchMetrics = useCallback(() => { setLoading(true); setError(null); @@ -39,6 +46,21 @@ export default function PRMetrics() { fetchMetrics(); }, [fetchMetrics]); + const isStale = (createdAt: string) => { + const createdDate = new Date(createdAt); + const now = new Date(); + + const diffTime = now.getTime() - createdDate.getTime(); + + const diffDays = diffTime / (1000 * 60 * 60 * 24); + + return diffDays > staleDays; +}; +const stalePRs = + metrics?.prs.filter( + (pr) => pr.state === "open" && isStale(pr.created_at) + ) || []; + const stats = metrics ? [ { label: "Open PRs", value: metrics.open }, @@ -52,6 +74,7 @@ export default function PRMetrics() {

PR Analytics

{loading ? ( +
{[1, 2, 3, 4].map((i) => (
) : ( + <> +
+
+ {stalePRs.length} PRs stale > {staleDays} days +
+ + +
+
- {stats.map((stat) => ( -
-
- {stat.value} -
-
{stat.label}
-
- ))} -
- )} + {stats.map((stat) => ( +
+
{stat.value} +
+ +
{stat.label} +
+
+ ))} +
+ +{stalePRs.length > 0 && ( +
+

+ Stale Pull Requests +

+ +
+ {stalePRs.map((pr) => ( + + + {pr.title} + + + + Stale + + + ))} +
+
+)} + +)} +
); }