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; + } +} diff --git a/src/app/issues/[id]/page.tsx b/src/app/issues/[id]/page.tsx index e7b0248..88469ae 100644 --- a/src/app/issues/[id]/page.tsx +++ b/src/app/issues/[id]/page.tsx @@ -4,9 +4,20 @@ import { fetchBounty } from "@/lib/api"; import { mockBounties } from "@/lib/mock-data"; import { StatusBadge, DifficultyBadge, Badge } from "@/components/ui/Badge"; import { BountyDescription } from "@/components/bounty/BountyDescription"; -import { formatCurrency, daysUntil } from "@/lib/utils"; +import { formatCurrency, formatDaysUntil, daysUntil } from "@/lib/utils"; import { IssueActions } from "./IssueActions"; +const escrowStatusLabel: 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 default async function IssueDetailPage({ params, }: { @@ -50,7 +61,7 @@ export default async function IssueDetailPage({ Escrow status

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

@@ -59,7 +70,7 @@ export default async function IssueDetailPage({ Deadline

- {days > 0 ? `${days} days left` : "Passed"} + {formatDaysUntil(days)}

diff --git a/src/components/bounty/BountyCard.tsx b/src/components/bounty/BountyCard.tsx index f9e9fd5..a6e79c7 100644 --- a/src/components/bounty/BountyCard.tsx +++ b/src/components/bounty/BountyCard.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { Clock, GitPullRequest } from "lucide-react"; import { StatusBadge, DifficultyBadge, Badge } from "@/components/ui/Badge"; import { Avatar } from "@/components/ui/Avatar"; -import { formatCurrency, daysUntil } from "@/lib/utils"; +import { formatCurrency, formatDaysUntil, daysUntil } from "@/lib/utils"; import { stripMarkdownToPlainText } from "@/lib/markdown"; import type { Bounty } from "@/types"; @@ -49,7 +49,7 @@ export function BountyCard({ bounty }: { bounty: Bounty }) {
- {days > 0 ? `${days}d left` : "Deadline passed"} + {formatDaysUntil(days)} {bounty.claimedBy && ( diff --git a/src/components/bounty/BountyDescription.tsx b/src/components/bounty/BountyDescription.tsx index 16c2ef7..d5dd213 100644 --- a/src/components/bounty/BountyDescription.tsx +++ b/src/components/bounty/BountyDescription.tsx @@ -49,6 +49,30 @@ const markdownComponents: Components = { {children} ), + table: ({ children }) => ( +
+ + {children} +
+
+ ), + thead: ({ children }) => ( + + {children} + + ), + tbody: ({ children }) => ( + + {children} + + ), + tr: ({ children }) => {children}, + th: ({ children }) => ( + + {children} + + ), + td: ({ children }) => {children}, }; /** diff --git a/src/components/ui/StatCard.tsx b/src/components/ui/StatCard.tsx index b39789b..397e5a2 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,10 +78,7 @@ function formatValue( ): { display: string; exact: string } { switch (format) { case "currency": { - const formatted = value.toLocaleString("en-US", { - maximumFractionDigits: 2, - }); - const display = `${formatted} ${asset}`; + const display = formatCurrency(value, asset); // Exact value for tooltip always shows full precision const exact = `${value.toLocaleString("en-US", { maximumFractionDigits: 6 })} ${asset}`; return { display, exact }; diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index 533a0a4..a5256a8 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -7,6 +7,7 @@ import { formatCurrency, + formatDaysUntil, formatPercent, coerceDecimal, coerceNonNegative, @@ -92,7 +93,20 @@ describe("formatCurrency", () => { }); }); -// ─── formatPercent ─────────────────────────────────────────────────────────── +// ─── formatDaysUntil ──────────────────────────────────────────────────────── + +describe("formatDaysUntil", () => { + it("formats positive days correctly", () => { + expect(formatDaysUntil(5)).toBe("5 days left"); + expect(formatDaysUntil(1)).toBe("1 day left"); + }); + + it("formats 0 or negative days as 'Deadline passed'", () => { + expect(formatDaysUntil(0)).toBe("Deadline passed"); + expect(formatDaysUntil(-2)).toBe("Deadline passed"); + }); +}); + describe("formatPercent", () => { it("converts a fraction to a percentage", () => { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 7c1f83c..7330dea 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -66,6 +66,11 @@ export function daysUntil(dateIso: string) { return Math.ceil(diffMs / (1000 * 60 * 60 * 24)); } +export function formatDaysUntil(days: number): string { + if (days <= 0) return "Deadline passed"; + return `${days} ${days === 1 ? "day" : "days"} left`; +} + export function validateTeamSplits( splits: Array<{ percentage: string | number }>, tolerance = 0.01