From 8d381f71879fe5e0b67d3f10723d8ebf9e6da4f9 Mon Sep 17 00:00:00 2001 From: Levi-Ojukwu Date: Sun, 30 Aug 2026 16:20:19 +0100 Subject: [PATCH 1/4] fix: add prefers-reduced-motion media query to disable animations globally Addresses #258. Users who set their OS/browser preference to reduce motion now get a reduced-motion experience: all transitions and animations are effectively disabled via a @media (prefers-reduced-motion: reduce) block. --- src/app/globals.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/globals.css b/src/app/globals.css index 2391048..e5ea895 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -58,3 +58,14 @@ body { background-clip: text; color: transparent; } + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} From ba9ed104192bd93d4214886fad70a283c0be5ef9 Mon Sep 17 00:00:00 2001 From: Levi-Ojukwu Date: Sun, 30 Aug 2026 16:24:53 +0100 Subject: [PATCH 2/4] fix: show correct escrow status label for all bounty statuses in IssueDetailPage Addresses #259. The escrow status card previously used a binary ternary that labeled every non-open status as 'Funds locked', even for paid, refunded, and expired bounties where funds have already left escrow. Now maps each BountyStatus to its accurate label: 'Awaiting funding' (open), 'Funds locked' (funded/claimed/in_review/merged), 'Paid out' (paid), 'Refunded to sponsor' (refunded), 'Expired, unclaimed' (expired). Also aligns the BountyStatus type with the actual statuses used in code. --- src/app/issues/[id]/page.tsx | 14 +++++++++++++- src/types/bounty.ts | 36 ++++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/app/issues/[id]/page.tsx b/src/app/issues/[id]/page.tsx index 0f24a46..cc85a79 100644 --- a/src/app/issues/[id]/page.tsx +++ b/src/app/issues/[id]/page.tsx @@ -6,8 +6,20 @@ import { mockBounties } from "@/lib/mock-data"; import { StatusBadge, DifficultyBadge, Badge } from "@/components/ui/Badge"; import { BountyDescription } from "@/components/bounty/BountyDescription"; import { formatCurrency, daysUntil, formatDaysUntil } from "@/lib/utils"; +import type { BountyStatus } from "@/types"; import { IssueActions } from "./IssueActions"; +const ESCROW_STATUS_LABELS: Record = { + open: "Awaiting funding", + funded: "Funds locked", + claimed: "Funds locked", + in_review: "Funds locked", + merged: "Funds locked", + paid: "Paid out", + refunded: "Refunded to sponsor", + expired: "Expired, unclaimed", +}; + export async function generateMetadata({ params, }: { @@ -85,7 +97,7 @@ export default async function IssueDetailPage({ Escrow status

- {bounty.status === "open" ? "Awaiting funding" : "Funds locked"} + {ESCROW_STATUS_LABELS[bounty.status]}

diff --git a/src/types/bounty.ts b/src/types/bounty.ts index ee57302..e9e6239 100644 --- a/src/types/bounty.ts +++ b/src/types/bounty.ts @@ -1,24 +1,32 @@ -export type BountyStatus = - | 'open' - | 'in-progress' - | 'claimed' - | 'completed' - | 'cancelled'; +export type BountyStatus = + | 'open' + | 'funded' + | 'claimed' + | 'in_review' + | 'merged' + | 'paid' + | 'refunded' + | 'expired'; export interface Bounty { id: string; title: string; description: string; - amount: number; + reward: number; + asset: "USDC" | "XLM"; + difficulty: string; status: BountyStatus; - claimedBy?: string; - claimedAt?: string; - createdAt: string; - updatedAt: string; - repository: string; + org: string; + repo: string; issueNumber: number; - maintainer?: string; - assignee?: string; + labels: string[]; + deadline: string | null; + claimedBy?: string; + claimedById?: string; + milestoneId?: string; + escrowId?: string; + teamSplits?: { role: string; percentage: number; contributor?: string }[]; + teamSplitsValid?: { valid: boolean; sum: number; message?: string }; } export interface BountyStatusUpdate { From 5ec78d23440ecc828bd802d2d5de922b9724ad3a Mon Sep 17 00:00:00 2001 From: Levi-Ojukwu Date: Sun, 30 Aug 2026 16:26:16 +0100 Subject: [PATCH 3/4] fix: sync contributor dashboard tab selection with URL search params Addresses #260. The Active/Completed tab state is now persisted to the URL as a ?tab=completed search param via shallow router.replace, so the selected tab survives page refreshes and can be shared via link. Wrapped the client component in Suspense as required by Next.js useSearchParams. --- .../ContributorDashboardClient.tsx | 19 +++++++++++++++++-- src/app/dashboard/contributor/page.tsx | 7 ++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/contributor/ContributorDashboardClient.tsx b/src/app/dashboard/contributor/ContributorDashboardClient.tsx index a1a8fe9..7580b93 100644 --- a/src/app/dashboard/contributor/ContributorDashboardClient.tsx +++ b/src/app/dashboard/contributor/ContributorDashboardClient.tsx @@ -1,7 +1,8 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; import { DollarSign, GitMerge, TrendingUp, ListChecks, GitPullRequest } from "lucide-react"; import { DashboardShell } from "@/components/dashboard/DashboardShell"; import { ActivityList } from "@/components/dashboard/ActivityList"; @@ -42,10 +43,14 @@ const earningsChartData = contributorEarningsHistory.map((value, i) => ({ export default function ContributorDashboardClient() { const { user, loading } = useAuth(); + const router = useRouter(); + const searchParams = useSearchParams(); const [stats, setStats] = useState(null); const [bounties, setBounties] = useState(mockBounties); const [isLive, setIsLive] = useState(false); - const [tab, setTab] = useState<"active" | "completed">("active"); + const [tab, setTabState] = useState<"active" | "completed">( + (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. const [fetchStatus, setFetchStatus] = useState("loading"); @@ -108,6 +113,16 @@ export default function ContributorDashboardClient() { const available = bounties.filter((b) => b.status === "open"); const shownClaims = tab === "active" ? activeClaims : completedClaims; + const setTab = useCallback( + (newTab: "active" | "completed") => { + setTabState(newTab); + const params = new URLSearchParams(searchParams.toString()); + params.set("tab", newTab); + router.replace(`?${params.toString()}`, { scroll: false }); + }, + [router, searchParams], + ); + return ( ; + return ( + + + + ); } From e1585e929ea798b7e85245a116a43eadb5d86587 Mon Sep 17 00:00:00 2001 From: Levi-Ojukwu Date: Sun, 30 Aug 2026 16:27:17 +0100 Subject: [PATCH 4/4] refactor: use shared formatCurrency utility in StatCard instead of duplicating logic Addresses #261. StatCard's currency formatting now calls formatCurrency from utils.ts directly instead of reimplementing the same toLocaleString logic. Eliminates the drift-prone pattern of maintaining identical formatting rules in two independent locations. --- src/components/ui/StatCard.tsx | 12 +++--------- src/lib/utils.ts | 4 ++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/components/ui/StatCard.tsx b/src/components/ui/StatCard.tsx index ee3b035..ec6a846 100644 --- a/src/components/ui/StatCard.tsx +++ b/src/components/ui/StatCard.tsx @@ -22,7 +22,7 @@ * always available via the title attribute (keyboard-navigable, hover tooltip). */ -import { cn } from "@/lib/utils"; +import { cn, formatCurrency } from "@/lib/utils"; import { ArrowUpRight, ArrowDownRight, AlertCircle } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import { Sparkline } from "./Sparkline"; @@ -78,14 +78,8 @@ function formatValue( ): { display: string; exact: string } { switch (format) { case "currency": { - const maxDecimals = asset === "XLM" ? 7 : 2; - const formatted = value.toLocaleString("en-US", { - maximumFractionDigits: maxDecimals, - }); - const display = `${formatted} ${asset}`; - // Exact value for tooltip shows full asset-specific precision - const exact = `${value.toLocaleString("en-US", { maximumFractionDigits: maxDecimals })} ${asset}`; - return { display, exact }; + const formatted = formatCurrency(value, asset); + return { display: formatted, exact: formatted }; } case "percent": { const pct = `${Math.round(value * 100)}%`; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6c1a24d..cfd734a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -58,8 +58,8 @@ const SANITY_CEILING = 1_000_000_000; // 1 billion * - Deltas or changes (e.g., budget remaining after overspending) * * This behavior matches StatCard's internal currency formatter, ensuring - * consistency across the app. The same negative input will now render - * identically whether formatted by formatCurrency() or StatCard. + * consistency across the app. StatCard's currency format uses this function + * directly. */ export function formatCurrency(amount: number, asset: "USDC" | "XLM" = "USDC") { if (!Number.isFinite(amount)) return `0 ${asset}`;