Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
34d2859
feat: add project admin scoring, dashboard profile, and path slug val…
Ixotic27 Jun 26, 2026
771363f
fix: resolve string interpolation bug for @owner in Opened Issues Sta…
Ixotic27 Jun 26, 2026
44a0ead
fix: retrieve only merged PRs via search and add rate limiting note
Ixotic27 Jun 26, 2026
4ca4a2b
fix: filter merged PRs by adminUsername, handle pagination errors, ca…
Ixotic27 Jun 26, 2026
230c5e9
fix: address PR reviews, adjust search pagination cap to 1000, and cl…
Ixotic27 Jun 26, 2026
de4f69f
rebrand: rename to PA Activity Tracker with estimated scores disclaimer
Ixotic27 Jun 26, 2026
6cda26b
fix: restore original scoring formula (count all merged approved PRs)…
Ixotic27 Jun 26, 2026
c3ea4f2
fix: restore original points formula and parameters-based cache keys
Ixotic27 Jun 26, 2026
e9ac7a3
fix: suppress hydration warning on html and body tags
Ixotic27 Jun 26, 2026
7b0e9ce
feat: add Supabase caching for project-admin tracker & PA role on hom…
Ixotic27 Jun 29, 2026
a999cc5
fix: correct points for non-merged PRs, add truncated pagination, upd…
Ixotic27 Jun 29, 2026
b96bb5f
chore: bump cache tags to bypass stale Next.js cache
Ixotic27 Jun 29, 2026
6193a80
fix: resolve client-side rate limits via server API and update Try Ag…
Ixotic27 Jun 29, 2026
e692dd5
fix: restrict admin merged PR count to truly merged PRs and closed is…
Ixotic27 Jun 29, 2026
4300d06
merge branch 'feat/project-admin-page' into feat/pa-supabase-dashboar…
Ixotic27 Jul 3, 2026
af2bf91
Merge branch 'upstream/main' into feat/pa-supabase-dashboard to resol…
Ixotic27 Jul 3, 2026
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
28 changes: 25 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions scratch/test-score.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { _buildAdminScore } from "../src/lib/admin-scoring";

async function main() {
try {
const score = await _buildAdminScore("ixotic27", "the-leetcode-city", "ixotic27");
console.log("SCORE BREAKDOWN:", JSON.stringify(score, null, 2));
} catch (err) {
console.error("ERROR:", err);
}
}

main();
30 changes: 30 additions & 0 deletions src/app/api/github-user/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from "next/server";

export async function GET(req: NextRequest) {
const username = req.nextUrl.searchParams.get("username")?.trim();
if (!username) {
return NextResponse.json({ error: "Username is required" }, { status: 400 });
}

const token = process.env.GH_TOKEN;
const headers: Record<string, string> = {
Accept: "application/vnd.github.v3+json",
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}

try {
const res = await fetch(`https://api.github.com/users/${encodeURIComponent(username)}`, { headers });
if (res.status === 404) {
return NextResponse.json({ error: "GitHub user not found" }, { status: 404 });
}
if (!res.ok) {
return NextResponse.json({ error: "Failed to fetch user from GitHub" }, { status: res.status });
}
const data = await res.json();
return NextResponse.json(data);
} catch (e) {
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
}
}
4 changes: 3 additions & 1 deletion src/app/api/subscribe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ export async function POST(req: NextRequest) {
}

// Verify GitHub username exists
const headers: Record<string, string> = { Accept: "application/vnd.github.v3+json" };
if (process.env.GH_TOKEN) headers.Authorization = `Bearer ${process.env.GH_TOKEN}`;
const ghRes = await fetch(`https://api.github.com/users/${encodeURIComponent(github)}`, {
headers: { Accept: "application/vnd.github.v3+json" },
headers
});
if (!ghRes.ok) {
return NextResponse.json({ error: `GitHub user "@${github}" not found` }, { status: 400 });
Expand Down
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<head>
<link rel="preconnect" href="https://api.github.com" />
</head>
<body className="min-h-screen">
<body className="min-h-screen" suppressHydrationWarning>
<CSPostHogProvider>
{children}
</CSPostHogProvider>
Expand Down
6 changes: 3 additions & 3 deletions src/app/mentor/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,9 @@ function ErrorPage({ username, code }: { username: string; code: string }) {
<p style={{ margin: "0 0 28px", fontSize: 14, color: ds.inkMute, lineHeight: 1.65 }}>
{isRateLimit ? "GitHub API rate limit exceeded. Try again in a few minutes." : `Failed to fetch mentor data for @${username}.`}
</p>
<Link href="/" style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "9px 20px", borderRadius: ds.rSm, background: ds.primary, color: ds.onPrimary, textDecoration: "none", fontSize: 14, fontWeight: 600 }}>
<ArrowLeft size={14} /> Try again
</Link>
<a href="" style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "9px 20px", borderRadius: ds.rSm, background: ds.primary, color: ds.onPrimary, textDecoration: "none", fontSize: 14, fontWeight: 600 }}>
<RefreshCw size={14} /> Try again
</a>
</div>
</div>
);
Expand Down
60 changes: 48 additions & 12 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { useState, useEffect, FormEvent } from "react";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { Loader2, AlertCircle, Star, GitPullRequest, Users, ArrowLeft } from "lucide-react";
import { Loader2, AlertCircle, Star, GitPullRequest, Users, ArrowLeft, FolderGit2 } from "lucide-react";
import { ds, fontMono } from "@/lib/ds";
import { GitHubIcon } from "@/components/icons";
import { SubscribeButton } from "@/components/SubscribeModal";
Expand All @@ -11,7 +11,7 @@ import Image from "next/image";

const REPO_URL = "https://github.com/PRODHOSH/gssoc-tracker";

type Role = "contributor" | "mentor";
type Role = "contributor" | "mentor" | "project-admin";

const ROLES: { id: Role; icon: React.ReactNode; label: string; desc: string; border: string; bg: string; hoverBorder: string; hoverBg: string }[] = [
{
Expand All @@ -30,6 +30,14 @@ const ROLES: { id: Role; icon: React.ReactNode; label: string; desc: string; bor
border: "rgba(251,191,36,0.2)", bg: "rgba(251,191,36,0.05)",
hoverBorder: "rgba(251,191,36,0.5)", hoverBg: "rgba(251,191,36,0.1)",
},
{
id: "project-admin",
icon: <FolderGit2 size={20} color="#818cf8" />,
label: "Project Admin",
desc: "Track issues, PRs & admin activity for your GSSoC project",
border: "rgba(129,140,248,0.2)", bg: "rgba(129,140,248,0.05)",
hoverBorder: "rgba(129,140,248,0.5)", hoverBg: "rgba(129,140,248,0.1)",
},
];

export default function Home() {
Expand Down Expand Up @@ -71,17 +79,42 @@ export default function Home() {
setState("loading");

try {
const res = await fetch(`https://api.github.com/users/${encodeURIComponent(raw)}`);
if (res.status === 404) { setErrMsg("GitHub user not found"); setState("error"); return; }
if (!res.ok) { setErrMsg("Couldn't reach GitHub. Try again."); setState("error"); return; }
router.push(role === "contributor" ? `/pr-tracker/${encodeURIComponent(raw)}` : `/mentor/${encodeURIComponent(raw)}`);
if (role === "project-admin") {
if (raw.includes("/")) {
// Expect owner/repo format
const parts = raw.replace(/^https?:\/\/github\.com\//, "").split("/");
if (parts.length < 2 || !parts[0] || !parts[1]) {
setErrMsg("Enter a valid repo in owner/repo format (e.g. PRODHOSH/gssoc-tracker)");
setState("error");
return;
}
const [owner, repo] = parts;
// Verify repo exists
const res = await fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`);
if (res.status === 404) { setErrMsg("Repository not found"); setState("error"); return; }
if (!res.ok) { setErrMsg("Couldn't reach GitHub. Try again."); setState("error"); return; }
router.push(`/project-admin/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`);
} else {
// Verify user exists using the server API
const res = await fetch(`/api/github-user?username=${encodeURIComponent(raw)}`);
if (res.status === 404) { setErrMsg("GitHub user not found"); setState("error"); return; }
if (!res.ok) { setErrMsg("Couldn't reach GitHub. Try again."); setState("error"); return; }
router.push(`/project-admin/${encodeURIComponent(raw)}`);
}
} else {
const res = await fetch(`/api/github-user?username=${encodeURIComponent(raw)}`);
if (res.status === 404) { setErrMsg("GitHub user not found"); setState("error"); return; }
if (!res.ok) { setErrMsg("Couldn't reach GitHub. Try again."); setState("error"); return; }
router.push(role === "contributor" ? `/pr-tracker/${encodeURIComponent(raw)}` : `/mentor/${encodeURIComponent(raw)}`);
}
} catch {
setErrMsg("Couldn't reach the API. Try again.");
setState("error");
}
}

const activeRole = ROLES.find((r) => r.id === role);
const isPA = role === "project-admin";

return (
<div className="dark" style={{
Expand Down Expand Up @@ -216,13 +249,16 @@ export default function Home() {
<form onSubmit={submit} style={{ display: "flex", gap: 8 }}>
<div style={{ position: "relative", flex: 1 }}>
<div style={{ position: "absolute", left: 13, top: "50%", transform: "translateY(-50%)", pointerEvents: "none" }}>
<GitHubIcon width={14} height={14} style={{ color: "rgba(255,255,255,0.25)" }} />
{isPA
? <FolderGit2 width={14} height={14} style={{ color: "rgba(255,255,255,0.25)" }} />
: <GitHubIcon width={14} height={14} style={{ color: "rgba(255,255,255,0.25)" }} />
}
</div>
<input
type="text"
value={input}
onChange={(e) => { setInput(e.target.value); setState("idle"); setErrMsg(""); }}
placeholder="GitHub username…"
placeholder={isPA ? "owner/repo or admin username…" : "GitHub username…"}
autoFocus
autoComplete="off"
suppressHydrationWarning
Expand All @@ -237,7 +273,7 @@ export default function Home() {
transition: "border-color 0.15s",
boxSizing: "border-box",
}}
onFocus={(e) => (e.currentTarget.style.borderColor = "rgba(62,207,142,0.45)")}
onFocus={(e) => (e.currentTarget.style.borderColor = isPA ? "rgba(129,140,248,0.45)" : "rgba(62,207,142,0.45)")}
onBlur={(e) => (e.currentTarget.style.borderColor = state === "error" ? "rgba(248,113,113,0.5)" : "rgba(255,255,255,0.08)")}
/>
</div>
Expand All @@ -248,8 +284,8 @@ export default function Home() {
style={{
height: 48, padding: "0 22px",
borderRadius: 10, border: "none",
background: state === "loading" ? "rgba(62,207,142,0.55)" : ds.primary,
color: ds.onPrimary, fontSize: 14, fontWeight: 600,
background: state === "loading" ? (isPA ? "rgba(129,140,248,0.55)" : "rgba(62,207,142,0.55)") : (isPA ? "#818cf8" : ds.primary),
color: isPA ? "#fff" : ds.onPrimary, fontSize: 14, fontWeight: 600,
cursor: state === "loading" || !input.trim() ? "not-allowed" : "pointer",
display: "flex", alignItems: "center", gap: 6, flexShrink: 0,
transition: "background 0.13s",
Expand All @@ -258,7 +294,7 @@ export default function Home() {
>
{state === "loading"
? <><Loader2 size={14} style={{ animation: "spin 1s linear infinite" }} /> Loading…</>
: "Track →"}
: isPA ? "View Stats →" : "Track →"}
</button>
</form>

Expand Down
Loading
Loading