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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ out
# Nuxt.js build / generate output
.nuxt
dist
next-env.d.ts

# Gatsby files
.cache/
Expand Down
8 changes: 4 additions & 4 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
10 changes: 6 additions & 4 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -112,10 +111,13 @@ export default async function RootLayout({ children }: { children: ReactNode })

return (
<html lang={initialLocale} dir={dir} suppressHydrationWarning>
<head>
<script
id="theme-init"
dangerouslySetInnerHTML={{ __html: themeInitScript }}
/>
</head>
<body>
<Script id="theme-init" strategy="beforeInteractive">
{themeInitScript}
</Script>
<Providers initialLocale={initialLocale}>{children}</Providers>
</body>
</html>
Expand Down
60 changes: 58 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -63,11 +65,65 @@ const softwareSchema = {
},
};

function HomePageFallback() {
return (
<main className="flex min-h-screen flex-col">
<AppHeader />

<div className="w-full flex-1 max-w-6xl mx-auto px-4 py-10 space-y-6">
<div className="rounded-2xl border border-border bg-card/90 p-6 shadow-lg backdrop-blur">
<div className="space-y-4 pb-4">
<Skeleton className="h-3 w-32 rounded-full" />
<Skeleton className="h-8 w-64 rounded-lg" />
<Skeleton className="h-4 w-full max-w-md rounded-lg" />
</div>

<div className="space-y-5">
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-1.5">
<Skeleton className="h-3 w-24 rounded-full" />
<Skeleton className="h-11 rounded-lg" />
</div>
<div className="space-y-1.5">
<Skeleton className="h-3 w-24 rounded-full" />
<Skeleton className="h-11 rounded-lg" />
</div>
</div>

<div className="space-y-2">
<Skeleton className="h-4 w-36 rounded-full" />
<div className="flex flex-wrap gap-2">
{Array.from({ length: 8 }).map((_, index) => (
<Skeleton key={index} className="h-7 w-20 rounded-full" />
))}
</div>
</div>

<div className="flex justify-end gap-3">
<Skeleton className="h-10 w-40 rounded-lg" />
<Skeleton className="h-10 w-10 rounded-lg" />
<Skeleton className="h-10 w-10 rounded-lg" />
</div>
</div>
</div>

<div className="flex flex-col items-center justify-center gap-4 py-20 text-center">
<Skeleton className="h-24 w-24 rounded-full sm:h-32 sm:w-32" />
<Skeleton className="h-6 w-64 rounded-lg" />
<Skeleton className="h-4 w-72 rounded-lg" />
</div>
</div>

<AppFooter />
</main>
);
}

export default function HomePage() {
return (
<>
<JsonLd data={[websiteSchema, softwareSchema]} />
<Suspense fallback={<DashboardSkeleton />}>
<Suspense fallback={<HomePageFallback />}>
<HomePageClient />
</Suspense>
</>
Expand Down
18 changes: 16 additions & 2 deletions components/compare-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type CompareFormProps = {
swapUsers?: () => void;
username1Error?: string | null;
username2Error?: string | null;
disableDuplicateFetch?: boolean;
};

export function CompareForm({
Expand All @@ -63,6 +64,7 @@ export function CompareForm({
reset,
username1Error,
username2Error,
disableDuplicateFetch,
}: CompareFormProps) {
const { t } = useTranslation();
const firstInputRef = useRef<HTMLInputElement>(null);
Expand All @@ -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;

Expand Down Expand Up @@ -238,7 +245,13 @@ export function CompareForm({
</div>
</div>

<div className="flex flex-wrap justify-end gap-3">
<div className="flex flex-col gap-2">
{sameUsername ? (
<p className="text-xs font-medium text-destructive">{t("error.sameUser")}</p>
) : null}


<div className="flex flex-wrap justify-end gap-3">
<Button
type="submit"
disabled={!canSubmit}
Expand All @@ -265,6 +278,7 @@ export function CompareForm({
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
</form>
Expand Down
14 changes: 14 additions & 0 deletions components/home-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -441,6 +454,7 @@ export function HomePageClient() {
reset={reset}
swapUsers={swapUsers}
hasData={Boolean(data)}
disableDuplicateFetch={disableDuplicateFetch}
username1Error={usernameErrors.username1}
username2Error={usernameErrors.username2}
/>
Expand Down
81 changes: 71 additions & 10 deletions components/skeletons.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,80 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";

export function DashboardSkeleton() {
return (
<div className="space-y-5 animate-pulse">
<Skeleton className="h-24 rounded-xl" />
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-4">
<Skeleton className="h-24 rounded-lg" />
<Skeleton className="h-24 rounded-lg" />
<Skeleton className="h-24 rounded-lg" />
<Skeleton className="h-24 rounded-lg" />
<div className="space-y-6 animate-fadeIn">
<Card className="border-2 border-primary/10 bg-gradient-to-r from-primary/5 via-primary/3 to-transparent">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="rounded-full p-3">
<Skeleton className="h-8 w-8 rounded-full" />
</div>
<div>
<Skeleton className="h-6 w-48 rounded-md" />
<Skeleton className="mt-2 h-5 w-36 rounded-md" />
</div>
</div>
<div className="text-right">
<Skeleton className="h-6 w-24 rounded-md" />
<Skeleton className="mt-2 h-8 w-32 rounded-md" />
</div>
</div>
</CardContent>
</Card>

<div className="grid gap-3 sm:grid-cols-2">
<Card>
<CardHeader className="pb-3">
<CardTitle>
<Skeleton className="h-5 w-40" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid gap-3 sm:grid-cols-2">
<Skeleton className="h-20 rounded-lg" />
<Skeleton className="h-20 rounded-lg" />
<Skeleton className="h-20 rounded-lg" />
<Skeleton className="h-20 rounded-lg" />
</div>
</CardContent>
</Card>

<Card>
<CardHeader className="pb-3">
<CardTitle>
<Skeleton className="h-5 w-40" />
</CardTitle>
</CardHeader>
<CardContent>
<Skeleton className="h-72 rounded-xl" />
</CardContent>
</Card>
</div>
<Skeleton className="h-72 rounded-xl" />

<div className="grid gap-4 md:grid-cols-2">
<Skeleton className="h-64 rounded-xl" />
<Skeleton className="h-64 rounded-xl" />
<Card>
<CardHeader className="pb-3">
<CardTitle>
<Skeleton className="h-5 w-40" />
</CardTitle>
</CardHeader>
<CardContent>
<Skeleton className="h-64 rounded-xl" />
</CardContent>
</Card>

<Card>
<CardHeader className="pb-3">
<CardTitle>
<Skeleton className="h-5 w-40" />
</CardTitle>
</CardHeader>
<CardContent>
<Skeleton className="h-64 rounded-xl" />
</CardContent>
</Card>
</div>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions components/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
attribute="class"
defaultTheme="system"
enableSystem
enableColorScheme
storageKey="devimpact-theme"
>
{children}
Expand Down
5 changes: 4 additions & 1 deletion components/ui/skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-muted", className)}
className={cn(
"animate-pulse rounded-md bg-[hsl(var(--muted-foreground)/0.12)] dark:bg-[hsl(var(--muted)/1)]",
className,
)}
{...props}
/>
)
Expand Down
1 change: 1 addition & 0 deletions locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 وطلبات السحب لتجنب العد المزدوج.",
Expand Down
1 change: 1 addition & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
6 changes: 0 additions & 6 deletions next-env.d.ts

This file was deleted.

Loading