Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1ffe964
fix: add GitHub API version header, PAT usage, router cleanup; contri…
ASR1015 Sep 2, 2025
1985c85
perf(router): lazy-load routes + suspense fallback
ASR1015 Sep 2, 2025
df1f6b0
feat: add ghJson helper for API error handling
ASR1015 Sep 2, 2025
f594f4e
fix: handle GitHub API deprecations + robust profile/PR fetching
ASR1015 Sep 2, 2025
d367be4
fix: migrate /search/issues to GraphQL + improve error handling
ASR1015 Sep 2, 2025
5b67ede
fix: await clipboard write for reliable copy + error handling
ASR1015 Sep 2, 2025
9bc8d72
fix: leverage resolveRepoFullName with fallback in Contributors page
ASR1015 Sep 2, 2025
60a8c64
fix(router): remove duplicate RouterProvider and export router config…
ASR1015 Sep 2, 2025
74eee8f
feat: migrate Tracker to GraphQL search, add missing fields, better e…
ASR1015 Sep 2, 2025
14bfce2
fix(github-data): improve GraphQL query with correct fields, paginati…
ASR1015 Sep 2, 2025
232cfe9
fix: map GitHub response to Tracker fields + cache endCursor
ASR1015 Sep 2, 2025
7ba06b2
fix: align PR.id with GraphQL databaseId
ASR1015 Sep 2, 2025
949406a
fix: align PR GraphQL fields with Tracker requirements (databaseId, r…
ASR1015 Sep 2, 2025
b8a87eb
fix(contributors): handle 204 No Content to avoid JSON parse error
ASR1015 Sep 2, 2025
1d3341a
types: make GitHub cursor paging type-safe (SearchType + typed cursor…
ASR1015 Sep 2, 2025
3e82610
fix(search): key GraphQL pagination cursors by username+page size to …
ASR1015 Sep 2, 2025
b90005a
fix(profile): map GraphQL PR nodes to local PR shape + handle GraphQL…
ASR1015 Sep 2, 2025
b5b4b5e
octokit: use shared API version/base, skip empty auth, add Accept hea…
ASR1015 Sep 2, 2025
58bbb99
fix: always tag PRs with pull_request; avoid misclassifying open PRs …
ASR1015 Sep 2, 2025
47f8948
feat(contributors): type Contributor, DRY token, unmount-safe state u…
ASR1015 Sep 2, 2025
5847da2
feat(contributors): type Contributor, DRY token, unmount-safe state u…
ASR1015 Sep 2, 2025
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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.6",
"@mui/material": "^5.15.6",
"@octokit/core": "^6.1.6",
"@octokit/plugin-paginate-rest": "^11.6.0",
"@octokit/plugin-throttling": "^9.6.1",
"@primer/octicons-react": "^19.15.5",
"@vitejs/plugin-react": "^4.3.3",
"axios": "^1.7.7",
Expand Down
56 changes: 41 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import { createBrowserRouter, RouterProvider, Outlet } from "react-router-dom";
import { lazy, Suspense } from "react";
import Navbar from "./components/Navbar";
import Footer from "./components/Footer";
import ScrollProgressBar from "./components/ScrollProgressBar";
import { Toaster } from "react-hot-toast";
import Router from "./Routes/Router";
import ThemeWrapper from "./context/ThemeContext";

function App() {
const Home = lazy(() => import("./pages/Home/Home.tsx"));
const Tracker = lazy(() => import("./pages/Tracker/Tracker.tsx"));
const About = lazy(() => import("./pages/About/About"));
const Contact = lazy(() => import("./pages/Contact/Contact"));
const Contributors = lazy(() => import("./pages/Contributors/Contributors"));
const Signup = lazy(() => import("./pages/Signup/Signup.tsx"));
const Login = lazy(() => import("./pages/Login/Login.tsx"));
const ContributorProfile = lazy(() => import("./pages/ContributorProfile/ContributorProfile.tsx"));

function RootLayout() {
return (
<ThemeWrapper>
<div className="relative flex flex-col min-h-screen">
<ScrollProgressBar />

<Navbar />

<main className="flex-grow bg-gray-50 dark:bg-gray-800 flex justify-center items-center">
<Router />
<Suspense fallback={<div className="p-6">Loading…</div>}>
<Outlet />
</Suspense>
</main>

<Footer />

<Toaster
position="top-center"
reverseOrder={false}
Expand All @@ -27,18 +35,36 @@ function App() {
toastOptions={{
className: "bg-white dark:bg-gray-800 text-black dark:text-white",
duration: 5000,
success: {
duration: 3000,
iconTheme: {
primary: "green",
secondary: "white",
},
},
success: { duration: 3000, iconTheme: { primary: "green", secondary: "white" } },
}}
/>
</div>
</ThemeWrapper>
);
}

export default App;
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <div className="p-6">Something went wrong loading this page.</div>,
children: [
{ index: true, element: <Home /> },
{ path: "track", element: <Tracker /> },
{ path: "signup", element: <Signup /> },
{ path: "login", element: <Login /> },
{ path: "about", element: <About /> },
{ path: "contact", element: <Contact /> },
{ path: "contributors", element: <Contributors /> },
{ path: "contributor/:username", element: <ContributorProfile /> },
],
},
]);

export default function AppRouter() {
return (
<RouterProvider
router={router}
/>
);
}
28 changes: 12 additions & 16 deletions src/Routes/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Routes, Route } from "react-router-dom";
import { createBrowserRouter } from "react-router-dom";
import Tracker from "../pages/Tracker/Tracker.tsx";
import About from "../pages/About/About";
import Contact from "../pages/Contact/Contact";
Expand All @@ -8,19 +8,15 @@ import Login from "../pages/Login/Login.tsx";
import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx";
import Home from "../pages/Home/Home.tsx";

const Router = () => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/track" element={<Tracker />} />
<Route path="/signup" element={<Signup />} />
<Route path="/login" element={<Login />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/contributors" element={<Contributors />} />
<Route path="/contributor/:username" element={<ContributorProfile />} />
</Routes>
);
};
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
{ path: "/track", element: <Tracker /> },
{ path: "/signup", element: <Signup /> },
{ path: "/login", element: <Login /> },
{ path: "/about", element: <About /> },
{ path: "/contact", element: <Contact /> },
{ path: "/contributors", element: <Contributors /> },
{ path: "/contributor/:username", element: <ContributorProfile /> },
]);

export default Router;
export default router;
84 changes: 71 additions & 13 deletions src/hooks/useGitHubData.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useCallback } from 'react';
import { useState, useRef, useCallback } from 'react';

export const useGitHubData = (getOctokit: () => any) => {
type SearchType = 'issue' | 'pr';
const [issues, setIssues] = useState([]);
const [prs, setPrs] = useState([]);
const [loading, setLoading] = useState(false);
Expand All @@ -9,20 +10,77 @@ export const useGitHubData = (getOctokit: () => any) => {
const [totalPrs, setTotalPrs] = useState(0);
const [rateLimited, setRateLimited] = useState(false);

const fetchPaginated = async (octokit: any, username: string, type: string, page = 1, per_page = 10) => {
const q = `author:${username} is:${type}`;
const response = await octokit.request('GET /search/issues', {
q,
sort: 'created',
order: 'desc',
per_page,
page,
const cursorsRef = useRef<Record<SearchType, Record<string, Record<number, string | null>>>>({
issue: {},
pr: {},
});

const fetchPaginated = async (octokit: any, username: string, type: SearchType, page = 1, per_page = 10) => {
const query = `
query ($queryString: String!, $first: Int!, $after: String) {
search(query: $queryString, type: ISSUE, first: $first, after: $after) {
issueCount
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
... on Issue {
databaseId
title
url
createdAt
state
repository { url }
}
... on PullRequest {
databaseId
title
url
createdAt
state
mergedAt
repository { url }
}
}
}
}
}
`;

const queryString = `author:${username} is:${type} sort:updated-desc`;
const cursorKey = `${username}:${Math.min(100, per_page)}`;
const response = await octokit.graphql(query, {
queryString,
first: Math.min(100, per_page),
after: page > 1 ? (cursorsRef.current[type]?.[cursorKey]?.[page - 1] ?? null) : null,
});

return {
items: response.data.items,
total: response.data.total_count,
};
const items = response.search.edges.map((edge: any) => {
const n = edge.node;
const base = {
id: n.databaseId,
title: n.title,
html_url: n.url,
created_at: n.createdAt,
state: n.state,
repository_url: n.repository.url,
};
// Always tag PRs with a pull_request marker; merged_at may be null for open PRs
if (type === 'pr') {
return { ...base, pull_request: { merged_at: n.mergedAt ?? null } };
}
return base;
});

// cache cursor for pagination (keyed by username + page size)
if (!cursorsRef.current[type]) cursorsRef.current[type] = {};
if (!cursorsRef.current[type][cursorKey]) cursorsRef.current[type][cursorKey] = {};
cursorsRef.current[type][cursorKey][page] = response.search.pageInfo.endCursor ?? null;

return { items, total: response.search.issueCount };
};

const fetchData = useCallback(
Expand Down
33 changes: 33 additions & 0 deletions src/lib/githubFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// src/lib/githubFetch.ts
export const GITHUB_API_VERSION = "2022-11-28";

export async function ghFetch(
path: string,
token: string,
init: RequestInit = {}
) {
const base = "https://api.github.com";
const headers = new Headers(init.headers || {});
if (token) {
headers.set("Authorization", `Bearer ${token}`); // or `token ${token}`
}
headers.set("Accept", "application/vnd.github+json");
headers.set("X-GitHub-Api-Version", GITHUB_API_VERSION);

return fetch(`${base}${path}`, { ...init, headers });
}

export async function ghJson<T>(
path: string,
token: string,
init: RequestInit = {}
): Promise<T> {
const res = await ghFetch(path, token, init);
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(
`GitHub API ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`
);
}
return res.json() as Promise<T>;
}
41 changes: 41 additions & 0 deletions src/lib/octokit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Octokit } from "@octokit/core";
import { paginateRest } from "@octokit/plugin-paginate-rest";
import { throttling } from "@octokit/plugin-throttling";
import { GITHUB_API_VERSION, GITHUB_API_BASE } from "../utils/constants";

const MyOctokit = Octokit.plugin(paginateRest, throttling);

// Make token optional; when empty/undefined we avoid sending an empty Authorization header
export const makeOctokit = (token?: string) =>
new MyOctokit({
auth: token || undefined,
baseUrl: GITHUB_API_BASE,
userAgent: "github-tracker/1.0",
request: {
// IMPORTANT: stops “deprecated / unversioned” warnings and aligns with fetch helpers
headers: {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": GITHUB_API_VERSION,
},
},
throttle: {
// Retry once on primary rate limit, then surface to caller
onRateLimit: (retryAfter, options, octokit, retryCount?: number) => {
const count = retryCount ?? 0;
if (count === 0) {
console.warn(
`Rate Limit: ${options.method} ${options.url}. Retrying in ${retryAfter}s.`
);
return true; // single retry
}
return false;
},
// Retry once on secondary rate limit as well
onSecondaryRateLimit: (_retryAfter, options) => {
console.warn(
`Secondary rate limit: ${options.method} ${options.url}.`
);
return true; // single retry
},
},
});
3 changes: 0 additions & 3 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { BrowserRouter } from "react-router-dom";
import ThemeWrapper from "./context/ThemeContext.tsx";

createRoot(document.getElementById("root")!).render(
<StrictMode>
<ThemeWrapper>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeWrapper>
</StrictMode>
);
Loading