From 9d5e447b1c2d5e36ab5409ab4d07e757cfef8aca Mon Sep 17 00:00:00 2001 From: Abhinav Rastogi Date: Thu, 22 Oct 2026 00:51:15 +0530 Subject: [PATCH] refactor: share signal metric entries between profile and comparison --- .../components/result-dashboard.tsx | 28 +---------- .../components/user-profile-client.tsx | 43 +--------------- src/features/developer/index.ts | 1 + src/features/developer/signal-metrics.ts | 27 ++++++++++ .../developer/tests/signal-metrics.test.ts | 50 +++++++++++++++++++ 5 files changed, 82 insertions(+), 67 deletions(-) create mode 100644 src/features/developer/signal-metrics.ts create mode 100644 src/features/developer/tests/signal-metrics.test.ts diff --git a/src/features/comparison/components/result-dashboard.tsx b/src/features/comparison/components/result-dashboard.tsx index b7875b9..99ea9ee 100644 --- a/src/features/comparison/components/result-dashboard.tsx +++ b/src/features/comparison/components/result-dashboard.tsx @@ -13,7 +13,7 @@ import { InsightsList } from "./insights-list"; import { ScoreCard } from "@/components/cards"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import type { UserResult } from "@/features/developer"; +import { getSignalMetricEntries, type UserResult } from "@/features/developer"; import { useTranslation } from "@/components/providers/language-provider"; import { formatComparisonToMarkdown } from "../format-comparison-to-markdown"; import type { CompareInsights, CompareWinner } from "../types"; @@ -274,31 +274,7 @@ export function ResultDashboard({ if (!user.signals) return null; const signalEntries: Array<{ label: string; value: string | number }> = [ - { label: t("signals.reposAnalyzed"), value: user.signals.reposAnalyzed ?? "-" }, - { - label: t("signals.pullRequestsAnalyzed"), - value: user.signals.pullRequestsAnalyzed ?? "-", - }, - { label: t("signals.mergedExternalPRs"), value: user.signals.mergedExternalPRs ?? "-" }, - { label: t("signals.ownRepoPRsIgnored"), value: user.signals.ownRepoPRsIgnored ?? "-" }, - { label: t("signals.unmergedPRsIgnored"), value: user.signals.unmergedPRsIgnored ?? "-" }, - { - label: t("signals.uniqueExternalPRRepos"), - value: user.signals.uniqueExternalPRRepos ?? "-", - }, - { label: t("signals.issuesAnalyzed"), value: user.signals.issuesAnalyzed ?? "-" }, - { - label: t("signals.externalIssuesCounted"), - value: user.signals.externalIssuesCounted ?? "-", - }, - { - label: t("signals.discussionsAnalyzed"), - value: user.signals.discussionsAnalyzed ?? "-", - }, - { - label: t("signals.externalDiscussionsCounted"), - value: user.signals.externalDiscussionsCounted ?? "-", - }, + ...getSignalMetricEntries(user.signals, t), { label: t("signals.reposWithLanguageData"), value: user.signals.reposWithLanguageData ?? "-", diff --git a/src/features/developer/components/user-profile-client.tsx b/src/features/developer/components/user-profile-client.tsx index 5c76a8d..679e5b6 100644 --- a/src/features/developer/components/user-profile-client.tsx +++ b/src/features/developer/components/user-profile-client.tsx @@ -32,6 +32,7 @@ import { useTranslation } from "@/components/providers/language-provider"; import { getCountryCode, detectCountry } from "@/lib/geo"; import countriesData from "@/data/countries.json"; import type { UserResult } from "../types"; +import { getSignalMetricEntries } from "../signal-metrics"; type CountryInfo = { slug: string; @@ -121,47 +122,7 @@ export function UserProfileClient({ user, location, countryParam }: Props) { const handleCopyLink = () => copy(window.location.href); // Signal stats entries for transparency - const signalEntries = user.signals - ? [ - { label: t("signals.reposAnalyzed"), value: user.signals.reposAnalyzed ?? "-" }, - { - label: t("signals.pullRequestsAnalyzed"), - value: user.signals.pullRequestsAnalyzed ?? "-", - }, - { - label: t("signals.mergedExternalPRs"), - value: user.signals.mergedExternalPRs ?? "-", - }, - { - label: t("signals.ownRepoPRsIgnored"), - value: user.signals.ownRepoPRsIgnored ?? "-", - }, - { - label: t("signals.unmergedPRsIgnored"), - value: user.signals.unmergedPRsIgnored ?? "-", - }, - { - label: t("signals.uniqueExternalPRRepos"), - value: user.signals.uniqueExternalPRRepos ?? "-", - }, - { - label: t("signals.issuesAnalyzed"), - value: user.signals.issuesAnalyzed ?? "-", - }, - { - label: t("signals.externalIssuesCounted"), - value: user.signals.externalIssuesCounted ?? "-", - }, - { - label: t("signals.discussionsAnalyzed"), - value: user.signals.discussionsAnalyzed ?? "-", - }, - { - label: t("signals.externalDiscussionsCounted"), - value: user.signals.externalDiscussionsCounted ?? "-", - }, - ] - : []; + const signalEntries = getSignalMetricEntries(user.signals, t); const totalRawSum = Math.max( 1, diff --git a/src/features/developer/index.ts b/src/features/developer/index.ts index c22c164..781c06c 100644 --- a/src/features/developer/index.ts +++ b/src/features/developer/index.ts @@ -1,2 +1,3 @@ export * from "./types"; export * from "./components"; +export { getSignalMetricEntries } from "./signal-metrics"; diff --git a/src/features/developer/signal-metrics.ts b/src/features/developer/signal-metrics.ts new file mode 100644 index 0000000..c41e62b --- /dev/null +++ b/src/features/developer/signal-metrics.ts @@ -0,0 +1,27 @@ +import type { I18nContextValue } from "@/lib/i18n"; +import type { ScoringSignals } from "@/features/scoring"; + +const SIGNAL_METRICS = [ + "reposAnalyzed", + "pullRequestsAnalyzed", + "mergedExternalPRs", + "ownRepoPRsIgnored", + "unmergedPRsIgnored", + "uniqueExternalPRRepos", + "issuesAnalyzed", + "externalIssuesCounted", + "discussionsAnalyzed", + "externalDiscussionsCounted", +] as const satisfies readonly (keyof ScoringSignals)[]; + +export function getSignalMetricEntries( + signals: ScoringSignals | undefined, + t: I18nContextValue["t"], +): Array<{ label: string; value: number | string }> { + if (!signals) return []; + + return SIGNAL_METRICS.map((metric) => ({ + label: t(`signals.${metric}`), + value: signals[metric] ?? "-", + })); +} diff --git a/src/features/developer/tests/signal-metrics.test.ts b/src/features/developer/tests/signal-metrics.test.ts new file mode 100644 index 0000000..99caa96 --- /dev/null +++ b/src/features/developer/tests/signal-metrics.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import type { ScoringSignals } from "@/features/scoring"; +import en from "@/locales/en.json"; +import ar from "@/locales/ar.json"; +import { getSignalMetricEntries } from "../signal-metrics"; + +const signals: ScoringSignals = { + reposAnalyzed: 0, + pullRequestsAnalyzed: 2, + mergedExternalPRs: 3, + ownRepoPRsIgnored: 4, + unmergedPRsIgnored: 5, + uniqueExternalPRRepos: 6, + issuesAnalyzed: 7, + externalIssuesCounted: 8, + discussionsAnalyzed: 9, + externalDiscussionsCounted: 10, + reposWithLanguageData: 11, +}; + +describe("getSignalMetricEntries", () => { + it.each([en, ar])("preserves metric order, zeroes and translated labels", (messages) => { + const entries = getSignalMetricEntries(signals, (key) => messages[key as keyof typeof en]); + + expect(entries).toEqual([ + { label: messages["signals.reposAnalyzed"], value: 0 }, + { label: messages["signals.pullRequestsAnalyzed"], value: 2 }, + { label: messages["signals.mergedExternalPRs"], value: 3 }, + { label: messages["signals.ownRepoPRsIgnored"], value: 4 }, + { label: messages["signals.unmergedPRsIgnored"], value: 5 }, + { label: messages["signals.uniqueExternalPRRepos"], value: 6 }, + { label: messages["signals.issuesAnalyzed"], value: 7 }, + { label: messages["signals.externalIssuesCounted"], value: 8 }, + { label: messages["signals.discussionsAnalyzed"], value: 9 }, + { label: messages["signals.externalDiscussionsCounted"], value: 10 }, + ]); + }); + + it("returns no entries when signals are absent", () => { + expect(getSignalMetricEntries(undefined, (key) => key)).toEqual([]); + }); + + it("retains the fallback for incomplete cached signals without mutating input", () => { + const incomplete = Object.freeze({ reposAnalyzed: 0 }) as ScoringSignals; + const entries = getSignalMetricEntries(incomplete, (key) => key); + expect(entries[0].value).toBe(0); + expect(entries.slice(1).every((entry) => entry.value === "-")).toBe(true); + expect(incomplete).toEqual({ reposAnalyzed: 0 }); + }); +});