Skip to content
Closed
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
17 changes: 16 additions & 1 deletion app/api/compare/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { fetchGitHubUserData } from "../../../lib/github";
import { fetchGitHubUserData, RateLimitError } from "../../../lib/github";
import { calculateUserScore } from "../../../lib/score";

export const runtime = "nodejs";
Expand Down Expand Up @@ -36,6 +36,21 @@ export async function GET(request: Request) {
return NextResponse.json({ success: true, users: results });
} catch (error: any) {
console.error("GitHub score error:", error);

if (error instanceof RateLimitError) {
const resetMsg = error.resetAt
? ` Try again after ${error.resetAt.toLocaleTimeString()}.`
: " Please try again later.";
return NextResponse.json(
{
success: false,
error: `GitHub API rate limit exceeded.${resetMsg}`,
rateLimitReset: error.resetAt?.toISOString() ?? null,
},
{ status: 429 }
);
}

const message =
error?.message === "User not found"
? "GitHub user not found"
Expand Down
11 changes: 10 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ type ApiResponse = {
success: boolean;
users?: UserResult[];
error?: string;
rateLimitReset?: string | null;
};

export default function HomePage() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isRateLimit, setIsRateLimit] = useState(false);
const [data, setData] = useState<{
user1: UserResult;
user2: UserResult;
Expand All @@ -23,13 +25,18 @@ export default function HomePage() {
const handleCompare = async (u1: string, u2: string) => {
setLoading(true);
setError(null);
setIsRateLimit(false);
setData(null);
try {
const params = new URLSearchParams();
params.append("username", u1);
params.append("username", u2);
const res = await fetch(`/api/compare?${params.toString()}`);
const body: ApiResponse = await res.json();
if (res.status === 429 || body.rateLimitReset !== undefined) {
setIsRateLimit(true);
throw new Error(body.error || "GitHub API rate limit exceeded. Please try again later.");
}
if (!body.success || !body.users || body.users.length < 2) {
throw new Error(body.error || "Comparison failed");
}
Expand Down Expand Up @@ -58,6 +65,7 @@ export default function HomePage() {
const reset = () => {
setData(null);
setError(null);
setIsRateLimit(false);
};
const swapUsers = () => {
if (!data) return;
Expand Down Expand Up @@ -88,7 +96,8 @@ export default function HomePage() {

{loading && skeleton}
{error && (
<div className="card p-4 text-sm text-red-600 bg-red-50 border border-red-100">
<div className={`card p-4 text-sm border ${isRateLimit ? "text-amber-700 bg-amber-50 border-amber-200" : "text-red-600 bg-red-50 border-red-100"}`}>
{isRateLimit && <span className="font-semibold">⏳ Rate limit reached — </span>}
{error}
</div>
)}
Expand Down
50 changes: 41 additions & 9 deletions lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,50 @@ const QUERY = /* GraphQL */ `
}
`;

export class RateLimitError extends Error {
resetAt?: Date;
constructor(message: string, resetAt?: Date) {
super(message);
this.name = "RateLimitError";
this.resetAt = resetAt;
}
}

function parseResetTime(headers: Record<string, string>): Date | undefined {
const resetEpoch = headers["x-ratelimit-reset"];
return resetEpoch ? new Date(parseInt(resetEpoch, 10) * 1000) : undefined;
}

export async function fetchGitHubUserData(
username: string
): Promise<GitHubUserData> {
const { user } = await client<{ user: any }>(QUERY, { login: username });
try {
const { user } = await client<{ user: any }>(QUERY, { login: username });

if (!user) {
throw new Error("User not found");
}
if (!user) {
throw new Error("User not found");
}

return {
repos: user.repositories.nodes as RepoNode[],
pullRequests: user.pullRequests.nodes as PullRequestNode[],
contributions: user.contributionsCollection as ContributionTotals,
};
} catch (error: any) {
const message: string = error?.message ?? "";
const isRateLimit =
error?.status === 429 ||
(error?.status === 403 && message.toLowerCase().includes("rate limit")) ||
message.toLowerCase().includes("api rate limit exceeded") ||
message.toLowerCase().includes("secondary rate limit");

return {
repos: user.repositories.nodes as RepoNode[],
pullRequests: user.pullRequests.nodes as PullRequestNode[],
contributions: user.contributionsCollection as ContributionTotals,
};
if (isRateLimit) {
const resetAt = error?.headers
? parseResetTime(error.headers)
: undefined;
throw new RateLimitError("GitHub API rate limit exceeded", resetAt);
}

throw error;
}
}
Loading