-
+
-
+
{(data.score ?? 0).toFixed(2)}
-
{t('comparsion.score')}
+
{t('comparsion.score')}
);
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..d763cd9
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx
new file mode 100644
index 0000000..0118624
--- /dev/null
+++ b/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/next-env.d.ts b/next-env.d.ts
index c4b7818..9edff1c 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 64e9b95..811389d 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -10,34 +10,34 @@ const config: Config = {
theme: {
extend: {
colors: {
- border: "hsl(210 24% 90%)",
- input: "hsl(210 24% 90%)",
- ring: "#3B82F6",
- background: "hsl(0 0% 100%)",
- foreground: "hsl(222 47% 11%)",
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
muted: {
- DEFAULT: "hsl(210 20% 96%)",
- foreground: "hsl(215 16% 47%)",
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
},
card: {
- DEFAULT: "hsl(0 0% 100%)",
- foreground: "hsl(222 47% 11%)",
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
},
primary: {
- DEFAULT: "#3B82F6",
- foreground: "#F8FBFF",
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
},
secondary: {
- DEFAULT: "#22D3EE",
- foreground: "#F8FBFF",
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
},
accent: {
- DEFAULT: "#0EA5E9",
- foreground: "#F8FBFF",
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
},
destructive: {
- DEFAULT: "hsl(0 84% 60%)",
- foreground: "hsl(210 40% 98%)",
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
},
},
boxShadow: {
From 137026ea0834482b5bd1de1cca06c7073085864d Mon Sep 17 00:00:00 2001
From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com>
Date: Fri, 17 Apr 2026 02:03:24 +0300
Subject: [PATCH 3/4] feat: enhance the ui/ux,
---
app/layout.tsx | 27 ++++++++
app/page.tsx | 88 +++++++++++-------------
components/app-footer.tsx | 73 ++++++++++++++++++++
components/app-header.tsx | 18 +++++
components/brand-logo.tsx | 40 +++++++++++
components/comparison-chart.tsx | 72 ++++++++++++++++----
components/comparison-table.tsx | 40 ++++++++---
components/result-dashboard.tsx | 38 ++++++++---
components/theme-toggle.tsx | 11 +--
components/top-list.tsx | 54 +++++++--------
locales/ar.json | 116 ++++++++++++++++++--------------
locales/en.json | 51 +++++++++-----
public/logo.svg | 46 +++++++++++++
13 files changed, 498 insertions(+), 176 deletions(-)
create mode 100644 components/app-footer.tsx
create mode 100644 components/app-header.tsx
create mode 100644 components/brand-logo.tsx
create mode 100644 public/logo.svg
diff --git a/app/layout.tsx b/app/layout.tsx
index c6898f4..328aa41 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,16 +1,43 @@
import "./globals.css";
import type { ReactNode } from "react";
+import Script from "next/script";
import Providers from "./providers";
+const themeInitScript = `
+ try {
+ const storageKey = "devimpact-theme";
+ const storedTheme = window.localStorage.getItem(storageKey);
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
+ const theme =
+ storedTheme === "light" || storedTheme === "dark"
+ ? storedTheme
+ : prefersDark
+ ? "dark"
+ : "light";
+
+ const root = document.documentElement;
+ root.classList.toggle("dark", theme === "dark");
+ root.style.colorScheme = theme;
+ } catch {}
+`;
+
export const metadata = {
title: "DevImpact",
description: "GitHub user scoring",
+ icons: {
+ icon: "/logo.svg",
+ shortcut: "/logo.svg",
+ apple: "/logo.svg",
+ },
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
+
{children}
diff --git a/app/page.tsx b/app/page.tsx
index 776ff42..8504569 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -5,8 +5,10 @@ import { CompareForm } from "../components/compare-form";
import { ResultDashboard } from "../components/result-dashboard";
import { DashboardSkeleton } from "../components/skeletons";
import { UserResult } from "@/types/user-result";
-import { LanguageSwitcher } from "@/components/language-switcher";
-import { ThemeToggle } from "@/components/theme-toggle";
+import { BrandLogo } from "@/components/brand-logo";
+import { AppHeader } from "@/components/app-header";
+import { AppFooter } from "@/components/app-footer";
+import { useTranslation } from "@/components/language-provider";
type ApiResponse = {
success: boolean;
@@ -15,6 +17,7 @@ type ApiResponse = {
};
export default function HomePage() {
+ const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [error, setError] = useState
(null);
const [data, setData] = useState<{
@@ -22,19 +25,40 @@ export default function HomePage() {
user2: UserResult;
} | null>(null);
+ const localizeErrorMessage = (message?: string) => {
+ switch (message) {
+ case "provide at least one username param":
+ return t("error.missingUsername");
+ case "GitHub user not found":
+ return t("error.userNotFound");
+ case "Failed to calculate score":
+ return t("error.calculateFailed");
+ case "Comparison failed":
+ return t("error.comparisonFailed");
+ case "Failed to fetch":
+ return t("error.fetchFailed");
+ default:
+ return message || t("error.generic");
+ }
+ };
+
const handleCompare = async (u1: string, u2: string) => {
setLoading(true);
setError(null);
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 (!body.success || !body.users || body.users.length < 2) {
- throw new Error(body.error || "Comparison failed");
+ throw new Error(localizeErrorMessage(body.error || "Comparison failed"));
}
+
if (body.users[0].finalScore > body.users[1].finalScore) {
setData({
user1: { ...body.users[0], isWinner: true },
@@ -49,7 +73,7 @@ export default function HomePage() {
setData({ user1: body.users[0], user2: body.users[1] });
}
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : "Failed to fetch");
+ setError(localizeErrorMessage(err instanceof Error ? err.message : undefined));
} finally {
setLoading(false);
}
@@ -61,29 +85,17 @@ export default function HomePage() {
setData(null);
setError(null);
};
+
const swapUsers = () => {
if (!data) return;
setData((d) => ({ user1: d!.user2, user2: d!.user1 }));
- console.log("Swapped users", data);
};
+
return (
- {" "}
-
-
-
-
- DevImpact
-
-
+
-
-
-
-
-
-
-
+
+
{error}
)}
+
{data && }
+
{!loading && !error && !data && (
-
-
-
Enter two usernames to compare
-
- Compare GitHub developer metrics side by side
-
+
+
+
{t("page.empty.title")}
+
{t("page.empty.description")}
)}
-
+
+
);
}
diff --git a/components/app-footer.tsx b/components/app-footer.tsx
new file mode 100644
index 0000000..cf2f07f
--- /dev/null
+++ b/components/app-footer.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import { useTranslation } from "@/components/language-provider";
+import { cn } from "@/lib/utils";
+
+export function AppFooter() {
+ const { t, dir } = useTranslation();
+
+ return (
+
+ );
+}
diff --git a/components/app-header.tsx b/components/app-header.tsx
new file mode 100644
index 0000000..97eb99d
--- /dev/null
+++ b/components/app-header.tsx
@@ -0,0 +1,18 @@
+import { BrandLogo } from "@/components/brand-logo";
+import { LanguageSwitcher } from "@/components/language-switcher";
+import { ThemeToggle } from "@/components/theme-toggle";
+
+export function AppHeader() {
+ return (
+
+ );
+}
diff --git a/components/brand-logo.tsx b/components/brand-logo.tsx
new file mode 100644
index 0000000..969b587
--- /dev/null
+++ b/components/brand-logo.tsx
@@ -0,0 +1,40 @@
+import Image from "next/image";
+import { cn } from "@/lib/utils";
+
+type BrandLogoProps = {
+ size?: "sm" | "md" | "lg" | "xl";
+ className?: string;
+ priority?: boolean;
+};
+
+const sizeClasses = {
+ sm: "h-12 w-12 sm:h-16 sm:w-16",
+ md: "h-14 w-14 sm:h-20 sm:w-20",
+ lg: "h-16 w-16 sm:h-24 sm:w-24",
+ xl: "h-24 w-24 sm:h-32 sm:w-32",
+} as const;
+
+export function BrandLogo({
+ size = "md",
+ className,
+ priority = false,
+}: BrandLogoProps) {
+ return (
+
+
+
+ );
+}
diff --git a/components/comparison-chart.tsx b/components/comparison-chart.tsx
index a6b6750..c406253 100644
--- a/components/comparison-chart.tsx
+++ b/components/comparison-chart.tsx
@@ -31,39 +31,87 @@ const metrics = [
];
export function ComparisonChart({ user1, user2 }: Props) {
- const {t} = useTranslation();
+ const { t, dir } = useTranslation();
+ const isRtl = dir === "rtl";
const data = metrics.map((m) => ({
name: t(m.label),
[user1.username]: user1[m.key as keyof UserResult] ?? 0,
[user2.username]: user2[m.key as keyof UserResult] ?? 0,
}));
- const renderLegendText = (value: string) => {
- return {value};
-};
+
+ const renderLegendText = (value: string) => {
+ return {value};
+ };
+ const renderYAxisTick = ({
+ x,
+ y,
+ payload,
+ }: {
+ x: number;
+ y: number;
+ payload: { value: number | string };
+ }) => (
+
+ {payload.value}
+
+ );
return (
- {t('barchart.title')}
+ {t("barchart.title")}
- {t('barchart.desc')}
+ {t("barchart.desc")}
-
+
-
-
+
+
+
-
{[user1, user2].map((user, idx) => (
-
-
+
+
{user.name || user.username}
{user.isWinner && (
- Winner
+
+ {t("banner.winner")}
+
)}
- {t("comparsion.final.score")}
+
+ {t("comparsion.final.score")}
+
{user.finalScore}
{t("comparsion.repo.score")}
- (idx === 0 ? user2.repoScore : user1.repoScore) ? "text-primary" : ""}`}>
+ (idx === 0 ? user2.repoScore : user1.repoScore) ? "text-primary" : ""}`}
+ >
{user.repoScore}
{t("comparsion.pr.score")}
- (idx === 0 ? user2.prScore : user1.prScore) ? "text-primary" : ""}`}>
+ (idx === 0 ? user2.prScore : user1.prScore) ? "text-primary" : ""}`}
+ >
{user.prScore}
- {t("comparsion.contribution.score")}
- (idx === 0 ? user2.contributionScore : user1.contributionScore) ? "text-primary" : ""}`}>
+
+ {t("comparsion.contribution.score")}
+
+ (idx === 0 ? user2.contributionScore : user1.contributionScore) ? "text-primary" : ""}`}
+ >
{user.contributionScore}
diff --git a/components/result-dashboard.tsx b/components/result-dashboard.tsx
index 4bab6b4..fd03be2 100644
--- a/components/result-dashboard.tsx
+++ b/components/result-dashboard.tsx
@@ -39,15 +39,25 @@ export function ResultDashboard({ user1, user2 }: Props) {
const prDiff =
Math.max(user1.prScore, user2.prScore) -
Math.min(user1.prScore, user2.prScore);
+
const getInsights = () => {
- const insights = [];
+ const insights: string[] = [];
+
if (user1.repoScore > user2.repoScore) {
insights.push(
- `${user1.username} ${t("insights.stronger.repo")} ${user1.repoScore} (vs) ${user2.repoScore}`,
+ t("insights.repo.leader", {
+ username: user1.username,
+ score: user1.repoScore,
+ other: user2.repoScore,
+ })
);
} else if (user2.repoScore > user1.repoScore) {
insights.push(
- `${user2.username} ${t("insights.stronger.repo")} ${user2.repoScore} (vs) ${user1.repoScore}`,
+ t("insights.repo.leader", {
+ username: user2.username,
+ score: user2.repoScore,
+ other: user1.repoScore,
+ })
);
} else {
insights.push(t("insights.equal.repo"));
@@ -55,20 +65,28 @@ export function ResultDashboard({ user1, user2 }: Props) {
if (user1.prScore > user2.prScore) {
insights.push(
- `${user1.username} ${t("insights.pull.leads")} ${user1.prScore} (vs) ${user2.prScore}`,
+ t("insights.pr.leader", {
+ username: user1.username,
+ score: user1.prScore,
+ other: user2.prScore,
+ })
);
} else if (user2.prScore > user1.prScore) {
insights.push(
- `${user2.username} ${t("insights.pull.leads")} ${user2.prScore} (vs) ${user1.prScore}`,
+ t("insights.pr.leader", {
+ username: user2.username,
+ score: user2.prScore,
+ other: user1.prScore,
+ })
);
} else {
insights.push(t("insights.equal.pr"));
}
if (user1.contributionScore > user2.contributionScore) {
- insights.push(`${user1.username} ${t("insights.higher.contribution")}`);
+ insights.push(t("insights.contribution.leader", { username: user1.username }));
} else if (user2.contributionScore > user1.contributionScore) {
- insights.push(`${user2.username} ${t("insights.higher.contribution")}`);
+ insights.push(t("insights.contribution.leader", { username: user2.username }));
} else {
insights.push(t("insights.equal.contribution"));
}
@@ -142,17 +160,17 @@ export function ResultDashboard({ user1, user2 }: Props) {
size="sm"
onClick={handleCopy}
className="flex items-center gap-2"
- aria-label="Copy comparison results to clipboard"
+ aria-label={t("results.copyAria")}
>
{copied ? (
<>
- Copied!
+ {t("results.copied")}
>
) : (
<>
- Copy Result
+ {t("results.copy")}
>
)}
diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx
index ccae4ee..27a1efc 100644
--- a/components/theme-toggle.tsx
+++ b/components/theme-toggle.tsx
@@ -3,12 +3,15 @@
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "./ui/button";
-import { useEffect, useState } from "react";
+import { useSyncExternalStore } from "react";
+import { useTranslation } from "./language-provider";
+
+const emptySubscribe = () => () => {};
export function ThemeToggle() {
+ const { t } = useTranslation();
const { theme, setTheme, resolvedTheme } = useTheme();
- const [mounted, setMounted] = useState(false);
- useEffect(() => setMounted(true), []);
+ const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);
const THEME_TRANSITION_MS = 520;
const current = resolvedTheme || theme || "light";
@@ -35,7 +38,7 @@ export function ThemeToggle() {
size="sm"
onClick={handleToggle}
className="flex items-center gap-2"
- aria-label="Toggle color mode"
+ aria-label={t("theme.toggle")}
>
{mounted && current === "dark" ? : }
diff --git a/components/top-list.tsx b/components/top-list.tsx
index bafd405..515d2f3 100644
--- a/components/top-list.tsx
+++ b/components/top-list.tsx
@@ -3,9 +3,9 @@ import { Eye, GitFork, GitPullRequest, Minus, Plus, Star } from "lucide-react";
import {
Card,
CardContent,
+ CardDescription,
CardHeader,
CardTitle,
- CardDescription,
} from "./ui/card";
import { UserResult } from "@/types/user-result";
import {
@@ -21,6 +21,7 @@ type Props = {
export function TopList({ userResults }: Props) {
const { t } = useTranslation();
+
const cardDetails = (data: {
title: string;
titleUrl?: string;
@@ -30,7 +31,7 @@ export function TopList({ userResults }: Props) {
key: string | number;
}) => (
@@ -40,7 +41,7 @@ export function TopList({ userResults }: Props) {
href={data.titleUrl}
target="_blank"
rel="noopener noreferrer"
- className="hover:underline text-primary"
+ className="text-primary hover:underline"
>
{data.title}
@@ -48,10 +49,10 @@ export function TopList({ userResults }: Props) {
data.title
)}
-
+
{data.subtitle}
-
+
{data.badges.map((badge, i) => (
@@ -70,33 +71,33 @@ export function TopList({ userResults }: Props) {
{(data.score ?? 0).toFixed(2)}
- {t('comparsion.score')}
+
+ {t("comparsion.score")}
+
);
return (
-
+
{userResults.map((user) => (
- {t('topwork.title')} • {user.username}
+ {t("topwork.titleForUser", { username: user.username })}
-
- {t('topwork.desc')}
-
+ {t("topwork.desc")}
-
- {t('topwork.toprepos')}
+
+ {t("topwork.toprepos")}
{user.topRepos.slice(0, 3).map((repo, i) =>
cardDetails({
key: `repo-${i}`,
- title: repo.name || t('untitled'),
+ title: repo.name || t("untitled"),
titleUrl: repo.name
? `https://github.com/${user.username}/${repo.name}`
: undefined,
@@ -105,64 +106,63 @@ export function TopList({ userResults }: Props) {
{
icon:
,
label: repo.stars,
- tooltip: `${repo.stars} ${t('topwork.stars')}`,
+ tooltip: `${repo.stars} ${t("topwork.stars")}`,
},
{
icon:
,
label: repo.forks,
- tooltip: `${repo.forks} ${t('topwork.forks')}`,
+ tooltip: `${repo.forks} ${t("topwork.forks")}`,
},
{
icon:
,
label: repo.watchers,
- tooltip: `${repo.watchers} ${t('topwork.watchers')}`,
+ tooltip: `${repo.watchers} ${t("topwork.watchers")}`,
},
],
}),
)}
{user.topRepos.length === 0 && (
- {t('topwork.norepos')}
+ {t("topwork.noRepos")}
)}
-
- {t('topwork.topprs')}
+
+ {t("topwork.topprs")}
{user.topPullRequests.slice(0, 3).map((pr, i) =>
cardDetails({
key: `pr-${i}`,
- title: pr.title || t('untitled'),
+ title: pr.title || t("untitled"),
titleUrl: pr.url,
- subtitle: `in ${pr.repo}`,
+ subtitle: t("topwork.inRepo", { repo: pr.repo || "" }),
score: pr.score,
badges: [
{
icon:
,
label: pr.stars,
- tooltip: `${pr.stars} ${t('topwork.pr.repo.stars')}`,
+ tooltip: `${pr.stars} ${t("topwork.pr.repo.stars")}`,
},
-
{
icon:
,
label: pr.additions || "0",
- tooltip: `+${pr.additions || 0} ${t('topwork.pr.additions')}`,
+ tooltip: `+${pr.additions || 0} ${t("topwork.pr.additions")}`,
},
{
icon:
,
label: pr.deletions || "0",
- tooltip: `-${pr.deletions || 0} ${t('topwork.pr.deletions')}`,
+ tooltip: `-${pr.deletions || 0} ${t("topwork.pr.deletions")}`,
},
],
}),
)}
{user.topPullRequests.length === 0 && (
- {t('topwork.noPRs')}
+ {t("topwork.noPRs")}
)}
diff --git a/locales/ar.json b/locales/ar.json
index 6c8f967..7c67016 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -1,55 +1,73 @@
{
- "app.subtitle": "قارن بين مطورين",
- "app.title": "قارن بين مطوري الجيتهب (GitHub)",
- "banner.leadby": "متقدم بـ",
- "banner.metric": "المقاييس",
- "banner.tie": "تعادل — كلا المطورين متساويان في الأداء.",
+ "app.subtitle": "قارن بين مطورين جنبًا إلى جنب",
+ "app.title": "قارن مطوري GitHub",
+ "banner.leadby": "يتقدم بنسبة",
+ "banner.metric": "النتيجة",
+ "banner.tie": "النتيجة متعادلة. أداء المطورين متقارب.",
"banner.winner": "الفائز",
- "barchart.desc": "تحليل بصري للمقاييس الرئيسية",
- "barchart.title": "مقارنة الأدء",
- "breakdown.contribution": "نقاط المساهمات (Contributions)",
- "breakdown.description": "أشرطة تقدم تظهر الأداء النسبي",
- "breakdown.pr": "نقاط طلبات الدمج (PRs)",
- "breakdown.repo": "نقاط المشاريع (Repos)",
- "breakdown.title": "مقارنة تفصيلية",
- "comparsion.activity.score": "نقاط النشاط (Activity)",
- "comparsion.contribution.diff": "فرق المساهمات",
- "comparsion.contribution.score": "نقاط المساهمات (Contributions)",
- "comparsion.diff": "فارق التقييم",
- "comparsion.final.score": "النتيجة النهائية",
- "comparsion.pr.diff": "فرق طلبات الدمج",
- "comparsion.pr.score": "نقاط طلبات الدمج (PRs)",
- "comparsion.repo.diff": "فرق المشاريع",
- "comparsion.repo.score": "نقاط المشاريع (Repos)",
- "comparsion.score": "التقييم",
- "error.generic": "فشل في جلب البيانات",
+ "barchart.desc": "عرض مرئي لأهم المقاييس",
+ "barchart.title": "مقارنة الدرجات",
+ "breakdown.contribution": "درجة المساهمات",
+ "breakdown.description": "أشرطة تقدم توضح الأداء النسبي",
+ "breakdown.pr": "درجة طلبات السحب",
+ "breakdown.repo": "درجة المستودعات",
+ "breakdown.title": "تفصيل الدرجات",
+ "comparison.avatarAlt": "الصورة الرمزية لـ{name}",
+ "comparsion.activity.score": "درجة النشاط",
+ "comparsion.contribution.diff": "فارق المساهمات",
+ "comparsion.contribution.score": "درجة المساهمات",
+ "comparsion.diff": "فارق الدرجات",
+ "comparsion.final.score": "الدرجة النهائية",
+ "comparsion.pr.diff": "فارق طلبات السحب",
+ "comparsion.pr.score": "درجة طلبات السحب",
+ "comparsion.repo.diff": "فارق المستودعات",
+ "comparsion.repo.score": "درجة المستودعات",
+ "comparsion.score": "الدرجة",
+ "error.calculateFailed": "تعذر حساب النتيجة",
+ "error.comparisonFailed": "فشلت المقارنة",
+ "error.fetchFailed": "تعذر جلب البيانات",
+ "error.generic": "تعذر جلب البيانات",
+ "error.missingUsername": "أدخل اسم مستخدم واحدًا على الأقل.",
+ "error.userNotFound": "لم يتم العثور على مستخدم GitHub",
+ "footer.description": "قارن مقاييس مطوري GitHub جنبًا إلى جنب مع رؤية أوضح للنشاط والاستمرارية والأثر العام.",
+ "footer.eyebrow": "مقارنة المطورين",
+ "footer.note": "مصمم لمراجعات احترافية سريعة جنبًا إلى جنب عندما تحتاج إلى إشارة أوضح من تصفح الملفات الشخصية فقط.",
+ "footer.summary": "قارن مقاييس مطوري GitHub",
+ "footer.tag": "مقاييس GitHub",
+ "footer.tagline": "رؤية أوضح. تقييم أسرع.",
"form.compare": "قارن",
"form.compare.ing": "جارٍ المقارنة...",
- "form.enterTwo": "أدخل اسم مستخدمين من GitHub لمقارنة مقاييسهم كمطورين",
+ "form.enterTwo": "أدخل اسمي مستخدم على GitHub لمقارنة مقاييسهما",
"form.reset": "إعادة تعيين",
- "form.swap": "تبديل",
- "form.username1": "اسم المستخدم 1 (مثال: torvalds)",
- "form.username2": "اسم المستخدم 2 (مثال: octocat)",
- "insights.equal.contribution": "كلا المطورين لديهم مستويات مساهمة متشابهة",
- "insights.equal.pr": "كلا المطورين لديهم تأثير طلبات الدمج متساوي",
- "insights.equal.repo": "كلا المطورين لديهم قوة مشاريع متساوية",
- "insights.higher.contribution": "يُظهر نشاطاً أكثر في المساهمات",
- "insights.pull.leads": "يتفوق في تأثير طلبات الدمج بـ",
- "insights.stronger.contribution": "لديه مساهمات عامة أقوى بـ",
- "insights.stronger.pr": "لديه تأثير أقوى في طلبات الدمج بـ",
- "insights.stronger.repo": "لديه مشاريع أقوى بـ",
- "insights.title": "أهم الملاحظات",
- "topwork.desc": "المشاريع والإسهامات الأكثر تأثيراً",
- "topwork.forks": "استنساخات",
- "topwork.noPRs": "لم يتم العثور على طلبات دمج",
- "topwork.noRepos": "لم يتم العثور على مشاريع",
- "topwork.pr.additions": "الإضافات",
- "topwork.pr.deletions": "الحذف",
- "topwork.pr.repo.stars": "النجوم في مستودع الطلب",
- "topwork.stars": "إعجابات",
- "topwork.title": "أفضل الأعمال",
- "topwork.topprs": "طلبات الدمج",
- "topwork.toprepos": "المشاريع",
- "topwork.watchers": "متابعون",
+ "form.swap": "تبديل المستخدمين",
+ "form.username1": "اسم المستخدم الأول (مثل torvalds)",
+ "form.username2": "اسم المستخدم الثاني (مثل octocat)",
+ "insights.contribution.leader": "{username} يظهر نشاطًا أعلى في المساهمات",
+ "insights.equal.contribution": "كلا المطورين متقاربان في مستوى المساهمات",
+ "insights.equal.pr": "كلا المطورين متساويان في تأثير طلبات السحب",
+ "insights.equal.repo": "كلا المطورين متقاربان في قوة المستودعات",
+ "insights.pr.leader": "{username} يتقدم في تأثير طلبات السحب بنتيجة {score} مقابل {other}",
+ "insights.repo.leader": "{username} يمتلك محفظة مستودعات أقوى بنتيجة {score} مقابل {other}",
+ "insights.title": "أبرز الملاحظات",
+ "page.empty.description": "قارن مقاييس مطوري GitHub جنبًا إلى جنب",
+ "page.empty.title": "أدخل اسمي مستخدم للمقارنة",
+ "results.copied": "تم النسخ!",
+ "results.copy": "نسخ النتيجة",
+ "results.copyAria": "انسخ نتائج المقارنة إلى الحافظة",
+ "theme.toggle": "تبديل وضع الألوان",
+ "topwork.desc": "أكثر المستودعات وطلبات السحب تأثيرًا",
+ "topwork.forks": "تفرعات",
+ "topwork.inRepo": "في {repo}",
+ "topwork.noPRs": "لا توجد طلبات سحب",
+ "topwork.noRepos": "لا توجد مستودعات",
+ "topwork.pr.additions": "إضافات",
+ "topwork.pr.deletions": "حذوفات",
+ "topwork.pr.repo.stars": "نجوم على مستودع طلب السحب",
+ "topwork.stars": "نجوم",
+ "topwork.title": "أبرز الأعمال",
+ "topwork.titleForUser": "أبرز الأعمال - {username}",
+ "topwork.topprs": "أفضل طلبات السحب",
+ "topwork.toprepos": "أفضل المستودعات",
+ "topwork.watchers": "مراقبون",
"untitled": "بدون عنوان"
-}
\ No newline at end of file
+}
diff --git a/locales/en.json b/locales/en.json
index b279b22..00b567f 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -1,9 +1,9 @@
{
- "app.subtitle": "Compare two developers",
+ "app.subtitle": "Evaluate two developers side by side",
"app.title": "Compare GitHub Developers",
- "banner.leadby": "Lead by",
- "banner.metric": "Metric",
- "banner.tie": "It's a tie — both developers are evenly matched.",
+ "banner.leadby": "Leads by",
+ "banner.metric": "Result",
+ "banner.tie": "It's a tie. Both developers are evenly matched.",
"banner.winner": "Winner",
"barchart.desc": "Visual breakdown of key metrics",
"barchart.title": "Score Comparison",
@@ -12,17 +12,29 @@
"breakdown.pr": "Pull Request Score",
"breakdown.repo": "Repository Score",
"breakdown.title": "Detailed Breakdown",
+ "comparison.avatarAlt": "{name}'s avatar",
"comparsion.activity.score": "Activity Score",
- "comparsion.contribution.diff": "Contribution diff",
+ "comparsion.contribution.diff": "Contribution Difference",
"comparsion.contribution.score": "Contribution Score",
"comparsion.diff": "Score Difference",
"comparsion.final.score": "Final Score",
- "comparsion.pr.diff": "PR diff",
+ "comparsion.pr.diff": "PR Difference",
"comparsion.pr.score": "PR Score",
- "comparsion.repo.diff": "Repo diff",
- "comparsion.repo.score": "Repo Score",
+ "comparsion.repo.diff": "Repository Difference",
+ "comparsion.repo.score": "Repository Score",
"comparsion.score": "Score",
+ "error.calculateFailed": "Failed to calculate score",
+ "error.comparisonFailed": "Comparison failed",
+ "error.fetchFailed": "Failed to fetch",
"error.generic": "Failed to fetch",
+ "error.missingUsername": "Provide at least one username.",
+ "error.userNotFound": "GitHub user not found",
+ "footer.description": "Compare GitHub developer metrics side by side with a clearer view of activity, consistency, and overall impact.",
+ "footer.eyebrow": "Developer Comparison",
+ "footer.note": "Built for quick, professional side-by-side reviews when you need a cleaner signal than raw profile browsing.",
+ "footer.summary": "Compare GitHub developer metrics",
+ "footer.tag": "GitHub Metrics",
+ "footer.tagline": "Sharper insights. Faster evaluation.",
"form.compare": "Compare",
"form.compare.ing": "Comparing...",
"form.enterTwo": "Enter two GitHub usernames to compare their developer metrics",
@@ -30,27 +42,32 @@
"form.swap": "Swap users",
"form.username1": "Username 1 (e.g., torvalds)",
"form.username2": "Username 2 (e.g., octocat)",
+ "insights.contribution.leader": "{username} shows higher contribution activity",
"insights.equal.contribution": "Both developers have similar contribution levels",
"insights.equal.pr": "Both developers have equal pull request impact",
- "insights.equal.repo": "Both developers have equal repository strength",
- "insights.higher.contribution": "shows higher contribution activity",
- "insights.pull.leads": "leads in pull request impact",
- "insights.stronger.contribution": "has stronger overall contribution with",
- "insights.stronger.pr": "has stronger pull request impact with",
- "insights.stronger.repo": "has stronger repository portfolio with",
+ "insights.equal.repo": "Both developers have similar repository strength",
+ "insights.pr.leader": "{username} leads in pull request impact with {score} vs {other}",
+ "insights.repo.leader": "{username} has a stronger repository portfolio with {score} vs {other}",
"insights.title": "Key Insights",
- "toggle.direction": "Toggle direction ({dir})",
+ "page.empty.description": "Compare GitHub developer metrics side by side",
+ "page.empty.title": "Enter two usernames to compare",
+ "results.copied": "Copied!",
+ "results.copy": "Copy Result",
+ "results.copyAria": "Copy comparison results to clipboard",
+ "theme.toggle": "Toggle color mode",
"topwork.desc": "Most impactful repositories and pull requests",
"topwork.forks": "forks",
+ "topwork.inRepo": "in {repo}",
"topwork.noPRs": "No pull requests found",
"topwork.noRepos": "No repositories found",
"topwork.pr.additions": "additions",
"topwork.pr.deletions": "deletions",
- "topwork.pr.repo.stars": "stars on the PR's repository",
+ "topwork.pr.repo.stars": "stars on the PR repository",
"topwork.stars": "stars",
"topwork.title": "Top Work",
+ "topwork.titleForUser": "Top Work - {username}",
"topwork.topprs": "Top Pull Requests",
"topwork.toprepos": "Top Repositories",
"topwork.watchers": "watchers",
"untitled": "Untitled"
-}
\ No newline at end of file
+}
diff --git a/public/logo.svg b/public/logo.svg
new file mode 100644
index 0000000..842fb05
--- /dev/null
+++ b/public/logo.svg
@@ -0,0 +1,46 @@
+
+
From cdb7347b1f94c3b32527a04ff13b9f525883c29c Mon Sep 17 00:00:00 2001
From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com>
Date: Fri, 17 Apr 2026 02:23:42 +0300
Subject: [PATCH 4/4] Merge branch 'main' of https://github.com/O2sa/DevImpact
into feat/enhance-ui-ux
---
components/comparison-chart.tsx | 2 --
components/comparison-table.tsx | 1 -
components/result-dashboard.tsx | 35 ---------------------------------
components/top-list.tsx | 4 ----
4 files changed, 42 deletions(-)
diff --git a/components/comparison-chart.tsx b/components/comparison-chart.tsx
index ac7dfcc..b36474b 100644
--- a/components/comparison-chart.tsx
+++ b/components/comparison-chart.tsx
@@ -18,7 +18,6 @@ import {
import { BarChart3 } from "lucide-react";
import { UserResult } from "@/types/user-result";
import { useTranslation } from "./language-provider";
-import { useTranslation } from "./language-provider";
type Props = {
user1: UserResult;
@@ -39,7 +38,6 @@ export function ComparisonChart({ user1, user2 }: Props) {
const isRtl = dir === "rtl";
const data = metrics.map((m) => ({
- name: t(m.label),
name: t(m.label),
[user1.username]: user1[m.key as keyof UserResult] ?? 0,
[user2.username]: user2[m.key as keyof UserResult] ?? 0,
diff --git a/components/comparison-table.tsx b/components/comparison-table.tsx
index c3587fc..5720117 100644
--- a/components/comparison-table.tsx
+++ b/components/comparison-table.tsx
@@ -2,7 +2,6 @@ import Image from "next/image";
import { UserResult } from "@/types/user-result";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
import { useTranslation } from "./language-provider";
-import { useTranslation } from "./language-provider";
type ComparisonTableProps = {
user1: UserResult;
diff --git a/components/result-dashboard.tsx b/components/result-dashboard.tsx
index 131d97a..d608a6d 100644
--- a/components/result-dashboard.tsx
+++ b/components/result-dashboard.tsx
@@ -39,42 +39,7 @@ export function ResultDashboard({ user1, user2 }: Props) {
const prDiff =
Math.max(user1.prScore, user2.prScore) -
Math.min(user1.prScore, user2.prScore);
- const getInsights = () => {
- const insights = [];
- if (user1.repoScore > user2.repoScore) {
- insights.push(
- `${user1.username} ${t("insights.stronger.repo")} ${user1.repoScore} (vs) ${user2.repoScore}`,
- );
- } else if (user2.repoScore > user1.repoScore) {
- insights.push(
- `${user2.username} ${t("insights.stronger.repo")} ${user2.repoScore} (vs) ${user1.repoScore}`,
- );
- } else {
- insights.push(t("insights.equal.repo"));
- }
-
- if (user1.prScore > user2.prScore) {
- insights.push(
- `${user1.username} ${t("insights.pull.leads")} ${user1.prScore} (vs) ${user2.prScore}`,
- );
- } else if (user2.prScore > user1.prScore) {
- insights.push(
- `${user2.username} ${t("insights.pull.leads")} ${user2.prScore} (vs) ${user1.prScore}`,
- );
- } else {
- insights.push(t("insights.equal.pr"));
- }
-
- if (user1.contributionScore > user2.contributionScore) {
- insights.push(`${user1.username} ${t("insights.higher.contribution")}`);
- } else if (user2.contributionScore > user1.contributionScore) {
- insights.push(`${user2.username} ${t("insights.higher.contribution")}`);
- } else {
- insights.push(t("insights.equal.contribution"));
- }
- return insights;
- };
const getInsights = () => {
const insights: string[] = [];
diff --git a/components/top-list.tsx b/components/top-list.tsx
index c010379..515d2f3 100644
--- a/components/top-list.tsx
+++ b/components/top-list.tsx
@@ -1,6 +1,5 @@
import { type ReactNode } from "react";
import { Eye, GitFork, GitPullRequest, Minus, Plus, Star } from "lucide-react";
-import { Eye, GitFork, GitPullRequest, Minus, Plus, Star } from "lucide-react";
import {
Card,
CardContent,
@@ -15,7 +14,6 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useTranslation } from "./language-provider";
-import { useTranslation } from "./language-provider";
type Props = {
userResults: UserResult[];
@@ -37,7 +35,6 @@ export function TopList({ userResults }: Props) {
key={data.key}
>
-
-
{(data.score ?? 0).toFixed(2)}