Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions src/app/api/metrics/prs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PRMetricsBase> {


const searchRes = await fetch(
`${GITHUB_API}/search/issues?q=type:pr+author:@me&per_page=100`,
{
Expand All @@ -34,7 +41,13 @@ async function fetchPRMetrics(token: string): Promise<PRMetricsBase> {

const data = (await searchRes.json()) as {
total_count: number;
items: Array<{ state: string; created_at: string; closed_at: string | null }>;
items: Array<{
title: string;
state: string;
created_at: string;
closed_at: string | null;
html_url: string;
}>;
};

const open = data.items.filter((pr) => pr.state === "open").length;
Expand All @@ -51,14 +64,21 @@ async function fetchPRMetrics(token: string): Promise<PRMetricsBase> {
0
) / closedPRs.length
: 0;

return {
open,
merged,
total: data.total_count,
avgReviewHours: Math.round(avgReviewMs / 3600000),
mergeRate: data.total_count > 0 ? merged / data.total_count : 0,
};
const prs = data.items.map((pr) => ({
title: pr.title,
created_at: pr.created_at,
html_url: pr.html_url,
state: pr.state,
}));

return {
open,
merged,
total: data.total_count,
avgReviewHours: Math.round(avgReviewMs / 3600000),
mergeRate: data.total_count > 0 ? merged / data.total_count : 0,
prs,
};
}

function formatPRMetrics(metrics: PRMetricsBase) {
Expand All @@ -71,6 +91,7 @@ function formatPRMetrics(metrics: PRMetricsBase) {
metrics.total > 0
? `${Math.round(metrics.mergeRate * 100)}%`
: "0%",
prs: metrics.prs,
};
}

Expand Down Expand Up @@ -129,6 +150,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,
Expand Down
60 changes: 32 additions & 28 deletions src/app/u/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BadgeSection from "@/components/BadgeSection";
import ContributionGraph from "@/components/ContributionGraph";
import StreakTracker from "@/components/StreakTracker";
import TopRepos from "@/components/TopRepos";

import BackToDashboard from "@/components/BackToDashboard";
interface PublicProfileData {
username: string;
userId: string;
Expand Down Expand Up @@ -86,38 +86,42 @@ export default async function PublicProfilePage({
const { username } = params;
const profile = await fetchPublicProfile(username);

if (!profile) {
return (
<div className="min-h-screen bg-[var(--background)] p-4 md:p-8 text-[var(--foreground)] transition-colors flex items-center justify-center">
<div className="text-center">
<h1 className="text-3xl md:text-4xl font-bold mb-2">
Profile Not Found
</h1>
<p className="text-[var(--muted-foreground)] mb-6">
This profile is not available or is private.
</p>
<a
href="/"
className="inline-block px-6 py-2 bg-[var(--accent)] text-[var(--accent-foreground)] rounded-lg hover:opacity-90 transition-opacity"
>
Back to Home
</a>
</div>
if (!profile) {
return (
<div className="min-h-screen bg-[var(--background)] p-4 md:p-8 text-[var(--foreground)] transition-colors flex items-center justify-center">
<div className="text-center">
<h1 className="text-3xl md:text-4xl font-bold mb-2">
Profile Not Found
</h1>
<p className="text-[var(--muted-foreground)] mb-6">
This profile is not available or is private.
</p>
<a
href="/"
className="inline-block px-6 py-2 bg-[var(--accent)] text-[var(--accent-foreground)] rounded-lg hover:opacity-90 transition-opacity"
>
Back to Home
</a>
</div>
);
}
</div>
);
}

return (
<div className="min-h-screen bg-[var(--background)] p-4 md:p-8 text-[var(--foreground)] transition-colors">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl md:text-4xl font-bold text-[var(--foreground)]">
@{profile.username}&apos;s Profile
</h1>
<p className="mt-2 text-[var(--muted-foreground)]">
GitHub activity and coding stats
</p>
</div>
{/* Header */}
<div className="mb-8">
<BackToDashboard username={username} />

<h1 className="text-3xl md:text-4xl font-bold text-[var(--foreground)]">
@{profile.username}&apos;s Profile
</h1>

<p className="mt-2 text-[var(--muted-foreground)]">
GitHub activity and coding stats
</p>
</div>

{/* Row 1: Contribution graph + Streak */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
Expand Down
27 changes: 27 additions & 0 deletions src/components/BackToDashboard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
href="/dashboard"
className="inline-block mb-4 text-[var(--muted-foreground)] hover:text-[var(--card-foreground)] transition-colors"
>
← Back to dashboard
</Link>
);
}
100 changes: 86 additions & 14 deletions src/components/PRMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ 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() {
const { selectedAccount } = useAccount();
const [metrics, setMetrics] = useState<PRData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

const [staleDays, setStaleDays] = useState(7);
const fetchMetrics = useCallback(() => {
setLoading(true);
setError(null);
Expand All @@ -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 },
Expand All @@ -52,6 +74,7 @@ export default function PRMetrics() {
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
<h2 className="mb-4 text-lg font-semibold text-[var(--card-foreground)]">PR Analytics</h2>
{loading ? (

<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div
Expand All @@ -72,20 +95,69 @@ export default function PRMetrics() {
</button>
</div>
) : (
<>
<div className="mb-4 flex items-center justify-between">
<div className="rounded-full bg-orange-500/10 px-3 py-1 text-sm text-orange-400">
{stalePRs.length} PRs stale &gt; {staleDays} days
</div>

<select
value={staleDays}
onChange={(e) => setStaleDays(Number(e.target.value))}
className="rounded-md border border-[var(--border)] bg-[var(--card)] px-2 py-1 text-sm"
>
<option value={7}>7 days</option>
<option value={14}>14 days</option>
<option value={30}>30 days</option>
</select>
</div>

<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat) => (
<div
key={stat.label}
className="rounded-lg bg-[var(--control)] p-4 text-center"
>
<div className="text-2xl font-bold text-[var(--accent)]">
{stat.value}
</div>
<div className="mt-1 text-sm text-[var(--muted-foreground)]">{stat.label}</div>
</div>
))}
</div>
)}
{stats.map((stat) => (
<div
key={stat.label}
className="rounded-lg bg-[var(--control)] p-4 text-center"
>
<div className="text-2xl font-bold text-[var(--accent)]">
{stat.value}
</div>

<div className="mt-1 text-sm text-[var(--muted-foreground)]">
{stat.label}
</div>
</div>
))}
</div>

{stalePRs.length > 0 && (
<div className="mt-6">
<h3 className="mb-3 text-sm font-semibold text-[var(--card-foreground)]">
Stale Pull Requests
</h3>

<div className="space-y-2">
{stalePRs.map((pr) => (
<a
key={pr.html_url}
href={pr.html_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between rounded-lg bg-[var(--control)] p-3 transition hover:opacity-90"
>
<span className="text-sm text-[var(--card-foreground)]">
{pr.title}
</span>

<span className="rounded-full bg-orange-500 px-2 py-1 text-xs font-medium text-white">
Stale
</span>
</a>
))}
</div>
</div>
)}
</>
)}
</div>
);
}
Loading