Skip to content
Closed
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
91 changes: 67 additions & 24 deletions src/app/dashboard/contributor/ContributorDashboardClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import { useEffect, useState, useCallback, useRef } from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { DollarSign, GitMerge, TrendingUp, ListChecks, GitPullRequest } from "lucide-react";
import {
DollarSign,
GitMerge,
TrendingUp,
ListChecks,
GitPullRequest,
} from "lucide-react";
import { DashboardShell } from "@/components/dashboard/DashboardShell";
import { ActivityList } from "@/components/dashboard/ActivityList";
import { StatCard, type StatCardStatus } from "@/components/ui/StatCard";
Expand All @@ -19,7 +25,7 @@ import { apiRequest, fetchBounties } from "@/lib/api";
import {
mockReputationProfiles,
mockBounties,
recentActivity,
getRecentActivity,
contributorEarningsHistory,
contributorSparkline,
} from "@/lib/mock-data";
Expand Down Expand Up @@ -72,7 +78,7 @@ export default function ContributorDashboardClient() {
const [statsSource, setStatsSource] = useState<DataSource>("loading");
const [bountiesSource, setBountiesSource] = useState<DataSource>("loading");
const [tab, setTabState] = useState<"active" | "completed">(
(searchParams.get("tab") as "active" | "completed") || "active"
(searchParams.get("tab") as "active" | "completed") || "active",
);
// Explicit fetch status: starts "loading" so cards shimmer rather than
// flashing zeroes while the auth check + API call are in flight.
Expand Down Expand Up @@ -118,12 +124,13 @@ export default function ContributorDashboardClient() {
const reputationResult = apiRequest<ReputationSnapshot | null>(
`/reputation/${user.id}`,
).then(
(snapshot) => ({
handle: user.username,
lifetimeEarnings: snapshot ? Number(snapshot.totalEarnings) : 0,
mergedPRs: snapshot ? snapshot.mergedPrCount : 0,
completionRate: snapshot ? Number(snapshot.completionRate) / 100 : 0,
} as DashboardStats | null),
(snapshot) =>
({
handle: user.username,
lifetimeEarnings: snapshot ? Number(snapshot.totalEarnings) : 0,
mergedPRs: snapshot ? snapshot.mergedPrCount : 0,
completionRate: snapshot ? Number(snapshot.completionRate) / 100 : 0,
}) as DashboardStats | null,
() => null,
);

Expand Down Expand Up @@ -159,21 +166,31 @@ export default function ContributorDashboardClient() {
labelled live while half of it is the bundled fixture. While either
source is still in flight, keep the badge on its initial "Demo data"
rather than guessing. */
const sourcesSettled = statsSource !== "loading" && bountiesSource !== "loading";
const sourcesSettled =
statsSource !== "loading" && bountiesSource !== "loading";
const allLive = statsSource === "live" && bountiesSource === "live";
const allMock = statsSource === "mock" && bountiesSource === "mock";
const badgeLabel = !sourcesSettled ? "Demo data" : allLive ? "Live data" : allMock ? "Demo data" : "Mixed data";
const badgeLabel = !sourcesSettled
? "Demo data"
: allLive
? "Live data"
: allMock
? "Demo data"
: "Mixed data";
const isLive = badgeLabel === "Live data";
/* Only called out sectionally in the partial-failure case: on a
signed-out demo the page badge already says "Demo data", so repeating
the chip on every list would be noise rather than information. */
const showBountySampleChip = sourcesSettled && bountiesSource === "mock" && statsSource !== "mock";
const showBountySampleChip =
sourcesSettled && bountiesSource === "mock" && statsSource !== "mock";

// Derive display handle: show username if live, else demo handle.
const handle = stats?.handle ?? (user?.username ?? "you");
const handle = stats?.handle ?? user?.username ?? "you";

const mine = bounties.filter((b) => b.claimedBy === handle);
const activeClaims = mine.filter((b) => !["paid", "refunded", "expired"].includes(b.status));
const activeClaims = mine.filter(
(b) => !["paid", "refunded", "expired"].includes(b.status),
);
const completedClaims = mine.filter((b) => b.status === "paid");
const available = bounties.filter((b) => b.status === "open");
const shownClaims = tab === "active" ? activeClaims : completedClaims;
Expand All @@ -192,7 +209,11 @@ export default function ContributorDashboardClient() {
<DashboardShell
role="contributor"
title={`Welcome back, @${handle}`}
subtitle={user ? undefined : "Sign in with GitHub to see your own earnings and claims."}
subtitle={
user
? undefined
: "Sign in with GitHub to see your own earnings and claims."
}
badge={
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset ${
Expand Down Expand Up @@ -222,7 +243,9 @@ export default function ContributorDashboardClient() {
status={fetchStatus}
icon={DollarSign}
trend={fetchStatus === "loaded" ? 12 : undefined}
sparkline={fetchStatus === "loaded" ? contributorEarningsHistory : undefined}
sparkline={
fetchStatus === "loaded" ? contributorEarningsHistory : undefined
}
zeroLabel="No earnings yet"
/>
<StatCard
Expand All @@ -232,7 +255,9 @@ export default function ContributorDashboardClient() {
status={fetchStatus}
icon={GitMerge}
trend={fetchStatus === "loaded" ? 8 : undefined}
sparkline={fetchStatus === "loaded" ? contributorSparkline : undefined}
sparkline={
fetchStatus === "loaded" ? contributorSparkline : undefined
}
zeroLabel="No merged PRs yet"
/>
<StatCard
Expand Down Expand Up @@ -260,7 +285,9 @@ export default function ContributorDashboardClient() {
<div className="mt-6 grid gap-6 lg:grid-cols-[1.4fr_1fr]">
<Card>
<div className="flex items-center gap-2">
<h2 className="font-medium text-slate-900 dark:text-white">Earnings, last 8 weeks</h2>
<h2 className="font-medium text-slate-900 dark:text-white">
Earnings, last 8 weeks
</h2>
{isLive && <SampleDataChip />}
</div>
<div className="mt-6">
Expand All @@ -273,18 +300,22 @@ export default function ContributorDashboardClient() {
</Card>
<div>
<div className="flex items-center gap-2">
<h2 className="font-medium text-slate-900 dark:text-white">Recent activity</h2>
<h2 className="font-medium text-slate-900 dark:text-white">
Recent activity
</h2>
{isLive && <SampleDataChip />}
</div>
<div className="mt-4">
<ActivityList events={recentActivity.slice(0, 4)} />
<ActivityList events={getRecentActivity().slice(0, 4)} />
</div>
</div>
</div>

<div className="mt-10 flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">Your claims</h2>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
Your claims
</h2>
{/* Marked in place when this list is the mock fallback but the
stats above it are real — the page badge alone can't say which
half of a "Mixed data" page you're looking at. */}
Expand All @@ -293,7 +324,11 @@ export default function ContributorDashboardClient() {
<Tabs
tabs={[
{ key: "active", label: "Active", count: activeClaims.length },
{ key: "completed", label: "Completed", count: completedClaims.length },
{
key: "completed",
label: "Completed",
count: completedClaims.length,
},
]}
active={tab}
onChange={setTab}
Expand All @@ -319,7 +354,11 @@ export default function ContributorDashboardClient() {
tone="neutral"
className="md:col-span-2"
headingLevel="h3"
title={t(tab === "active" ? "empty.claims.active.title" : "empty.claims.completed.title")}
title={t(
tab === "active"
? "empty.claims.active.title"
: "empty.claims.completed.title",
)}
description={t(
tab === "active"
? "empty.claims.active.description"
Expand All @@ -332,7 +371,11 @@ export default function ContributorDashboardClient() {
href="#open-bounties"
className="text-sm font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400"
>
{t(tab === "active" ? "empty.claims.active.cta" : "empty.claims.completed.cta")}
{t(
tab === "active"
? "empty.claims.active.cta"
: "empty.claims.completed.cta",
)}
</a>
}
/>
Expand Down
20 changes: 15 additions & 5 deletions src/app/dashboard/maintainer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export default async function MaintainerDashboardPage() {
: [];

const needsReview = bounties.filter((b) => b.status === "in_review");
const open = bounties.filter((b) => b.status === "open" || b.status === "funded");
const open = bounties.filter(
(b) => b.status === "open" || b.status === "funded",
);
const repoCount = new Set(bounties.map((b) => `${b.org}/${b.repo}`)).size;
// Summed in integer minor units to avoid float drift (#353). 7 decimals
// covers both USDC (2) and XLM (7) rewards without truncation.
Expand Down Expand Up @@ -94,7 +96,9 @@ export default async function MaintainerDashboardPage() {
format="currency"
status={statStatus}
icon={Coins}
sparkline={statStatus === "loaded" ? bountiesCompletedSparkline : undefined}
sparkline={
statStatus === "loaded" ? bountiesCompletedSparkline : undefined
}
zeroLabel="Nothing in escrow yet"
/>
<StatCard
Expand Down Expand Up @@ -144,12 +148,18 @@ export default async function MaintainerDashboardPage() {
className="flex items-center justify-between gap-4"
>
<div className="flex items-center gap-3">
<Avatar seed={`${bounty.org}/${bounty.repo}`} size={36} className="rounded-xl" />
<Avatar
seed={`${bounty.org}/${bounty.repo}`}
size={36}
className="rounded-xl"
/>
<div>
<p className="text-xs text-slate-500 dark:text-slate-400">
{bounty.org}/{bounty.repo} #{bounty.issueNumber}
</p>
<p className="font-medium text-slate-900 dark:text-white">{bounty.title}</p>
<p className="font-medium text-slate-900 dark:text-white">
{bounty.title}
</p>
{bounty.claimedBy && (
<p className="mt-0.5 text-xs text-slate-400 dark:text-slate-500">
by {bounty.claimedBy}
Expand Down Expand Up @@ -197,7 +207,7 @@ export default async function MaintainerDashboardPage() {
{source === "mock" && <SampleDataChip />}
</div>
<div className="mt-4">
<ActivityList events={recentActivity.slice(0, 5)} />
<ActivityList events={getRecentActivity().slice(0, 5)} />
</div>
</div>
</div>
Expand Down
46 changes: 33 additions & 13 deletions src/app/dashboard/sponsor/SponsorDashboardClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

import { useEffect, useState, useRef } from "react";
import Link from "next/link";
import { Wallet, Receipt, Lock, FolderGit2, Inbox, CheckCircle2 } from "lucide-react";
import {
Wallet,
Receipt,
Lock,
FolderGit2,
Inbox,
CheckCircle2,
} from "lucide-react";
import { DashboardShell } from "@/components/dashboard/DashboardShell";
import { ActivityList } from "@/components/dashboard/ActivityList";
import { StatCard, type StatCardStatus } from "@/components/ui/StatCard";
Expand All @@ -19,7 +26,7 @@ import { adaptBounty, type RawBounty, type RawMilestone } from "@/lib/adapters";
import {
mockSponsorSummary,
mockBounties,
recentActivity,
getRecentActivity,
sponsorSpendHistory,
sponsorSpendByRepo,
budgetSparkline,
Expand Down Expand Up @@ -107,7 +114,9 @@ export default function SponsorDashboardClient() {
.then((raw) => {
if (generation !== fetchGeneration.current) return;
const activeBounties = raw.activeBounties.map(adaptBounty);
const repoCount = new Set(activeBounties.map((b) => `${b.org}/${b.repo}`)).size;
const repoCount = new Set(
activeBounties.map((b) => `${b.org}/${b.repo}`),
).size;
setData({
activeBounties,
totalSpent: raw.totalSpent,
Expand Down Expand Up @@ -151,7 +160,11 @@ export default function SponsorDashboardClient() {
return (
<DashboardShell
role="sponsor"
title={user ? `${user.displayName ?? user.username}'s sponsorships` : mockSponsorSummary.name}
title={
user
? `${user.displayName ?? user.username}'s sponsorships`
: mockSponsorSummary.name
}
subtitle={
isLive
? "Track spending, active bounties, and escrowed budget across the repositories you fund."
Expand Down Expand Up @@ -219,7 +232,9 @@ export default function SponsorDashboardClient() {
<div className="mt-6 grid gap-6 lg:grid-cols-[1.4fr_1fr]">
<Card>
<div className="flex items-center gap-2">
<h2 className="font-medium text-slate-900 dark:text-white">Spend, last 8 weeks</h2>
<h2 className="font-medium text-slate-900 dark:text-white">
Spend, last 8 weeks
</h2>
{isLive && <SampleDataChip />}
</div>
<div className="mt-6">
Expand All @@ -232,14 +247,18 @@ export default function SponsorDashboardClient() {
</Card>
<Card>
<div className="flex items-center gap-2">
<h2 className="font-medium text-slate-900 dark:text-white">Spend by repository</h2>
<h2 className="font-medium text-slate-900 dark:text-white">
Spend by repository
</h2>
{isLive && <SampleDataChip />}
</div>
<div className="mt-5 space-y-3">
{sponsorSpendByRepo.map((r) => (
<div key={r.repo}>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 dark:text-slate-300">{r.repo}</span>
<span className="text-slate-600 dark:text-slate-300">
{r.repo}
</span>
<span className="font-medium text-slate-900 dark:text-white">
{formatCurrency(r.amount)}
</span>
Expand All @@ -256,21 +275,23 @@ export default function SponsorDashboardClient() {
</Card>
</div>

<h2 className="mt-10 text-xl font-semibold text-slate-900 dark:text-white">Active bounties</h2>
<h2 className="mt-10 text-xl font-semibold text-slate-900 dark:text-white">
Active bounties
</h2>
<div className="mt-6 grid gap-4 md:grid-cols-2">
{(data?.activeBounties ?? []).map((bounty) => (
<BountyCard key={bounty.id} bounty={bounty} />
))}
</div>
{data?.activeBounties?.length === 0 && (
{data?.activeBounties?.length === 0 &&
/* Two genuinely different situations collapse into the same
"0 active bounties" count, and the previous single copy treated
both as "fund something", which is the wrong advice for a sponsor
whose entire portfolio has already been paid out:
- never funded anything → needs a "fund your first" CTA
- everything has settled → this is the success state, and
suggesting they fund more is noise */
sponsorHasHistory ? (
(sponsorHasHistory ? (
<EmptyState
icon={CheckCircle2}
tone="success"
Expand All @@ -293,8 +314,7 @@ export default function SponsorDashboardClient() {
</Link>
}
/>
)
)}
))}

<div className="mt-10 flex items-center gap-2">
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">Recent activity</h2>
Expand Down Expand Up @@ -336,7 +356,7 @@ export default function SponsorDashboardClient() {
))}
</div>
) : (
<ActivityList events={recentActivity.slice(0, 5)} />
<ActivityList events={getRecentActivity().slice(0, 5)} />
)}
</div>

Expand Down
Loading