From e9307d5ae7cac91d63ad9afb12d9964f0c98c701 Mon Sep 17 00:00:00 2001 From: Oluwatos94 Date: Sun, 22 Feb 2026 06:54:31 +0100 Subject: [PATCH 1/5] feat: Implement Earnings & Payout Tracking --- app/profile/[userId]/page.tsx | 437 +++++++++++++-------- components/reputation/earnings-summary.tsx | 97 +++++ 2 files changed, 363 insertions(+), 171 deletions(-) create mode 100644 components/reputation/earnings-summary.tsx diff --git a/app/profile/[userId]/page.tsx b/app/profile/[userId]/page.tsx index 743a525..6085d0e 100644 --- a/app/profile/[userId]/page.tsx +++ b/app/profile/[userId]/page.tsx @@ -4,7 +4,15 @@ import { useContributorReputation } from "@/hooks/use-reputation"; import { useBounties } from "@/hooks/use-bounties"; import { ReputationCard } from "@/components/reputation/reputation-card"; import { CompletionHistory } from "@/components/reputation/completion-history"; -import { MyClaims, type MyClaim } from "@/components/reputation/my-claims"; +import { + MyClaims, + type MyClaim, + normalizeStatus, +} from "@/components/reputation/my-claims"; +import { + EarningsSummary, + type EarningsSummary as EarningsSummaryType, +} from "@/components/reputation/earnings-summary"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -14,184 +22,271 @@ import { useParams } from "next/navigation"; import { useMemo } from "react"; export default function ProfilePage() { - const params = useParams(); - const userId = params.userId as string; - const { data: reputation, isLoading, error } = useContributorReputation(userId); - const { data: bountyResponse } = useBounties(); - - const MAX_MOCK_HISTORY = 50; - - const mockHistory = useMemo(() => { - if (!reputation) return []; - const count = Math.min(reputation.stats.totalCompleted ?? 0, MAX_MOCK_HISTORY); - return Array(count).fill(null).map((_, i) => ({ - id: `bounty-${i}`, - bountyId: `b-${i}`, - bountyTitle: `Implemented feature #${100 + i}`, - projectName: "Drips Protocol", - projectLogoUrl: null, - difficulty: ["BEGINNER", "INTERMEDIATE", "ADVANCED"][i % 3] as "BEGINNER" | "INTERMEDIATE" | "ADVANCED", - rewardAmount: 500, - rewardCurrency: "USDC", - claimedAt: "2023-01-01T00:00:00Z", - completedAt: "2024-01-15T12:00:00Z", - completionTimeHours: 48, - maintainerRating: 5, - maintainerFeedback: "Great work!", - pointsEarned: 150 - })); - }, [reputation]); - - const myClaims = useMemo(() => { - const bounties = bountyResponse?.data ?? []; - - return bounties - .filter((bounty) => bounty.claimedBy === userId) - .map((bounty) => { - let status = "active"; - - if (bounty.status === "closed") { - status = "completed"; - } else if (bounty.status === "claimed" && bounty.claimExpiresAt) { - const claimExpiry = new Date(bounty.claimExpiresAt); - if (!Number.isNaN(claimExpiry.getTime()) && claimExpiry < new Date()) { - status = "in-review"; - } - } - - return { - bountyId: bounty.id, - title: bounty.issueTitle, - status, - rewardAmount: bounty.rewardAmount ?? undefined, - }; - }); - }, [bountyResponse?.data, userId]); - - if (isLoading) { - return ( -
- -
- - -
-
- ); - } + const params = useParams(); + const userId = params.userId as string; + const { + data: reputation, + isLoading, + error, + } = useContributorReputation(userId); + const { data: bountyResponse } = useBounties(); + + const MAX_MOCK_HISTORY = 50; + + const mockHistory = useMemo(() => { + if (!reputation) return []; + const count = Math.min( + reputation.stats.totalCompleted ?? 0, + MAX_MOCK_HISTORY, + ); + return Array(count) + .fill(null) + .map((_, i) => ({ + id: `bounty-${i}`, + bountyId: `b-${i}`, + bountyTitle: `Implemented feature #${100 + i}`, + projectName: "Drips Protocol", + projectLogoUrl: null, + difficulty: ["BEGINNER", "INTERMEDIATE", "ADVANCED"][i % 3] as + | "BEGINNER" + | "INTERMEDIATE" + | "ADVANCED", + rewardAmount: 500, + rewardCurrency: "USDC", + claimedAt: "2023-01-01T00:00:00Z", + completedAt: "2024-01-15T12:00:00Z", + completionTimeHours: 48, + maintainerRating: 5, + maintainerFeedback: "Great work!", + pointsEarned: 150, + })); + }, [reputation]); + + const earningsSummary = useMemo(() => { + const currency = reputation?.stats.earningsCurrency ?? "USDC"; + const completedStatuses = ["completed", "closed", "accepted", "done"]; + const pendingStatuses = [ + "active", + "claimed", + "in-progress", + "in-review", + "in review", + "review", + "pending", + "under-review", + ]; - if (error) { - // Check if it's a 404 (Not Found) - const apiError = error as { status?: number; message?: string }; - const isNotFound = apiError?.status === 404 || apiError?.message?.includes("404"); - - if (isNotFound) { - return ( -
- -

Profile Not Found

-

- We could not find a reputation profile for this user. -

- -
- ); + const allBounties = bountyResponse?.data ?? []; + const userBounties = allBounties.filter((b) => b.claimedBy === userId); + + let totalEarned = 0; + let pendingAmount = 0; + const payoutHistory: EarningsSummaryType["payoutHistory"] = []; + + for (const bounty of userBounties) { + const amount = bounty.rewardAmount ?? 0; + let status = "active"; + if (bounty.status === "closed") { + status = "completed"; + } else if (bounty.status === "claimed" && bounty.claimExpiresAt) { + const expiry = new Date(bounty.claimExpiresAt); + if (!Number.isNaN(expiry.getTime()) && expiry < new Date()) { + status = "in-review"; } + } - // Generic Error - return ( -
- -

Something went wrong

-

- We encountered an error while loading the profile. -

- -
- ); + const normalized = normalizeStatus(status); + if (completedStatuses.some((s) => normalizeStatus(s) === normalized)) { + totalEarned += amount; + payoutHistory.push({ + amount, + date: bounty.claimExpiresAt ?? new Date().toISOString(), + status: "completed", + }); + } else if ( + pendingStatuses.some((s) => normalizeStatus(s) === normalized) + ) { + pendingAmount += amount; + if (normalized === "in-review") { + payoutHistory.push({ + amount, + date: bounty.claimExpiresAt ?? new Date().toISOString(), + status: "processing", + }); + } + } } - if (!reputation) { - return ( -
- -

Profile Not Found

-

- We could not find a reputation profile for this user. -

- -
- ); + // If no real claim data, fall back to reputation stats + if (userBounties.length === 0 && reputation) { + totalEarned = reputation.stats.totalEarnings; } + return { totalEarned, pendingAmount, currency, payoutHistory }; + }, [bountyResponse?.data, userId, reputation]); + + const myClaims = useMemo(() => { + const bounties = bountyResponse?.data ?? []; + + return bounties + .filter((bounty) => bounty.claimedBy === userId) + .map((bounty) => { + let status = "active"; + + if (bounty.status === "closed") { + status = "completed"; + } else if (bounty.status === "claimed" && bounty.claimExpiresAt) { + const claimExpiry = new Date(bounty.claimExpiresAt); + if ( + !Number.isNaN(claimExpiry.getTime()) && + claimExpiry < new Date() + ) { + status = "in-review"; + } + } + + return { + bountyId: bounty.id, + title: bounty.issueTitle, + status, + rewardAmount: bounty.rewardAmount ?? undefined, + }; + }); + }, [bountyResponse?.data, userId]); + + if (isLoading) { return ( -
- - -
- {/* Left Sidebar: Reputation Card */} -
- - - {/* Additional Sidebar Info could go here */} -
- - {/* Main Content: Activity & History */} -
- - - - Bounty History - - - Analytics - - - My Claims - - - - -

Activity History

- -
- - -
- Detailed analytics coming soon. -
-
- - -

My Claims

- -
-
-
-
+
+ +
+ +
+
); + } + + if (error) { + // Check if it's a 404 (Not Found) + const apiError = error as { status?: number; message?: string }; + const isNotFound = + apiError?.status === 404 || apiError?.message?.includes("404"); + + if (isNotFound) { + return ( +
+ +

Profile Not Found

+

+ We could not find a reputation profile for this user. +

+ +
+ ); + } + + // Generic Error + return ( +
+ +

Something went wrong

+

+ We encountered an error while loading the profile. +

+ +
+ ); + } + + if (!reputation) { + return ( +
+ +

Profile Not Found

+

+ We could not find a reputation profile for this user. +

+ +
+ ); + } + + return ( +
+ + +
+ {/* Left Sidebar: Reputation Card */} +
+ + + {/* Additional Sidebar Info could go here */} +
+ + {/* Main Content: Activity & History */} +
+ + + + Bounty History + + + Analytics + + + My Claims + + + + +

Activity History

+ +
+ + +
+ Detailed analytics coming soon. +
+
+ + +

My Claims

+
+ + +
+
+
+
+
+
+ ); } diff --git a/components/reputation/earnings-summary.tsx b/components/reputation/earnings-summary.tsx new file mode 100644 index 0000000..c6028c6 --- /dev/null +++ b/components/reputation/earnings-summary.tsx @@ -0,0 +1,97 @@ +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { formatCurrency, formatDate } from "@/helpers/format.helper"; +import { TrendingUp, Clock } from "lucide-react"; + +export type EarningsSummary = { + totalEarned: number; + pendingAmount: number; + currency: string; + payoutHistory: Array<{ + amount: number; + date: string; + status: "processing" | "completed"; + }>; +}; + +interface EarningsSummaryProps { + earnings: EarningsSummary; +} + +export function EarningsSummary({ earnings }: EarningsSummaryProps) { + const currencySymbol = + earnings.currency === "USDC" ? "USDC" : earnings.currency; + + return ( +
+ {/* Summary Cards */} +
+ + + + + Total Earned + + + +

+ {formatCurrency(earnings.totalEarned, currencySymbol)} +

+
+
+ + + + + + Pending + + + +

+ {formatCurrency(earnings.pendingAmount, currencySymbol)} +

+
+
+
+ + {/* Payout History */} + + + Payout History + + + {earnings.payoutHistory.length === 0 ? ( +

No payouts yet.

+ ) : ( +
+ {earnings.payoutHistory.map((payout, index) => ( +
+
+ + {payout.status} + + + {formatDate(payout.date, "long")} + +
+ + {formatCurrency(payout.amount, currencySymbol)} + +
+ ))} +
+ )} +
+
+
+ ); +} From 9c1214bceafb225380f4c2b5a91f2ff9e39c1af5 Mon Sep 17 00:00:00 2001 From: Oluwatos94 Date: Sun, 22 Feb 2026 07:47:42 +0100 Subject: [PATCH 2/5] fix reviews --- app/profile/[userId]/page.tsx | 98 +++++++++++++---------------------- 1 file changed, 36 insertions(+), 62 deletions(-) diff --git a/app/profile/[userId]/page.tsx b/app/profile/[userId]/page.tsx index 6085d0e..c98a619 100644 --- a/app/profile/[userId]/page.tsx +++ b/app/profile/[userId]/page.tsx @@ -4,11 +4,7 @@ import { useContributorReputation } from "@/hooks/use-reputation"; import { useBounties } from "@/hooks/use-bounties"; import { ReputationCard } from "@/components/reputation/reputation-card"; import { CompletionHistory } from "@/components/reputation/completion-history"; -import { - MyClaims, - type MyClaim, - normalizeStatus, -} from "@/components/reputation/my-claims"; +import { MyClaims, type MyClaim } from "@/components/reputation/my-claims"; import { EarningsSummary, type EarningsSummary as EarningsSummaryType, @@ -21,6 +17,20 @@ import Link from "next/link"; import { useParams } from "next/navigation"; import { useMemo } from "react"; +function deriveBountyStatus(bounty: { + status: string; + claimExpiresAt?: string | null; +}): "completed" | "in-review" | "active" { + if (bounty.status === "closed") return "completed"; + if (bounty.status === "claimed" && bounty.claimExpiresAt) { + const expiry = new Date(bounty.claimExpiresAt); + if (!Number.isNaN(expiry.getTime()) && expiry < new Date()) { + return "in-review"; + } + } + return "active"; +} + export default function ProfilePage() { const params = useParams(); const userId = params.userId as string; @@ -64,18 +74,6 @@ export default function ProfilePage() { const earningsSummary = useMemo(() => { const currency = reputation?.stats.earningsCurrency ?? "USDC"; - const completedStatuses = ["completed", "closed", "accepted", "done"]; - const pendingStatuses = [ - "active", - "claimed", - "in-progress", - "in-review", - "in review", - "review", - "pending", - "under-review", - ]; - const allBounties = bountyResponse?.data ?? []; const userBounties = allBounties.filter((b) => b.claimedBy === userId); @@ -85,35 +83,27 @@ export default function ProfilePage() { for (const bounty of userBounties) { const amount = bounty.rewardAmount ?? 0; - let status = "active"; - if (bounty.status === "closed") { - status = "completed"; - } else if (bounty.status === "claimed" && bounty.claimExpiresAt) { - const expiry = new Date(bounty.claimExpiresAt); - if (!Number.isNaN(expiry.getTime()) && expiry < new Date()) { - status = "in-review"; - } - } + const status = deriveBountyStatus(bounty); - const normalized = normalizeStatus(status); - if (completedStatuses.some((s) => normalizeStatus(s) === normalized)) { + if (status === "completed") { totalEarned += amount; - payoutHistory.push({ - amount, - date: bounty.claimExpiresAt ?? new Date().toISOString(), - status: "completed", - }); - } else if ( - pendingStatuses.some((s) => normalizeStatus(s) === normalized) - ) { - pendingAmount += amount; - if (normalized === "in-review") { + if (bounty.claimExpiresAt) { payoutHistory.push({ amount, - date: bounty.claimExpiresAt ?? new Date().toISOString(), - status: "processing", + date: bounty.claimExpiresAt, + status: "completed", }); } + } else if (status === "in-review") { + pendingAmount += amount; + // claimExpiresAt is guaranteed when status is "in-review" + payoutHistory.push({ + amount, + date: bounty.claimExpiresAt!, + status: "processing", + }); + } else { + pendingAmount += amount; } } @@ -130,28 +120,12 @@ export default function ProfilePage() { return bounties .filter((bounty) => bounty.claimedBy === userId) - .map((bounty) => { - let status = "active"; - - if (bounty.status === "closed") { - status = "completed"; - } else if (bounty.status === "claimed" && bounty.claimExpiresAt) { - const claimExpiry = new Date(bounty.claimExpiresAt); - if ( - !Number.isNaN(claimExpiry.getTime()) && - claimExpiry < new Date() - ) { - status = "in-review"; - } - } - - return { - bountyId: bounty.id, - title: bounty.issueTitle, - status, - rewardAmount: bounty.rewardAmount ?? undefined, - }; - }); + .map((bounty) => ({ + bountyId: bounty.id, + title: bounty.issueTitle, + status: deriveBountyStatus(bounty), + rewardAmount: bounty.rewardAmount ?? undefined, + })); }, [bountyResponse?.data, userId]); if (isLoading) { From 3feca06d4d79e0e5dd5e11b18bd5d8b736f6b65e Mon Sep 17 00:00:00 2001 From: Oluwatos94 Date: Sun, 22 Feb 2026 08:18:18 +0100 Subject: [PATCH 3/5] fix reviews --- app/profile/[userId]/page.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/profile/[userId]/page.tsx b/app/profile/[userId]/page.tsx index c98a619..8d9e785 100644 --- a/app/profile/[userId]/page.tsx +++ b/app/profile/[userId]/page.tsx @@ -39,7 +39,7 @@ export default function ProfilePage() { isLoading, error, } = useContributorReputation(userId); - const { data: bountyResponse } = useBounties(); + const { data: bountyResponse, isLoading: isBountiesLoading } = useBounties(); const MAX_MOCK_HISTORY = 50; @@ -87,13 +87,9 @@ export default function ProfilePage() { if (status === "completed") { totalEarned += amount; - if (bounty.claimExpiresAt) { - payoutHistory.push({ - amount, - date: bounty.claimExpiresAt, - status: "completed", - }); - } + // Use claimExpiresAt as a proxy for payout date, fall back to createdAt. + const payoutDate = bounty.claimExpiresAt ?? bounty.createdAt; + payoutHistory.push({ amount, date: payoutDate, status: "completed" }); } else if (status === "in-review") { pendingAmount += amount; // claimExpiresAt is guaranteed when status is "in-review" @@ -110,6 +106,7 @@ export default function ProfilePage() { // If no real claim data, fall back to reputation stats if (userBounties.length === 0 && reputation) { totalEarned = reputation.stats.totalEarnings; + // pendingAmount has no reputation-stats equivalent; intentionally left as 0 } return { totalEarned, pendingAmount, currency, payoutHistory }; @@ -128,7 +125,7 @@ export default function ProfilePage() { })); }, [bountyResponse?.data, userId]); - if (isLoading) { + if (isLoading || isBountiesLoading) { return (
From d9e79c3fbcb8f48b92a09446b334130bc3ba2e44 Mon Sep 17 00:00:00 2001 From: Oluwatos94 Date: Sun, 22 Feb 2026 15:54:18 +0100 Subject: [PATCH 4/5] fix reviews --- app/profile/[userId]/page.tsx | 82 ++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/app/profile/[userId]/page.tsx b/app/profile/[userId]/page.tsx index 8d9e785..b143603 100644 --- a/app/profile/[userId]/page.tsx +++ b/app/profile/[userId]/page.tsx @@ -39,38 +39,36 @@ export default function ProfilePage() { isLoading, error, } = useContributorReputation(userId); - const { data: bountyResponse, isLoading: isBountiesLoading } = useBounties(); - - const MAX_MOCK_HISTORY = 50; + const { + data: bountyResponse, + isLoading: isBountiesLoading, + error: bountiesError, + } = useBounties(); - const mockHistory = useMemo(() => { - if (!reputation) return []; - const count = Math.min( - reputation.stats.totalCompleted ?? 0, - MAX_MOCK_HISTORY, - ); - return Array(count) - .fill(null) - .map((_, i) => ({ - id: `bounty-${i}`, - bountyId: `b-${i}`, - bountyTitle: `Implemented feature #${100 + i}`, - projectName: "Drips Protocol", - projectLogoUrl: null, - difficulty: ["BEGINNER", "INTERMEDIATE", "ADVANCED"][i % 3] as + const completionHistory = useMemo(() => { + const bounties = bountyResponse?.data ?? []; + return bounties + .filter((b) => b.claimedBy === userId && b.status === "closed") + .map((b) => ({ + id: b.id, + bountyId: b.id, + bountyTitle: b.issueTitle, + projectName: b.projectName, + projectLogoUrl: b.projectLogoUrl, + difficulty: (b.difficulty?.toUpperCase() ?? "BEGINNER") as | "BEGINNER" | "INTERMEDIATE" | "ADVANCED", - rewardAmount: 500, - rewardCurrency: "USDC", - claimedAt: "2023-01-01T00:00:00Z", - completedAt: "2024-01-15T12:00:00Z", - completionTimeHours: 48, - maintainerRating: 5, - maintainerFeedback: "Great work!", - pointsEarned: 150, + rewardAmount: b.rewardAmount ?? 0, + rewardCurrency: b.rewardCurrency, + claimedAt: b.claimedAt ?? b.createdAt, + completedAt: b.updatedAt, + completionTimeHours: 0, + maintainerRating: null, + maintainerFeedback: null, + pointsEarned: 0, })); - }, [reputation]); + }, [bountyResponse?.data, userId]); const earningsSummary = useMemo(() => { const currency = reputation?.stats.earningsCurrency ?? "USDC"; @@ -236,10 +234,17 @@ export default function ProfilePage() {

Activity History

- + {bountiesError ? ( +
+ + Failed to load bounty history. Please try again. +
+ ) : ( + + )}
@@ -250,10 +255,17 @@ export default function ProfilePage() {

My Claims

-
- - -
+ {bountiesError ? ( +
+ + Failed to load claims and earnings. Please try again. +
+ ) : ( +
+ + +
+ )}
From 5fc38a6e92962c4f32eec08ff867b3eefbd488a2 Mon Sep 17 00:00:00 2001 From: Oluwatos94 Date: Sun, 22 Feb 2026 16:20:56 +0100 Subject: [PATCH 5/5] fix reviews --- app/profile/[userId]/page.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/profile/[userId]/page.tsx b/app/profile/[userId]/page.tsx index b143603..9cdef84 100644 --- a/app/profile/[userId]/page.tsx +++ b/app/profile/[userId]/page.tsx @@ -98,6 +98,11 @@ export default function ProfilePage() { }); } else { pendingAmount += amount; + payoutHistory.push({ + amount, + date: bounty.claimExpiresAt ?? bounty.createdAt, + status: "processing", + }); } }