Skip to content

Commit 277375f

Browse files
authored
Merge branch 'main' into main
2 parents 56d9f47 + 3ec3e38 commit 277375f

63 files changed

Lines changed: 3939 additions & 464 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.local.example

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
# Generate new token (classic)
2121
# Required scope: read:user
2222
# ------------------------------------------------------------
23-
GITHUB_TOKEN=ghp_your_personal_access_token_here
24-
23+
GITHUB_TOKEN=
2524

2625
# ------------------------------------------------------------
2726
# REQUIRED — Public site URL

app/(root)/dashboard/[username]/page.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ describe('DashboardPage', () => {
100100
achievements: [],
101101
commitClock: [],
102102
graphData: { nodes: [], links: [] },
103+
lastSyncedAt: undefined,
103104
};
104105

105106
beforeEach(() => {

app/(root)/dashboard/[username]/page.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export default async function DashboardPage({
6363
params: Promise<{ username: string }>;
6464
searchParams: Promise<{
6565
refresh?: string;
66+
compare?: string;
6667
year?: string;
6768
month?: string;
6869
from?: string;
@@ -72,6 +73,7 @@ export default async function DashboardPage({
7273
const { username } = await params;
7374
const resolvedSearchParams = await searchParams;
7475
const bypassCache = resolvedSearchParams?.refresh === 'true';
76+
const compareUsername = resolvedSearchParams?.compare;
7577
const period = resolveDashboardPeriod({
7678
year: resolvedSearchParams?.year,
7779
month: resolvedSearchParams?.month,
@@ -90,10 +92,11 @@ export default async function DashboardPage({
9092
});
9193
} catch (error) {
9294
if (error instanceof Error && error.message.includes('not found')) {
93-
// Smart Redirect: If the GraphQL "user" query fails, check if it's actually an Organization
9495
let fallbackProfile;
9596
try {
96-
fallbackProfile = await fetchUserProfile(username, { bypassCache });
97+
fallbackProfile = await fetchUserProfile(username, {
98+
bypassCache,
99+
});
97100
} catch {
98101
return notFound();
99102
}
@@ -105,5 +108,24 @@ export default async function DashboardPage({
105108
throw error;
106109
}
107110

108-
return <DashboardClient initialData={data} username={username} period={period} />;
111+
let compareData = null;
112+
113+
if (compareUsername && compareUsername.toLowerCase() !== username.toLowerCase()) {
114+
try {
115+
compareData = await getFullDashboardData(compareUsername, {
116+
bypassCache,
117+
});
118+
} catch {
119+
compareData = null;
120+
}
121+
}
122+
123+
return (
124+
<DashboardClient
125+
initialData={data}
126+
username={username}
127+
compareData={compareData}
128+
period={period}
129+
/>
130+
);
109131
}

app/api/compare/route.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,38 @@ describe('GET /api/compare', () => {
9090
const res = await GET(makeRequest('user1=octocat&user2=ghost123'));
9191
expect(res.status).toBe(404);
9292
});
93+
94+
it('returns 403 when the user1 fetch hits a GitHub rate limit', async () => {
95+
vi.mocked(getFullDashboardData).mockRejectedValueOnce(new Error('API Rate Limit Exceeded'));
96+
97+
const res = await GET(makeRequest('user1=octocat&user2=torvalds'));
98+
99+
expect(res.status).toBe(403);
100+
const data = await res.json();
101+
expect(data.error).toBe('GitHub API rate limit reached. Please configure GITHUB_TOKEN.');
102+
});
103+
104+
it('returns 403 when the user2 fetch hits a GitHub rate limit', async () => {
105+
vi.mocked(getFullDashboardData)
106+
.mockResolvedValueOnce({ calendar: { totalContributions: 0, weeks: [] } } as never)
107+
.mockRejectedValueOnce(new Error('GitHub GraphQL API returned status 403'));
108+
109+
const res = await GET(makeRequest('user1=octocat&user2=torvalds'));
110+
111+
expect(res.status).toBe(403);
112+
const data = await res.json();
113+
expect(data.error).toBe('GitHub API rate limit reached. Please configure GITHUB_TOKEN.');
114+
});
115+
116+
it('returns 500 for non-rate-limit upstream failures instead of reporting not found', async () => {
117+
vi.mocked(getFullDashboardData).mockRejectedValueOnce(
118+
new Error('GitHub GraphQL API returned status 500')
119+
);
120+
121+
const res = await GET(makeRequest('user1=octocat&user2=torvalds'));
122+
123+
expect(res.status).toBe(500);
124+
const data = await res.json();
125+
expect(data.error).toContain('GitHub GraphQL API returned status 500');
126+
});
93127
});

app/api/compare/route.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,38 @@ import { compareParamsSchema } from '@/lib/validations';
44

55
export const revalidate = 3600;
66

7+
function buildCompareFetchErrorResponse(user: string, reason: unknown): NextResponse {
8+
const message = reason instanceof Error ? reason.message : 'Unknown error';
9+
const lowerMessage = message.toLowerCase();
10+
11+
if (lowerMessage.includes('not found') || lowerMessage.includes('could not resolve')) {
12+
return NextResponse.json(
13+
{
14+
error: `Failed to fetch data for "${user}": ${message}`,
15+
},
16+
{ status: 404 }
17+
);
18+
}
19+
20+
if (
21+
lowerMessage.includes('rate limit') ||
22+
message.includes('API limit reached') ||
23+
message.includes('status 403')
24+
) {
25+
return NextResponse.json(
26+
{ error: 'GitHub API rate limit reached. Please configure GITHUB_TOKEN.' },
27+
{ status: 403 }
28+
);
29+
}
30+
31+
return NextResponse.json(
32+
{
33+
error: `Failed to fetch data for "${user}": ${message}`,
34+
},
35+
{ status: 500 }
36+
);
37+
}
38+
739
export async function GET(request: Request) {
840
const { searchParams } = new URL(request.url);
941

@@ -26,21 +58,11 @@ export async function GET(request: Request) {
2658
]);
2759

2860
if (result1.status === 'rejected') {
29-
return NextResponse.json(
30-
{
31-
error: `Failed to fetch data for "${user1}": ${result1.reason?.message || 'Unknown error'}`,
32-
},
33-
{ status: 404 }
34-
);
61+
return buildCompareFetchErrorResponse(user1, result1.reason);
3562
}
3663

3764
if (result2.status === 'rejected') {
38-
return NextResponse.json(
39-
{
40-
error: `Failed to fetch data for "${user2}": ${result2.reason?.message || 'Unknown error'}`,
41-
},
42-
{ status: 404 }
43-
);
65+
return buildCompareFetchErrorResponse(user2, result2.reason);
4466
}
4567

4668
return NextResponse.json({

0 commit comments

Comments
 (0)