Skip to content
Open
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
87 changes: 58 additions & 29 deletions src/app/leaderboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from "next/link";
import EmptyState from "@/components/EmptyState";

type LeaderboardTab = "streak" | "commits" | "prs";

Expand Down Expand Up @@ -111,6 +112,7 @@ export default async function LeaderboardPage({
})}
</div>

<section className="overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--card)] shadow-sm">
<section className="overflow-hidden rounded-2xl border border-[var(--border)] bg-[var(--card)] shadow-[var(--shadow-soft)]">
<div className="grid grid-cols-[72px_1fr_110px_110px] border-b border-[var(--border)] bg-[var(--control)] px-4 py-3 text-xs font-semibold uppercase tracking-wide text-[var(--muted-foreground)] md:grid-cols-[80px_1fr_140px_140px_120px]">
<div>Rank</div>
Expand All @@ -125,43 +127,70 @@ export default async function LeaderboardPage({
Leaderboard data is temporarily unavailable.
</div>
) : rows.length === 0 ? (
<div className="px-4 py-12 text-center text-sm text-[var(--muted-foreground)]">
No opted-in public profiles yet.
</div>
<EmptyState
icon="🏆"
title="No public profiles yet"
description="No public profiles yet — be the first to enable yours in Settings!"
actionLabel="Go to Settings"
actionHref="/dashboard/settings"
/>
) : (
rows.map((entry) => (
<div
key={`${activeTab}-${entry.username}`}
className="grid grid-cols-[72px_1fr_110px_110px] items-center border-b border-[var(--border)] px-4 py-4 last:border-b-0 md:grid-cols-[80px_1fr_140px_140px_120px]"
>
<div className="text-lg font-bold text-[var(--card-foreground)]">
#{entry.rank}
</div>
<div className="flex min-w-0 items-center gap-3">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={entry.avatarUrl}
alt=""
className="h-10 w-10 rounded-full border border-[var(--border)]"
/>
<div className="min-w-0">
<div className="truncate font-semibold text-[var(--card-foreground)]">
@{entry.username}
<>
<div className="grid grid-cols-[72px_1fr_110px_110px] border-b border-[var(--border)] bg-[var(--control)] px-4 py-3 text-xs font-semibold uppercase tracking-wide text-[var(--muted-foreground)] md:grid-cols-[80px_1fr_140px_140px_120px]">
<div>Rank</div>
<div>Contributor</div>
<div>{activeMeta.label}</div>
<div className="hidden md:block">Score</div>
<div>Profile</div>
</div>

{rows.map((entry) => (
<div
key={`${activeTab}-${entry.username}`}
className="grid grid-cols-[72px_1fr_110px_110px] items-center border-b border-[var(--border)] px-4 py-4 last:border-b-0 md:grid-cols-[80px_1fr_140px_140px_120px]"
>
<div className="text-lg font-bold text-[var(--card-foreground)]">
#{entry.rank}
</div>
<div className="flex min-w-0 items-center gap-3">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={entry.avatarUrl}
alt=""
className="h-10 w-10 rounded-full border border-[var(--border)]"
/>
<div className="min-w-0">
<div className="truncate font-semibold text-[var(--card-foreground)]">
@{entry.username}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
{entry.commits} commits · {entry.prs} PRs · {entry.streak}d
streak
</div>
</div>
</div>
<div>
<div className="text-lg font-semibold text-[var(--card-foreground)]">
{getMetricValue(entry, activeTab)}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
{entry.commits} commits · {entry.prs} PRs · {entry.streak}d
streak
{activeMeta.metric}
</div>
</div>
</div>
<div>
<div className="text-lg font-semibold text-[var(--card-foreground)]">
{getMetricValue(entry, activeTab)}
<div className="hidden text-sm font-medium text-[var(--card-foreground)] md:block">
{entry.score}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
{activeMeta.metric}
<div>
<Link
href={entry.profileUrl}
className="inline-flex rounded-lg border border-[var(--border)] px-3 py-2 text-sm font-medium text-[var(--card-foreground)] hover:bg-[var(--control)]"
>
View
</Link>
</div>
</div>
))}
</>
<div className="hidden text-sm font-medium text-[var(--card-foreground)] md:block">
{entry.score}
</div>
Expand Down
39 changes: 39 additions & 0 deletions src/components/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Link from "next/link";

interface EmptyStateProps {
icon?: string;
title: string;
description: string;
actionLabel?: string;
actionHref?: string;
}

export default function EmptyState({
icon = "🏆",
title,
description,
actionLabel,
actionHref,
}: EmptyStateProps) {
return (
<div className="flex flex-col items-center justify-center py-20 px-4 text-center">
<div className="text-6xl mb-6 select-none" role="img" aria-label={title}>
{icon}
</div>
<h2 className="text-xl font-semibold text-[var(--foreground)] mb-3">
{title}
</h2>
<p className="text-[var(--muted-foreground)] max-w-sm mb-8 leading-relaxed">
{description}
</p>
{actionLabel && actionHref && (
<Link
href={actionHref}
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-[var(--foreground)] text-[var(--background)] font-medium text-sm hover:opacity-80 transition-opacity"
>
{actionLabel} →
</Link>
)}
</div>
);
}
Loading