From 994b691239cec11eefb24cc4fa262f633fa77cd3 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:27:16 +0300 Subject: [PATCH 1/8] fix: prevent theme flash on initial load --- app/layout.tsx | 10 ++++++---- components/theme-provider.tsx | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index cf1565a..5865918 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,6 @@ import "./globals.css"; import type { ReactNode } from "react"; import type { Metadata } from "next"; import { cookies, headers } from "next/headers"; -import Script from "next/script"; import { DEFAULT_LOCALE, LOCALE_COOKIE, @@ -112,10 +111,13 @@ export default async function RootLayout({ children }: { children: ReactNode }) return ( + + {children} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx index 75a06f3..cadf070 100644 --- a/components/theme-provider.tsx +++ b/components/theme-provider.tsx @@ -9,6 +9,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { attribute="class" defaultTheme="system" enableSystem + enableColorScheme storageKey="devimpact-theme" > {children} From 8b05b65a72d800af6a8b6890bb67d4728e5dc033 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:26:36 +0300 Subject: [PATCH 2/8] fix: Prevent home page skeleton from covering the whole page --- app/page.tsx | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 7c58084..c3a57fe 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,9 @@ import type { Metadata } from "next"; import { Suspense } from "react"; -import { DashboardSkeleton } from "@/components/skeletons"; +import { AppFooter } from "@/components/app-footer"; +import { AppHeader } from "@/components/app-header"; import { HomePageClient } from "@/components/home-page-client"; +import { Skeleton } from "@/components/ui/skeleton"; import { JsonLd } from "@/components/seo/json-ld"; import { toAbsoluteUrl } from "@/lib/seo"; @@ -63,11 +65,65 @@ const softwareSchema = { }, }; +function HomePageFallback() { + return ( +
+ + +
+
+
+ + + +
+ +
+
+
+ + +
+
+ + +
+
+ +
+ +
+ {Array.from({ length: 8 }).map((_, index) => ( + + ))} +
+
+ +
+ + + +
+
+
+ +
+ + + +
+
+ + +
+ ); +} + export default function HomePage() { return ( <> - }> + }> From c30fc4234606677108aaf892021a3248f080c060 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:27:16 +0300 Subject: [PATCH 3/8] fix: prevent theme flash on initial load --- app/layout.tsx | 10 ++++++---- components/theme-provider.tsx | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index cf1565a..5865918 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,6 @@ import "./globals.css"; import type { ReactNode } from "react"; import type { Metadata } from "next"; import { cookies, headers } from "next/headers"; -import Script from "next/script"; import { DEFAULT_LOCALE, LOCALE_COOKIE, @@ -112,10 +111,13 @@ export default async function RootLayout({ children }: { children: ReactNode }) return ( + + {children} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx index 75a06f3..cadf070 100644 --- a/components/theme-provider.tsx +++ b/components/theme-provider.tsx @@ -9,6 +9,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { attribute="class" defaultTheme="system" enableSystem + enableColorScheme storageKey="devimpact-theme" > {children} From 7667b37d22ae0eb0c2a45594ba6ff4fb19f55b99 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:26:36 +0300 Subject: [PATCH 4/8] fix: Prevent home page skeleton from covering the whole page --- app/page.tsx | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 7c58084..c3a57fe 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,9 @@ import type { Metadata } from "next"; import { Suspense } from "react"; -import { DashboardSkeleton } from "@/components/skeletons"; +import { AppFooter } from "@/components/app-footer"; +import { AppHeader } from "@/components/app-header"; import { HomePageClient } from "@/components/home-page-client"; +import { Skeleton } from "@/components/ui/skeleton"; import { JsonLd } from "@/components/seo/json-ld"; import { toAbsoluteUrl } from "@/lib/seo"; @@ -63,11 +65,65 @@ const softwareSchema = { }, }; +function HomePageFallback() { + return ( +
+ + +
+
+
+ + + +
+ +
+
+
+ + +
+
+ + +
+
+ +
+ +
+ {Array.from({ length: 8 }).map((_, index) => ( + + ))} +
+
+ +
+ + + +
+
+
+ +
+ + + +
+
+ + +
+ ); +} + export default function HomePage() { return ( <> - }> + }> From add9251baf1ff6a697a020cf9f489cc7744a71d2 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:43:10 +0300 Subject: [PATCH 5/8] chore: remove next env file (#197) --- .gitignore | 1 + next-env.d.ts | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 next-env.d.ts diff --git a/.gitignore b/.gitignore index 93bdc13..ef5d71d 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ out # Nuxt.js build / generate output .nuxt dist +next-env.d.ts # Gatsby files .cache/ diff --git a/next-env.d.ts b/next-env.d.ts deleted file mode 100644 index c4b7818..0000000 --- a/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -import "./.next/dev/types/routes.d.ts"; - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From 2296bd9ac7a2f3f8d8e7438aef3c801d7285a8f7 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:31:09 +0300 Subject: [PATCH 6/8] Style: update top right gradient color to match the app visual identity (#198) --- app/globals.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/globals.css b/app/globals.css index 65ff396..794cfbc 100644 --- a/app/globals.css +++ b/app/globals.css @@ -74,16 +74,16 @@ body::after { body::before { background-image: - radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.08), transparent 35%), - radial-gradient(circle at 80% 0%, rgba(168, 85, 247, 0.08), transparent 30%), + radial-gradient(circle at 20% 20%, hsl(var(--primary) / 0.08), transparent 35%), + radial-gradient(circle at 80% 0%, hsl(var(--accent) / 0.08), transparent 30%), linear-gradient(180deg, #f8fbff 0%, #f2f5f9 40%, #f9fafb 100%); opacity: 1; } body::after { background-image: - radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.08), transparent 35%), - radial-gradient(circle at 80% 0%, rgba(124, 58, 237, 0.12), transparent 30%), + radial-gradient(circle at 20% 20%, hsl(var(--primary) / 0.08), transparent 35%), + radial-gradient(circle at 80% 0%, hsl(var(--accent) / 0.12), transparent 30%), linear-gradient(180deg, #0f172a 0%, #0b1221 40%, #0a0f1c 100%); opacity: 0; } From 3e880cd6a0275a6677cc50c15fd2889d2315c935 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:24:05 +0300 Subject: [PATCH 7/8] feat: add duplicate fetch prevention and same user error handling in comparison form (#199) * feat: add duplicate fetch prevention and same user error handling in comparison form * style: enhance DashboardSkeleton layout and update Skeleton component styles --- components/compare-form.tsx | 18 +++++++- components/home-page-client.tsx | 14 ++++++ components/skeletons.tsx | 81 +++++++++++++++++++++++++++++---- components/ui/skeleton.tsx | 5 +- locales/ar.json | 1 + locales/en.json | 1 + 6 files changed, 107 insertions(+), 13 deletions(-) diff --git a/components/compare-form.tsx b/components/compare-form.tsx index 60ee6f9..4d854e6 100644 --- a/components/compare-form.tsx +++ b/components/compare-form.tsx @@ -47,6 +47,7 @@ type CompareFormProps = { swapUsers?: () => void; username1Error?: string | null; username2Error?: string | null; + disableDuplicateFetch?: boolean; }; export function CompareForm({ @@ -63,6 +64,7 @@ export function CompareForm({ reset, username1Error, username2Error, + disableDuplicateFetch, }: CompareFormProps) { const { t } = useTranslation(); const firstInputRef = useRef(null); @@ -71,7 +73,12 @@ export function CompareForm({ firstInputRef.current?.focus(); }, []); - const canSubmit = Boolean(username1.trim() && username2.trim() && !loading); + const normalized1 = username1.trim().toLowerCase(); + const normalized2 = username2.trim().toLowerCase(); + const sameUsername = Boolean(normalized1 && normalized2 && normalized1 === normalized2); + const canSubmit = Boolean( + username1.trim() && username2.trim() && !loading && !sameUsername && !disableDuplicateFetch, + ); const isEmpty = !username1.trim() && !username2.trim() && !hasData; const hasLanguageSelection = selectedLanguages.length > 0; @@ -238,7 +245,13 @@ export function CompareForm({ -
+
+ {sameUsername ? ( +

{t("error.sameUser")}

+ ) : null} + + +
+
diff --git a/components/home-page-client.tsx b/components/home-page-client.tsx index 59c5917..479ae0c 100644 --- a/components/home-page-client.tsx +++ b/components/home-page-client.tsx @@ -228,6 +228,11 @@ export function HomePageClient() { return inFlightPromiseRef.current; } + // If we've already fetched this exact comparison and have the data, skip. + if (lastFetchedKeyRef.current === fetchKey && data) { + return Promise.resolve(); + } + lastFetchedKeyRef.current = fetchKey; const requestPromise = (async () => { @@ -383,6 +388,14 @@ export function HomePageClient() { const isRefreshing = loading && Boolean(displayData); const isExiting = !loading && !data && Boolean(displayData); + const currentFetchKey = createFetchKey(username1.trim(), username2.trim(), { + selectedLanguages, + }); + + const disableDuplicateFetch = Boolean( + lastFetchedKeyRef.current === currentFetchKey && (data || inFlightFetchKeyRef.current === currentFetchKey), + ); + const handleUsername1Change = (value: string) => { setUsername1(value); if (usernameErrors.username1) { @@ -441,6 +454,7 @@ export function HomePageClient() { reset={reset} swapUsers={swapUsers} hasData={Boolean(data)} + disableDuplicateFetch={disableDuplicateFetch} username1Error={usernameErrors.username1} username2Error={usernameErrors.username2} /> diff --git a/components/skeletons.tsx b/components/skeletons.tsx index d6e0e7c..d33ad28 100644 --- a/components/skeletons.tsx +++ b/components/skeletons.tsx @@ -1,19 +1,80 @@ import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; export function DashboardSkeleton() { return ( -
- -
- - - - +
+ + +
+
+
+ +
+
+ + +
+
+
+ + +
+
+
+
+ +
+ + + + + + + +
+ + + + +
+
+
+ + + + + + + + + + +
- +
- - + + + + + + + + + + + + + + + + + + + + +
); diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx index 0118624..8fc953e 100644 --- a/components/ui/skeleton.tsx +++ b/components/ui/skeleton.tsx @@ -4,7 +4,10 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) { return (
) diff --git a/locales/ar.json b/locales/ar.json index 9431ed5..c1182eb 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -47,6 +47,7 @@ "error.resourceLimit": "تم الوصول إلى حدود موارد GitHub API لهذا الطلب. حاول مرة أخرى بعد قليل.", "error.missingUsername": "أدخل اسمين للمقارنة.", "error.userNotFound": "لم يتم العثور على مستخدم GitHub", + "error.sameUser": "مطلوب اسمان مختلفان لمستخدمي GitHub.", "explanations.contribution": "درجة المساهمات", "explanations.line.contribution.1": "تعتمد درجة المساهمات على المشكلات والنقاشات الخارجية فقط.", "explanations.line.contribution.2": "لا يتم احتساب الـ commits وطلبات السحب لتجنب العد المزدوج.", diff --git a/locales/en.json b/locales/en.json index 4a07471..af52421 100644 --- a/locales/en.json +++ b/locales/en.json @@ -47,6 +47,7 @@ "error.resourceLimit": "GitHub API resource limits were reached for this request. Please retry shortly.", "error.missingUsername": "Provide two usernames to compare.", "error.userNotFound": "GitHub user not found", + "error.sameUser": "Two different GitHub usernames are required.", "explanations.contribution": "Contribution Score", "explanations.line.contribution.1": "Contribution score is based on external issues and discussions only.", "explanations.line.contribution.2": "Commits and pull requests are excluded to avoid double-counting.", From 9b7a578c4265d31dc55605606c0bb3a66bf800c9 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:41:31 +0300 Subject: [PATCH 8/8] fix: Cannot access ref value during render Co-authored-by: Copilot --- components/home-page-client.tsx | 35 +++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/components/home-page-client.tsx b/components/home-page-client.tsx index 479ae0c..c5edf71 100644 --- a/components/home-page-client.tsx +++ b/components/home-page-client.tsx @@ -96,6 +96,7 @@ export function HomePageClient() { ); const [data, setData] = useState(null); const [displayData, setDisplayData] = useState(null); + const [disableDuplicateFetch, setDisableDuplicateFetch] = useState(false); const lastFetchedKeyRef = useRef(null); const inFlightFetchKeyRef = useRef(null); const inFlightPromiseRef = useRef | null>(null); @@ -235,6 +236,11 @@ export function HomePageClient() { lastFetchedKeyRef.current = fetchKey; + // update duplicate fetch state for current form values + setDisableDuplicateFetch( + Boolean(lastFetchedKeyRef.current === createFetchKey(username1.trim(), username2.trim(), { selectedLanguages }) && (data || inFlightFetchKeyRef.current === fetchKey)), + ); + const requestPromise = (async () => { if (options.updateUrl !== false) { const params = new URLSearchParams(); @@ -311,6 +317,11 @@ export function HomePageClient() { inFlightFetchKeyRef.current = fetchKey; inFlightPromiseRef.current = requestPromise; + // mark duplicate fetch disabled while request is in-flight + setDisableDuplicateFetch( + Boolean(lastFetchedKeyRef.current === createFetchKey(username1.trim(), username2.trim(), { selectedLanguages }) && (data || inFlightFetchKeyRef.current === fetchKey)), + ); + return requestPromise; }; @@ -324,6 +335,7 @@ export function HomePageClient() { lastFetchedKeyRef.current = null; setData(null); resetErrors(); + setDisableDuplicateFetch(false); return; } @@ -331,10 +343,7 @@ export function HomePageClient() { selectedLanguages: languages, }); - if ( - lastFetchedKeyRef.current === nextKey && - (data || inFlightFetchKeyRef.current === nextKey) - ) { + if (lastFetchedKeyRef.current === nextKey && data) { return; } @@ -388,13 +397,18 @@ export function HomePageClient() { const isRefreshing = loading && Boolean(displayData); const isExiting = !loading && !data && Boolean(displayData); - const currentFetchKey = createFetchKey(username1.trim(), username2.trim(), { - selectedLanguages, - }); - const disableDuplicateFetch = Boolean( - lastFetchedKeyRef.current === currentFetchKey && (data || inFlightFetchKeyRef.current === currentFetchKey), - ); + useEffect(() => { + const currentFetchKey = createFetchKey(username1.trim(), username2.trim(), { + selectedLanguages, + }); + + const lastKey = lastFetchedKeyRef.current; + const inFlightKey = inFlightFetchKeyRef.current; + + const disabled = Boolean(lastKey === currentFetchKey && (data || inFlightKey === currentFetchKey)); + setDisableDuplicateFetch(disabled); + }, [username1, username2, selectedLanguages, data, loading]); const handleUsername1Change = (value: string) => { setUsername1(value); @@ -416,6 +430,7 @@ export function HomePageClient() { resetErrors(); inFlightFetchKeyRef.current = null; inFlightPromiseRef.current = null; + setDisableDuplicateFetch(false); setUsername1(""); setUsername2(""); setSelectedLanguages([]);