Skip to content
Merged
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
41 changes: 27 additions & 14 deletions app/api/compare/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ function calculateWinner(users: ComparedUserResult[]): {
winner?: {
username: string;
finalScoreDifference: number;
percentageDifference: number;
percentageDifference: number | null;
};
languageWinner?: {
username: string;
finalScoreDifference: number;
percentageDifference: number;
percentageDifference: number | null;
selectedLanguages: string[];
};
} {
Expand All @@ -95,28 +95,29 @@ function calculateWinner(users: ComparedUserResult[]): {
const overallLoser =
overallWinner.username === userA.username ? userB : userA;
const overallDifference = Math.abs(userA.finalScore - userB.finalScore);
const overallPercentage =
overallLoser.finalScore > 0
? (overallDifference / overallLoser.finalScore) * 100
: 0;
const overallPercentage = calculatePercentageDifference(
overallDifference,
overallLoser.finalScore,
);

const result: {
winner: {
username: string;
finalScoreDifference: number;
percentageDifference: number;
percentageDifference: number | null;
};
languageWinner?: {
username: string;
finalScoreDifference: number;
percentageDifference: number;
percentageDifference: number | null;
selectedLanguages: string[];
};
} = {
winner: {
username: overallWinner.username,
finalScoreDifference: Math.round(overallDifference),
percentageDifference: Math.round(overallPercentage),
percentageDifference:
overallPercentage === null ? null : Math.round(overallPercentage),
},
};

Expand All @@ -132,22 +133,34 @@ function calculateWinner(users: ComparedUserResult[]): {
const languageDifference = Math.abs(
winnerLanguageScores.finalScore - loserLanguageScores.finalScore,
);
const languagePercentage =
loserLanguageScores.finalScore > 0
? (languageDifference / loserLanguageScores.finalScore) * 100
: 0;
const languagePercentage = calculatePercentageDifference(
languageDifference,
loserLanguageScores.finalScore,
);

result.languageWinner = {
username: languageWinner.username,
finalScoreDifference: Math.round(languageDifference),
percentageDifference: Math.round(languagePercentage),
percentageDifference:
languagePercentage === null ? null : Math.round(languagePercentage),
selectedLanguages: winnerLanguageScores.selectedLanguages,
};
}

return result;
}

function calculatePercentageDifference(
difference: number,
baseline: number,
): number | null {
if (baseline <= 0) {
return difference > 0 ? null : 0;
}

return (difference / baseline) * 100;
}

function createComparisonInsights(
users: ComparedUserResult[],
locale: Locale,
Expand Down
2 changes: 1 addition & 1 deletion components/home-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type ComparisonData = {
languageWinner?: {
username: string;
finalScoreDifference: number;
percentageDifference: number;
percentageDifference: number | null;
selectedLanguages: string[];
};
insights?: CompareInsights;
Expand Down
49 changes: 36 additions & 13 deletions components/result-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type Props = {
languageWinner?: {
username: string;
finalScoreDifference: number;
percentageDifference: number;
percentageDifference: number | null;
selectedLanguages: string[];
};
insights?: CompareInsights;
Expand All @@ -42,6 +42,17 @@ function getGithubProfileUrl(username: string): string {
return `https://github.com/${username}`;
}

function getPercentageDifference(
winnerScore: number,
loserScore: number,
): number | null {
if (loserScore <= 0) {
return winnerScore > loserScore ? null : 0;
}

return Math.round(((winnerScore - loserScore) / loserScore) * 100);
}

export function ResultDashboard({
user1,
user2,
Expand Down Expand Up @@ -88,13 +99,20 @@ export function ResultDashboard({
: winnerFromScores;
const loserUser =
overallWinnerUser === user1 ? user2 : overallWinnerUser === user2 ? user1 : null;
const winnerDiffPct =
overallWinnerUser && loserUser && loserUser.finalScore > 0
? Math.round(
((overallWinnerUser.finalScore - loserUser.finalScore) / loserUser.finalScore) *
100,
)
const winnerDiffPct = winner
? winner.percentageDifference
: overallWinnerUser && loserUser
? getPercentageDifference(overallWinnerUser.finalScore, loserUser.finalScore)
: 0;
const winnerDiffPoints =
winner?.finalScoreDifference ??
(overallWinnerUser && loserUser
? Math.round(Math.abs(overallWinnerUser.finalScore - loserUser.finalScore))
: 0);
const winnerLeadValue =
winnerDiffPct === null
? t("results.pointsLead", { points: winnerDiffPoints })
: `${winnerDiffPct}%`;

const handleCopy = async () => {
try {
Expand Down Expand Up @@ -210,7 +228,7 @@ export function ResultDashboard({
);
};

const renderSignalStats = (user: UserResult) => {
const renderSignalStats = (user: UserResult, idx: number) => {
if (!user.signals) return null;

const signalEntries: Array<{ label: string; value: string | number }> = [
Expand Down Expand Up @@ -258,7 +276,7 @@ export function ResultDashboard({
];

return (
<Card key={`signal-${user.username}`}>
<Card key={`signal-${user.username}-${idx}`}>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Avatar
Expand Down Expand Up @@ -320,7 +338,7 @@ export function ResultDashboard({
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground">{t("banner.leadby")}</p>
<p className="text-2xl font-bold text-primary">{winnerDiffPct}%</p>
<p className="text-2xl font-bold text-primary">{winnerLeadValue}</p>
</div>
</>
) : (
Expand Down Expand Up @@ -377,7 +395,12 @@ export function ResultDashboard({
{t("language.focus")}: {languageWinner.selectedLanguages.join(", ")}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{t("banner.leadby")} {languageWinner.percentageDifference}%
{t("banner.leadby")}{" "}
{languageWinner.percentageDifference === null
? t("results.pointsLead", {
points: languageWinner.finalScoreDifference,
})
: `${languageWinner.percentageDifference}%`}
</p>
<p className="mt-2 text-xs text-muted-foreground">{t("language.winnerNote")}</p>
</CardContent>
Expand Down Expand Up @@ -449,8 +472,8 @@ export function ResultDashboard({
{t("signals.title")}
</summary>
<div className="mt-4 grid gap-4 lg:grid-cols-2">
{renderSignalStats(user1)}
{renderSignalStats(user2)}
{renderSignalStats(user1, 1)}
{renderSignalStats(user2, 2)}
</div>
</details>

Expand Down
4 changes: 2 additions & 2 deletions components/top-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ export function TopList({ userResults, selectedLanguages = [] }: Props) {

return (
<div className="grid gap-6 md:grid-cols-2">
{userResults.map((user) => (
<Card key={`top-${user.username}`}>
{userResults.map((user, idx) => (
<Card key={`top-${user.username}-${idx}`}>
<CardHeader>
<CardTitle className="text-lg">
{t("topwork.titleForUser", { username: user.name || user.username })}
Expand Down
1 change: 1 addition & 0 deletions locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
"results.copied": "تم النسخ!",
"results.copy": "نسخ النتيجة",
"results.copyAria": "نسخ نتائج المقارنة إلى الحافظة",
"results.pointsLead": "+{points} نقطة",
"score.rawLabel": "الدرجة الخام: {value}",
"results.scoreVersion": "إصدار التقييم",
"section.language": "النتيجة اللغوية",
Expand Down
1 change: 1 addition & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
"results.copied": "Copied!",
"results.copy": "Copy Result",
"results.copyAria": "Copy comparison results to clipboard",
"results.pointsLead": "+{points} points",
"score.rawLabel": "Raw: {value}",
"results.scoreVersion": "Score version",
"section.language": "Language-focused Score",
Expand Down
94 changes: 92 additions & 2 deletions test/api/compare.route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,26 @@ function makeUser(login: string, name: string) {
};
}

function makeScore(finalScore: number) {
function makeLanguageScores(finalScore: number) {
return {
selectedLanguages: ["TypeScript"],
repoScore: 0,
prScore: 0,
contributionScore: 0,
finalScore,
normalizedRepoScore: 0,
normalizedPRScore: 0,
normalizedContributionScore: 0,
normalizedFinalScore: 0,
topRepos: [],
topPullRequests: [],
};
}

function makeScore(
finalScore: number,
languageScores?: ReturnType<typeof makeLanguageScores>,
) {
return {
repoScore: 10,
prScore: 20,
Expand All @@ -64,7 +83,7 @@ function makeScore(finalScore: number) {
topRepos: [],
topPullRequests: [],
topCommunityContributions: [],
languageScores: undefined,
languageScores,
signals: {
reposAnalyzed: 1,
pullRequestsAnalyzed: 1,
Expand Down Expand Up @@ -188,6 +207,77 @@ describe("GET /api/compare", () => {
expect(body.winner?.username).toBe("user-a");
});

test("returns null percentage when the losing final score is zero", async () => {
mocks.getUserData.mockResolvedValueOnce({
data: makeUser("user-a", "User A"),
metrics: { duration: 0, errors: [] },
});
mocks.getUserData.mockResolvedValueOnce({
data: makeUser("user-b", "User B"),
metrics: { duration: 0, errors: [] },
});

mocks.calculateUserScore.mockReturnValueOnce(makeScore(20));
mocks.calculateUserScore.mockReturnValueOnce(makeScore(0));

const response = await GET(
makeRequest({
username: ["user-a", "user-b"],
}),
);
const body = (await response.json()) as {
winner?: {
username: string;
finalScoreDifference: number;
percentageDifference: number | null;
};
};

expect(response.status).toBe(200);
expect(body.winner).toEqual({
username: "user-a",
finalScoreDifference: 20,
percentageDifference: null,
});
});

test("returns null language winner percentage when language loser score is zero", async () => {
mocks.getUserData.mockResolvedValueOnce({
data: makeUser("user-a", "User A"),
metrics: { duration: 0, errors: [] },
});
mocks.getUserData.mockResolvedValueOnce({
data: makeUser("user-b", "User B"),
metrics: { duration: 0, errors: [] },
});

mocks.calculateUserScore.mockReturnValueOnce(makeScore(20, makeLanguageScores(12)));
mocks.calculateUserScore.mockReturnValueOnce(makeScore(10, makeLanguageScores(0)));

const response = await GET(
makeRequest({
username: ["user-a", "user-b"],
selectedLanguage: "TypeScript",
}),
);
const body = (await response.json()) as {
languageWinner?: {
username: string;
finalScoreDifference: number;
percentageDifference: number | null;
selectedLanguages: string[];
};
};

expect(response.status).toBe(200);
expect(body.languageWinner).toEqual({
username: "user-a",
finalScoreDifference: 12,
percentageDifference: null,
selectedLanguages: ["TypeScript"],
});
});

test("returns targeted username for not-found errors", async () => {
mocks.getUserData.mockRejectedValueOnce(new Error("User not found"));

Expand Down
4 changes: 2 additions & 2 deletions types/api-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { UserResult } from "./user-result";
export type CompareWinner = {
username: string;
finalScoreDifference: number;
percentageDifference: number;
percentageDifference: number | null;
};

export type SafeApiError = {
Expand Down Expand Up @@ -48,7 +48,7 @@ export type ApiResponse = {
languageWinner?: {
username: string;
finalScoreDifference: number;
percentageDifference: number;
percentageDifference: number | null;
selectedLanguages: string[];
};
insights?: CompareInsights;
Expand Down
Loading