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
17 changes: 17 additions & 0 deletions src/features/comparison/components/home-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { AppFooter } from "@/components/layout/app-footer";
import { useTranslation } from "@/components/providers/language-provider";
import { cn } from "@/utils/cn";
import { useComparisonController } from "../hooks";
import { type ComparisonPreset } from "../config/comparison-presets";
import { PresetPills } from "./preset-pills";

export function HomePageClient() {
const { t } = useTranslation();
Expand All @@ -34,6 +36,13 @@ export function HomePageClient() {
usernameErrors,
} = useComparisonController();

const handlePresetClick = (preset: ComparisonPreset) => {
handleUsername1Change(preset.username1);
handleUsername2Change(preset.username2);

handleCompare(preset.username1, preset.username2, { selectedLanguages });
};

const skeleton = useMemo(() => <DashboardSkeleton />, []);

return (
Expand All @@ -57,6 +66,14 @@ export function HomePageClient() {
username1Error={usernameErrors.username1}
username2Error={usernameErrors.username2}
/>
{!displayData && (
<PresetPills
onSelect={handlePresetClick}
loading={loading}
username1={username1}
username2={username2}
/>
)}

<div className="relative min-h-[28rem]" aria-live="polite">
{displayData ? (
Expand Down
64 changes: 64 additions & 0 deletions src/features/comparison/components/preset-pills.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import { useId } from "react";
import { useTranslation } from "@/components/providers/language-provider";
import { cn } from "@/utils/cn";
import { COMPARISON_PRESETS } from "../config/comparison-presets";

type Preset = (typeof COMPARISON_PRESETS)[number];

interface PresetPillsProps {
onSelect: (preset: Preset) => void;
loading: boolean;
username1: string;
username2: string;
}

export function PresetPills({ onSelect, loading, username1, username2 }: PresetPillsProps) {
const { t } = useTranslation();
const headingId = useId();

return (
<section
aria-labelledby={headingId}
className="flex animate-fadeIn flex-col items-center gap-3"
>
<h2 id={headingId} className="text-sm text-muted-foreground">
{t("presets.title")}
</h2>

<ul className="flex flex-wrap justify-center gap-2" aria-busy={loading}>
{COMPARISON_PRESETS.map((preset) => {
const isActive =
loading && username1 === preset.username1 && username2 === preset.username2;

return (
<li key={preset.labelKey}>
{/* Native <button>: Tab to focus, Enter/Space to activate */}
<button
type="button"
disabled={loading}
onClick={() => onSelect(preset)}
className={cn(
"inline-flex items-center gap-2 rounded-full border border-border bg-card px-4 py-2 text-sm text-foreground",
"transition-all duration-200 hover:bg-accent",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none",
isActive && "opacity-100 ring-2 ring-ring",
)}
>
{isActive && (
<span
aria-hidden="true"
className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-current border-t-transparent"
/>
)}
{t(preset.labelKey)}
</button>
</li>
);
})}
</ul>
</section>
);
}
19 changes: 19 additions & 0 deletions src/features/comparison/config/comparison-presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const COMPARISON_PRESETS = [
{
username1: "yyx990803",
username2: "antfu",
labelKey: "presets.evanVsAnthony",
},
{
username1: "rauchg",
username2: "leerob",
labelKey: "presets.guillermoVsLee",
},
{
username1: "kentcdodds",
username2: "t3dotgg",
labelKey: "presets.kentVsTheo",
},
] as const;

export type ComparisonPreset = (typeof COMPARISON_PRESETS)[number];
4 changes: 4 additions & 0 deletions src/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@
"profile.signalShare": "{pct}% من الإشارة النهائية",
"profile.title": "تأثير وإحصائيات المطور",
"profile.topWork": "أبرز الأعمال والمساهمات",
"presets.title": "لا تعرف من تقارن؟ جرّب أحد هذه الأمثلة:",
"presets.evanVsAnthony": "إيفان يو ضد أنتوني فو",
"presets.guillermoVsLee": "غييرمو راوتش ضد لي روبنسون",
"presets.kentVsTheo": "كينت سي. دودز ضد ثيو براون",
"profile.badgeDescription": "احصل على شارة لملف README الخاص بك على GitHub.",
"profile.badgeStyle": "نمط الشارة",
"profile.markdownLabel": "ماركداون",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@
"profile.location": "Location",
"profile.metaDescription": "Explore {name}'s (@{username}) open-source developer impact score, top repositories, merged pull requests, and community contributions on DevImpact.",
"profile.metaTitle": "{name} (@{username}) - GitHub Impact & Developer Stats",
"presets.title": "Not sure who to compare? Try one of these:",
"presets.evanVsAnthony": "Evan You vs Anthony Fu",
"presets.guillermoVsLee": "Guillermo Rauch vs Lee Robinson",
"presets.kentVsTheo": "Kent C. Dodds vs Theo Browne",
"profile.methodologyCta": "Learn how this developer score was calculated",
"profile.notFound.description": "We couldn't find GitHub data for @{username}. Please check the username and try again.",
"profile.notFound.title": "Developer Not Found",
Expand Down
Loading