From 59e58029b6b6c426cd9e0c74c01eb7b05ef56855 Mon Sep 17 00:00:00 2001 From: Yggdrasil Date: Wed, 14 Oct 2026 00:48:12 -0400 Subject: [PATCH] fix(mock): fictional sponsor names + render-time timestamps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - recentActivity used real org names (Stellar Development Foundation, Stellar Community Fund) as 'funded' — a misleading endorsement when the backend isn't wired. Replaced with clearly-fictional sponsors (#712). - timestamps anchored at module-load stayed frozen on a long-lived server — now computed per-call via getRecentActivity() so the live feed's relative-time actually advances (#707). Resolves #712, #707 --- .../ContributorDashboardClient.tsx | 91 +++++++--- src/app/dashboard/maintainer/page.tsx | 20 ++- .../sponsor/SponsorDashboardClient.tsx | 46 +++-- src/app/page.tsx | 71 +++++--- src/lib/mock-data.ts | 157 ++++++++++-------- 5 files changed, 254 insertions(+), 131 deletions(-) diff --git a/src/app/dashboard/contributor/ContributorDashboardClient.tsx b/src/app/dashboard/contributor/ContributorDashboardClient.tsx index 5343c3a..e06e867 100644 --- a/src/app/dashboard/contributor/ContributorDashboardClient.tsx +++ b/src/app/dashboard/contributor/ContributorDashboardClient.tsx @@ -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"; @@ -19,7 +25,7 @@ import { apiRequest, fetchBounties } from "@/lib/api"; import { mockReputationProfiles, mockBounties, - recentActivity, + getRecentActivity, contributorEarningsHistory, contributorSparkline, } from "@/lib/mock-data"; @@ -72,7 +78,7 @@ export default function ContributorDashboardClient() { const [statsSource, setStatsSource] = useState("loading"); const [bountiesSource, setBountiesSource] = useState("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. @@ -118,12 +124,13 @@ export default function ContributorDashboardClient() { const reputationResult = apiRequest( `/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, ); @@ -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; @@ -192,7 +209,11 @@ export default function ContributorDashboardClient() {
-

Earnings, last 8 weeks

+

+ Earnings, last 8 weeks +

{isLive && }
@@ -273,18 +300,22 @@ export default function ContributorDashboardClient() {
-

Recent activity

+

+ Recent activity +

{isLive && }
- +
-

Your claims

+

+ Your claims +

{/* 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. */} @@ -293,7 +324,11 @@ export default function ContributorDashboardClient() { - {t(tab === "active" ? "empty.claims.active.cta" : "empty.claims.completed.cta")} + {t( + tab === "active" + ? "empty.claims.active.cta" + : "empty.claims.completed.cta", + )} } /> diff --git a/src/app/dashboard/maintainer/page.tsx b/src/app/dashboard/maintainer/page.tsx index 2602f34..15f7d68 100644 --- a/src/app/dashboard/maintainer/page.tsx +++ b/src/app/dashboard/maintainer/page.tsx @@ -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. @@ -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" />
- +

{bounty.org}/{bounty.repo} #{bounty.issueNumber}

-

{bounty.title}

+

+ {bounty.title} +

{bounty.claimedBy && (

by {bounty.claimedBy} @@ -197,7 +207,7 @@ export default async function MaintainerDashboardPage() { {source === "mock" && }

- +
diff --git a/src/app/dashboard/sponsor/SponsorDashboardClient.tsx b/src/app/dashboard/sponsor/SponsorDashboardClient.tsx index 3f97e1c..b94e73d 100644 --- a/src/app/dashboard/sponsor/SponsorDashboardClient.tsx +++ b/src/app/dashboard/sponsor/SponsorDashboardClient.tsx @@ -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"; @@ -19,7 +26,7 @@ import { adaptBounty, type RawBounty, type RawMilestone } from "@/lib/adapters"; import { mockSponsorSummary, mockBounties, - recentActivity, + getRecentActivity, sponsorSpendHistory, sponsorSpendByRepo, budgetSparkline, @@ -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, @@ -151,7 +160,11 @@ export default function SponsorDashboardClient() { return (
-

Spend, last 8 weeks

+

+ Spend, last 8 weeks +

{isLive && }
@@ -232,14 +247,18 @@ export default function SponsorDashboardClient() {
-

Spend by repository

+

+ Spend by repository +

{isLive && }
{sponsorSpendByRepo.map((r) => (
- {r.repo} + + {r.repo} + {formatCurrency(r.amount)} @@ -256,13 +275,15 @@ export default function SponsorDashboardClient() {
-

Active bounties

+

+ Active bounties +

{(data?.activeBounties ?? []).map((bounty) => ( ))}
- {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 @@ -270,7 +291,7 @@ export default function SponsorDashboardClient() { - 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 ? ( } /> - ) - )} + ))}

Recent activity

@@ -336,7 +356,7 @@ export default function SponsorDashboardClient() { ))}
) : ( - + )}
diff --git a/src/app/page.tsx b/src/app/page.tsx index 184898f..ff77616 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -23,7 +23,7 @@ import { platformStats as mockPlatformStats, trustedOrgs, topContributors, - recentActivity, + getRecentActivity, faqs, } from "@/lib/mock-data"; import { formatCurrency, formatInteger } from "@/lib/utils"; @@ -120,7 +120,9 @@ export default async function HomePage() {
- {STELLAR_NETWORK === "PUBLIC" ? "Built for Stellar Mainnet" : "Built for Stellar (Testnet)"} + {STELLAR_NETWORK === "PUBLIC" + ? "Built for Stellar Mainnet" + : "Built for Stellar (Testnet)"}

Merge code. @@ -129,14 +131,14 @@ export default async function HomePage() {

MergeFi is the financial infrastructure for open source. Sponsors - fund issues, contributors get paid the moment their pull request - is merged, and Soroban smart contracts on Stellar handle the - escrow. + fund issues, contributors get paid the moment their pull request is + merged, and Soroban smart contracts on Stellar handle the escrow.

@@ -163,21 +165,33 @@ export default async function HomePage() {

Trusted by open-source teams on Stellar

-
    +
      {trustedOrgs.map((org) => ( -
    • +
    • {org}
    • ))}
    -
      +
        {integrations.map(({ icon: Icon, label }) => (
      • -
      • ))} @@ -222,15 +236,16 @@ export default async function HomePage() {
{features.map(({ icon: Icon, title, description }) => ( - + -

{title}

-

{description}

+

+ {title} +

+

+ {description} +

))}
@@ -247,7 +262,9 @@ export default async function HomePage() { {i + 1} -

{step}

+

+ {step} +

))} @@ -299,7 +316,7 @@ export default async function HomePage() {
- +
@@ -321,7 +338,12 @@ export default async function HomePage() { {faq.question} - +

@@ -338,13 +360,16 @@ export default async function HomePage() {

Very low transaction costs make micro-bounties practical, fast - settlement means contributors aren't waiting days for - payment, and Soroban smart contracts handle escrow, splits, and - milestone payouts natively on-chain. + settlement means contributors aren't waiting days for payment, + and Soroban smart contracts handle escrow, splits, and milestone + payouts natively on-chain.

- diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts index de0dab0..d779982 100644 --- a/src/lib/mock-data.ts +++ b/src/lib/mock-data.ts @@ -247,9 +247,19 @@ export interface LeaderboardEntry { export const topContributors: LeaderboardEntry[] = [ { handle: "priyaeth", earnings: 8420, mergedPRs: 61, topLanguage: "Rust" }, { handle: "0xkoda", earnings: 3210, mergedPRs: 27, topLanguage: "Rust" }, - { handle: "devrel_ana", earnings: 2890, mergedPRs: 24, topLanguage: "TypeScript" }, + { + handle: "devrel_ana", + earnings: 2890, + mergedPRs: 24, + topLanguage: "TypeScript", + }, { handle: "qa_marcus", earnings: 2140, mergedPRs: 19, topLanguage: "Go" }, - { handle: "linh_dev", earnings: 1780, mergedPRs: 15, topLanguage: "TypeScript" }, + { + handle: "linh_dev", + earnings: 1780, + mergedPRs: 15, + topLanguage: "TypeScript", + }, ]; export interface ActivityEvent { @@ -268,77 +278,92 @@ export interface ActivityEvent { occurredAt: string; } -// minutesAgo(n) anchors each mock event n minutes before whenever this -// module is evaluated, so the feed's relative-time text keeps advancing -// (within a given server render) instead of being frozen at deploy time. +// Timestamps are computed at CALL time, not module load — a long-lived +// Next.js server keeps the module resident, so anchoring at import left the +// "live" feed hours/days stale (#707). getRecentActivity() computes fresh +// relative times on every render. function minutesAgo(n: number): string { return new Date(Date.now() - n * 60_000).toISOString(); } -export const recentActivity: ActivityEvent[] = [ - { - id: "a1", - handle: "devrel_ana", - action: "was paid", - target: "core-indexer#288", - amount: 40, - asset: "USDC", - occurredAt: minutesAgo(6), - }, - { - id: "a2", - handle: "0xkoda", - action: "claimed", - target: "soroban-escrow-sdk#17", - occurredAt: minutesAgo(22), - }, - { - id: "a3", - handle: "linh_dev", - action: "opened a pull request for", - target: "stellar-wallet-kit#475", - occurredAt: minutesAgo(48), - }, - { - id: "a4", - handle: "Stellar Development Foundation", - action: "funded", - target: "v2.0: Multi-asset escrow", - amount: 2500, - asset: "USDC", - occurredAt: minutesAgo(71), - }, - { - id: "a4b", - handle: "Stellar Community Fund", - action: "funded", - target: "v1.4: Indexer performance pass", - amount: 15000, - asset: "XLM", - occurredAt: minutesAgo(95), - }, - { - id: "a5", - handle: "priyaeth", - action: "was paid", - target: "core-indexer#301", - amount: 480, - asset: "USDC", - occurredAt: minutesAgo(130), - }, - { - id: "a6", - handle: "qa_marcus", - action: "joined as a", - target: "contributor", - occurredAt: minutesAgo(210), - }, -]; +// Sponsor/org names in fixtures are clearly fictional — the feed renders as +// "live", so real names like "Stellar Development Foundation" would read as +// a real endorsement of the platform (#712). Fictional names + the sample- +// data label keep the demo honest. +export function getRecentActivity(): ActivityEvent[] { + return [ + { + id: "a1", + handle: "devrel_ana", + action: "was paid", + target: "core-indexer#288", + amount: 40, + asset: "USDC", + occurredAt: minutesAgo(6), + }, + { + id: "a2", + handle: "0xkoda", + action: "claimed", + target: "soroban-escrow-sdk#17", + occurredAt: minutesAgo(22), + }, + { + id: "a3", + handle: "linh_dev", + action: "opened a pull request for", + target: "stellar-wallet-kit#475", + occurredAt: minutesAgo(48), + }, + { + id: "a4", + handle: "Meridian Grants", + action: "funded", + target: "v2.0: Multi-asset escrow", + amount: 2500, + asset: "USDC", + occurredAt: minutesAgo(71), + }, + { + id: "a4b", + handle: "Brightpath Fund", + action: "funded", + target: "v1.4: Indexer performance pass", + amount: 15000, + asset: "XLM", + occurredAt: minutesAgo(95), + }, + { + id: "a5", + handle: "priyaeth", + action: "was paid", + target: "core-indexer#301", + amount: 480, + asset: "USDC", + occurredAt: minutesAgo(130), + }, + { + id: "a6", + handle: "qa_marcus", + action: "joined as a", + target: "contributor", + occurredAt: minutesAgo(210), + }, + ]; +} +// Back-compat: `recentActivity` stays exported for stragglers/tests. New +// consumers should call getRecentActivity() so timestamps compute at render +// time (#707) — this snapshot only exists for the remaining readers. +export const recentActivity: ActivityEvent[] = getRecentActivity(); // Weekly earnings/spend series (8 weeks) for dashboard charts. -export const contributorEarningsHistory = [420, 560, 310, 780, 640, 890, 720, 1050]; -export const sponsorSpendHistory = [1800, 2400, 2100, 3200, 2800, 3600, 3100, 4200]; +export const contributorEarningsHistory = [ + 420, 560, 310, 780, 640, 890, 720, 1050, +]; +export const sponsorSpendHistory = [ + 1800, 2400, 2100, 3200, 2800, 3600, 3100, 4200, +]; export const contributorSparkline = [12, 18, 14, 22, 19, 27, 24, 31]; export const bountiesCompletedSparkline = [40, 44, 42, 51, 55, 58, 60, 64];