Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5d92718

Browse files
authored
feat: enhance error handling and user feedback in comparison feature (#152)
1 parent 7e76329 commit 5d92718

6 files changed

Lines changed: 234 additions & 30 deletions

File tree

app/api/compare/route.ts

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { fetchGitHubUserData } from "../../../lib/github";
33
import { calculateUserScore } from "../../../lib/score";
44
import { normalizeSelectedLanguages } from "@/lib/scoring/languageScoring";
55
import { toSafeApiError } from "@/lib/github-graphql-client";
6-
import type { CompareInsights } from "@/types/api-response";
6+
import type { CompareInsights, SafeApiError } from "@/types/api-response";
77
import {
88
DEFAULT_LOCALE,
99
LOCALE_COOKIE,
@@ -14,6 +14,18 @@ import {
1414

1515
export const runtime = "nodejs";
1616

17+
class CompareUserFetchError extends Error {
18+
readonly username: string;
19+
readonly causeError: unknown;
20+
21+
constructor(username: string, causeError: unknown) {
22+
super(`Failed to fetch GitHub data for ${username}`);
23+
this.name = "CompareUserFetchError";
24+
this.username = username;
25+
this.causeError = causeError;
26+
}
27+
}
28+
1729
type ComparedUserResult = {
1830
username: string;
1931
name: string | null;
@@ -36,6 +48,8 @@ type ComparedUserResult = {
3648
explanations: ReturnType<typeof calculateUserScore>["explanations"];
3749
};
3850

51+
type ClientSafeError = Pick<SafeApiError, "code" | "message" | "targetUsernames">;
52+
3953
function parseSelectedLanguagesFromSearchParams(
4054
searchParams: URLSearchParams,
4155
): string[] {
@@ -292,7 +306,13 @@ async function compareUsers(
292306
const results: ComparedUserResult[] = [];
293307

294308
for (const username of usernames) {
295-
const data = await fetchGitHubUserData(username);
309+
let data: Awaited<ReturnType<typeof fetchGitHubUserData>>;
310+
try {
311+
data = await fetchGitHubUserData(username);
312+
} catch (error: unknown) {
313+
throw new CompareUserFetchError(username, error);
314+
}
315+
296316
const score = calculateUserScore(
297317
{
298318
...data,
@@ -341,6 +361,14 @@ function toApiErrorStatus(code: ReturnType<typeof toSafeApiError>["code"]): numb
341361
}
342362
}
343363

364+
function toClientSafeError(error: SafeApiError): ClientSafeError {
365+
return {
366+
code: error.code,
367+
message: error.message,
368+
targetUsernames: error.targetUsernames,
369+
};
370+
}
371+
344372
export async function GET(request: Request) {
345373
const { searchParams } = new URL(request.url);
346374
const usernames = searchParams
@@ -364,16 +392,39 @@ export async function GET(request: Request) {
364392
return NextResponse.json({ success: true, users, ...winnerData, insights });
365393
} catch (error: unknown) {
366394
console.error("GitHub score error:", error);
367-
const safeError =
368-
error instanceof Error && error.message === "User not found"
369-
? { code: "GITHUB_NOT_FOUND" as const, message: "GitHub user not found" }
370-
: toSafeApiError(error);
395+
396+
let safeError: SafeApiError;
397+
398+
if (error instanceof CompareUserFetchError) {
399+
const mappedCause = toSafeApiError(error.causeError);
400+
if (
401+
mappedCause.code === "GITHUB_NOT_FOUND" ||
402+
(error.causeError instanceof Error &&
403+
error.causeError.message === "User not found")
404+
) {
405+
safeError = {
406+
code: "GITHUB_NOT_FOUND",
407+
message: "GitHub user not found",
408+
targetUsernames: [error.username],
409+
rateLimit: mappedCause.rateLimit,
410+
};
411+
} else {
412+
safeError = mappedCause;
413+
}
414+
} else {
415+
safeError =
416+
error instanceof Error && error.message === "User not found"
417+
? { code: "GITHUB_NOT_FOUND", message: "GitHub user not found" }
418+
: toSafeApiError(error);
419+
}
420+
421+
const clientSafeError = toClientSafeError(safeError);
371422

372423
return NextResponse.json(
373424
{
374425
success: false,
375-
error: safeError.message,
376-
errorDetails: safeError,
426+
error: clientSafeError.message,
427+
errorDetails: clientSafeError,
377428
},
378429
{ status: toApiErrorStatus(safeError.code) },
379430
);

components/compare-form.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
CardHeader,
1010
CardTitle,
1111
} from "./ui/card";
12-
import { Alert, AlertDescription } from "./ui/alert";
1312
import { useTranslation } from "./language-provider";
1413
import { cn } from "@/lib/utils";
1514

@@ -46,7 +45,8 @@ type CompareFormProps = {
4645
loading?: boolean;
4746
reset?: () => void;
4847
swapUsers?: () => void;
49-
error?: string | null;
48+
username1Error?: string | null;
49+
username2Error?: string | null;
5050
};
5151

5252
export function CompareForm({
@@ -61,7 +61,8 @@ export function CompareForm({
6161
loading,
6262
swapUsers,
6363
reset,
64-
error,
64+
username1Error,
65+
username2Error,
6566
}: CompareFormProps) {
6667
const { t } = useTranslation();
6768
const firstInputRef = useRef<HTMLInputElement>(null);
@@ -131,7 +132,14 @@ export function CompareForm({
131132
value={username1}
132133
onChange={(e) => setUsername1(e.target.value)}
133134
aria-label={t("form.username1.label")}
135+
aria-invalid={Boolean(username1Error)}
136+
aria-describedby={username1Error ? "username1-error" : undefined}
134137
/>
138+
{username1Error ? (
139+
<p id="username1-error" className="text-xs font-medium text-destructive">
140+
{username1Error}
141+
</p>
142+
) : null}
135143
</div>
136144
<div className="space-y-1.5">
137145
<label htmlFor="username2" className="text-xs font-semibold text-muted-foreground">
@@ -144,7 +152,14 @@ export function CompareForm({
144152
value={username2}
145153
onChange={(e) => setUsername2(e.target.value)}
146154
aria-label={t("form.username2.label")}
155+
aria-invalid={Boolean(username2Error)}
156+
aria-describedby={username2Error ? "username2-error" : undefined}
147157
/>
158+
{username2Error ? (
159+
<p id="username2-error" className="text-xs font-medium text-destructive">
160+
{username2Error}
161+
</p>
162+
) : null}
148163
</div>
149164
</div>
150165

@@ -250,12 +265,6 @@ export function CompareForm({
250265
<RefreshCw className="h-4 w-4" />
251266
</Button>
252267
</div>
253-
254-
{error ? (
255-
<Alert variant="destructive">
256-
<AlertDescription>{error}</AlertDescription>
257-
</Alert>
258-
) : null}
259268
</CardContent>
260269
</Card>
261270
</form>

0 commit comments

Comments
 (0)