Skip to content
Merged
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
19 changes: 17 additions & 2 deletions src/app/dashboard/contributor/ContributorDashboardClient.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<DashboardStats | null>(null);
const [bounties, setBounties] = useState<Bounty[]>(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<StatCardStatus>("loading");
Expand Down Expand Up @@ -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 (
<DashboardShell
role="contributor"
Expand Down
7 changes: 6 additions & 1 deletion src/app/dashboard/contributor/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import ContributorDashboardClient from "./ContributorDashboardClient";

export const metadata: Metadata = {
Expand All @@ -8,5 +9,9 @@ export const metadata: Metadata = {
};

export default function ContributorDashboardPage() {
return <ContributorDashboardClient />;
return (
<Suspense fallback={null}>
<ContributorDashboardClient />
</Suspense>
);
}
11 changes: 11 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
14 changes: 13 additions & 1 deletion src/app/issues/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<BountyStatus, string> = {
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,
}: {
Expand Down Expand Up @@ -85,7 +97,7 @@ export default async function IssueDetailPage({
<span className="text-sm">Escrow status</span>
</div>
<p className="mt-2 font-medium text-slate-900 dark:text-white">
{bounty.status === "open" ? "Awaiting funding" : "Funds locked"}
{ESCROW_STATUS_LABELS[bounty.status]}
</p>
</div>
<div className="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
Expand Down
12 changes: 3 additions & 9 deletions src/components/ui/StatCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)}%`;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down
36 changes: 22 additions & 14 deletions src/types/bounty.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down