Skip to content

Comments

feat: Implement Earnings & Payout Tracking#97

Open
Oluwatos94 wants to merge 7 commits intoboundlessfi:mainfrom
Oluwatos94:feature/Earnings-&payout-tracking
Open

feat: Implement Earnings & Payout Tracking#97
Oluwatos94 wants to merge 7 commits intoboundlessfi:mainfrom
Oluwatos94:feature/Earnings-&payout-tracking

Conversation

@Oluwatos94
Copy link

@Oluwatos94 Oluwatos94 commented Feb 22, 2026

feat: implement earnings & payout tracking in My Claims

Adds an EarningsSummary component to the My Claims tab on the profile page, showing total earned, pending amount (by currency), and a payout history list with processing/completed statuses. Earnings data is derived from the user's bounty claims, with a fallback to reputation stats when no claim data is available.

Screenshot (1588)

closes #84

Summary by CodeRabbit

  • New Features

    • Earnings dashboard on profile pages showing Total Earned, Pending, and Payout History with status badges and empty-state handling.
    • Profile layout refreshed to a two-column view with tabs for History, Analytics, and Claims; My Claims integrated into Claims tab.
  • Improvements

    • Distinct loading and error states for reputation and bounty data (including not-found handling).
    • More accurate bounty status and earnings aggregation for analytics and claims.

@vercel
Copy link

vercel bot commented Feb 22, 2026

@Oluwatos94 is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Integrates a new EarningsSummary component and earnings derivation into the profile page: adds deriveBountyStatus, computes earningsSummary from bounties/reputation, separates bounty vs reputation loading and error paths, and renders earnings and claims in the Claims tab.

Changes

Cohort / File(s) Summary
Earnings UI Component
components/reputation/earnings-summary.tsx
New exported EarningsSummary component and EarningsSummary type. Renders total earned, pending amount, and payout history with formatted currency, dates, and status badges.
Profile Page Integration
app/profile/[userId]/page.tsx
Refactors profile page: imports EarningsSummary, adds deriveBountyStatus, computes earningsSummary, separates loading flags (isLoading vs isBountiesLoading), improves 404/generic error handling, and renders a two-column layout with ReputationCard plus Tabs (History, Analytics, Claims) showing EarningsSummary and MyClaims.
Types / Data Flow
app/profile/[userId]/page.tsx, components/reputation/earnings-summary.tsx
Introduces the EarningsSummary shape and normalizes bounty/reputation data into a payoutHistory array (amount, date, status) consumed by the UI component.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Page as ProfilePage
  participant BountySvc as BountyService
  participant RepSvc as ReputationService
  participant UI as EarningsSummary

  Client->>Page: Request profile page (userId)
  Page->>BountySvc: fetch bounties for userId
  Page->>RepSvc: fetch reputation for userId
  BountySvc-->>Page: bountyResponse
  RepSvc-->>Page: reputationResponse
  Page->>Page: deriveBountyStatus() and compute earningsSummary (total, pending, payoutHistory)
  Page->>UI: render EarningsSummary with earningsSummary
  Page->>Client: render profile page (Claims tab includes earnings + claims)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through bounties, coins to chart,
I tallied payouts with a nimble heart.
Pending, paid, each date in view,
I render totals fresh and true.
Hop, tally, shine — the earnings grew! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Implement Earnings & Payout Tracking' directly and clearly summarizes the main changes, which involve adding earnings and payout tracking functionality to the profile page.
Linked Issues check ✅ Passed The PR implements all requirements from issue #84: EarningsSummary component with the exact expected data shape, proper rendering in the My Claims tab, and derives accurate totals from bounty data.
Out of Scope Changes check ✅ Passed All code changes are directly aligned with the linked issue requirements; the profile page restructuring, new components, and data derivation all serve the earnings tracking feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
components/reputation/earnings-summary.tsx (2)

6-15: Type and component share the same name EarningsSummary, forcing awkward aliasing at import sites.

In page.tsx (line 14), the consumer must alias the type as EarningsSummaryType to avoid collision with the component. Consider renaming either the type (e.g., EarningsSummaryData) or the component (e.g., EarningsSummaryCard) so both can be imported cleanly.

Also applies to: 21-21

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/reputation/earnings-summary.tsx` around lines 6 - 15, Rename one
of the colliding identifiers so imports are unambiguous: either rename the type
EarningsSummary to EarningsSummaryData (update the export type declaration) or
rename the React component EarningsSummary to EarningsSummaryCard (update the
component declaration and its export). Ensure all references in this file (the
exported type and the component name) and any imports in consumers like page.tsx
are updated accordingly to use the new symbol name.

22-23: currencySymbol ternary is a no-op — both branches return the same value.

earnings.currency === "USDC" ? "USDC" : earnings.currency always evaluates to earnings.currency. If the intent was to map currency codes to symbols (e.g., "USD""$"), that mapping is missing. Otherwise, simplify to const currencySymbol = earnings.currency;.

Proposed fix
-  const currencySymbol =
-    earnings.currency === "USDC" ? "USDC" : earnings.currency;
+  const currencySymbol = earnings.currency;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/reputation/earnings-summary.tsx` around lines 22 - 23, The ternary
assigning currencySymbol is redundant: replace the expression using
earnings.currency === "USDC" ? "USDC" : earnings.currency with either a direct
assignment const currencySymbol = earnings.currency or implement a proper
mapping from currency codes to display symbols (e.g., map["USD"] = "$",
map["USDC"] = "USDC") and use that mapping to set currencySymbol; update uses of
currencySymbol accordingly in the earnings-summary component.
app/profile/[userId]/page.tsx (4)

93-93: new Date() is instantiated multiple times across loop iterations — subtle inconsistency risk.

Each new Date() call (lines 93, 103, 113, 142) captures a slightly different timestamp. While practically negligible, a bounty right at the expiry boundary could be classified differently across the two useMemo blocks within the same render. Capture const now = new Date() once at the top of each memo callback.

Proposed fix (in earningsSummary memo)
   const earningsSummary = useMemo<EarningsSummaryType>(() => {
+    const now = new Date();
     // ...
     for (const bounty of userBounties) {
       // ...
-        if (!Number.isNaN(expiry.getTime()) && expiry < new Date()) {
+        if (!Number.isNaN(expiry.getTime()) && expiry < now) {

Apply the same pattern in the myClaims memo.

Also applies to: 103-103, 113-113, 142-142

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx at line 93, The code repeatedly calls new
Date() inside memo callbacks causing subtle inconsistencies; inside each useMemo
(notably the earningsSummary and myClaims memo callbacks) capture a single const
now = new Date() at the top of the callback and replace all subsequent new
Date() comparisons (e.g., expiry < new Date(), expiry.getTime() checks) with the
single now variable so all expiry/time comparisons within that memo use the same
timestamp.

79-80: All bounties are fetched client-side and then filtered — potential scalability concern.

useBounties() fetches the entire bounty list, and lines 79-80 then filter to the current user. As the dataset grows this will become increasingly wasteful. Consider adding a server-side filter (e.g., useBounties({ claimedBy: userId })) if the API supports it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 79 - 80, The page is fetching all
bounties then filtering in the client (allBounties -> userBounties) which will
not scale; update the useBounties call to request only the current user’s
bounties (e.g., change useBounties() to useBounties({ claimedBy: userId })) and
remove the client-side filter on allBounties, or if the API lacks that param
implement a server-side fetch for user bounties and populate userBounties from
that response; refer to useBounties, allBounties, userBounties and userId when
making the change.

34-63: mockHistory is entirely synthetic hardcoded data.

This produces fake "Implemented feature #N" entries with static dates and ratings. If this is a placeholder pending real data integration, consider adding a // TODO comment to make that intent explicit so it isn't forgotten.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 34 - 63, The mockHistory useMemo
block (using MAX_MOCK_HISTORY and the mockHistory variable) currently returns
fully synthetic hardcoded entries (static titles, dates, ratings, etc.);
annotate this clearly as a temporary placeholder by adding a TODO comment above
the useMemo (e.g., TODO: replace with real user reputation/bounty history from
API) and briefly describe what real data should contain (completedAt, claimedAt,
maintainerRating, pointsEarned, etc.) so future maintainers know to replace
mockHistory with real fetch/transform logic tied to reputation; keep the
existing structure but mark it explicit and consider leaving a small note about
limits (MAX_MOCK_HISTORY) and expected types.

67-77: normalizeStatus is called repeatedly on static string lists inside the loop — precompute once.

completedStatuses and pendingStatuses are constant arrays, yet normalizeStatus(s) is called for each element via .some() on every loop iteration. Precompute the normalized sets before the loop to avoid redundant work.

Proposed fix
+    const normalizedCompleted = new Set(completedStatuses.map(normalizeStatus));
+    const normalizedPending = new Set(pendingStatuses.map(normalizeStatus));
+
     for (const bounty of userBounties) {
       // ...derive status...
       const normalized = normalizeStatus(status);
-      if (completedStatuses.some((s) => normalizeStatus(s) === normalized)) {
+      if (normalizedCompleted.has(normalized)) {
         // ...
-      } else if (pendingStatuses.some((s) => normalizeStatus(s) === normalized)) {
+      } else if (normalizedPending.has(normalized)) {
         // ...
       }
     }

Also applies to: 98-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 67 - 77, Precompute normalized
status sets to avoid calling normalizeStatus repeatedly: map completedStatuses
and pendingStatuses through normalizeStatus once before the loop and store them
in Sets (e.g., normalizedCompletedStatuses, normalizedPendingStatuses), then
inside the loop normalize only the current item's status and check membership
with Set.has(...) instead of using array.some(... normalizeStatus ...); update
all occurrences that currently call normalizeStatus within .some (including the
repeated instances around completedStatuses/pendingStatuses) to use these
precomputed Sets.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/profile/`[userId]/page.tsx:
- Around line 101-105: The payout date fallback currently uses new
Date().toISOString() in the payoutHistory.push call, which yields a misleading
current-render timestamp; change the fallback to a meaningful value such as
bounty.createdAt (or leave the date undefined/null) so the UI shows an actual
related timestamp instead of the render time—update the occurrences where
payoutHistory.push sets date (referencing payoutHistory.push,
bounty.claimExpiresAt, and bounty.createdAt) and apply the same fix to the other
similar block around the second occurrence.
- Around line 86-96: The status-derivation logic for bounties is duplicated;
extract it into a single helper function e.g. deriveBountyStatus(bounty) that
takes a bounty object and returns "completed" for bounty.status === "closed",
"in-review" when bounty.status === "claimed" and claimExpiresAt is a valid past
date, otherwise "active". Replace the inline logic in both the earningsSummary
useMemo and myClaims useMemo with calls to deriveBountyStatus(bounty) so both
blocks use the shared helper.
- Around line 67-77: The arrays completedStatuses and pendingStatuses include
many values that the component's status derivation only ever returns
("completed", "in-review", "active"); update them to match the actual derived
values or update the derivation to emit the additional variants. Specifically,
either trim completedStatuses to only ["completed"] (and any exact synonyms the
derivation can produce) and pendingStatuses to only ["in-review","active"], or
modify the status derivation logic so it returns
"accepted","done","claimed","in-progress","pending","under-review", etc.,
consistently; ensure the .some() checks on completedStatuses/pendingStatuses use
the exact strings emitted by the derivation so the checks are meaningful.

---

Nitpick comments:
In `@app/profile/`[userId]/page.tsx:
- Line 93: The code repeatedly calls new Date() inside memo callbacks causing
subtle inconsistencies; inside each useMemo (notably the earningsSummary and
myClaims memo callbacks) capture a single const now = new Date() at the top of
the callback and replace all subsequent new Date() comparisons (e.g., expiry <
new Date(), expiry.getTime() checks) with the single now variable so all
expiry/time comparisons within that memo use the same timestamp.
- Around line 79-80: The page is fetching all bounties then filtering in the
client (allBounties -> userBounties) which will not scale; update the
useBounties call to request only the current user’s bounties (e.g., change
useBounties() to useBounties({ claimedBy: userId })) and remove the client-side
filter on allBounties, or if the API lacks that param implement a server-side
fetch for user bounties and populate userBounties from that response; refer to
useBounties, allBounties, userBounties and userId when making the change.
- Around line 34-63: The mockHistory useMemo block (using MAX_MOCK_HISTORY and
the mockHistory variable) currently returns fully synthetic hardcoded entries
(static titles, dates, ratings, etc.); annotate this clearly as a temporary
placeholder by adding a TODO comment above the useMemo (e.g., TODO: replace with
real user reputation/bounty history from API) and briefly describe what real
data should contain (completedAt, claimedAt, maintainerRating, pointsEarned,
etc.) so future maintainers know to replace mockHistory with real
fetch/transform logic tied to reputation; keep the existing structure but mark
it explicit and consider leaving a small note about limits (MAX_MOCK_HISTORY)
and expected types.
- Around line 67-77: Precompute normalized status sets to avoid calling
normalizeStatus repeatedly: map completedStatuses and pendingStatuses through
normalizeStatus once before the loop and store them in Sets (e.g.,
normalizedCompletedStatuses, normalizedPendingStatuses), then inside the loop
normalize only the current item's status and check membership with Set.has(...)
instead of using array.some(... normalizeStatus ...); update all occurrences
that currently call normalizeStatus within .some (including the repeated
instances around completedStatuses/pendingStatuses) to use these precomputed
Sets.

In `@components/reputation/earnings-summary.tsx`:
- Around line 6-15: Rename one of the colliding identifiers so imports are
unambiguous: either rename the type EarningsSummary to EarningsSummaryData
(update the export type declaration) or rename the React component
EarningsSummary to EarningsSummaryCard (update the component declaration and its
export). Ensure all references in this file (the exported type and the component
name) and any imports in consumers like page.tsx are updated accordingly to use
the new symbol name.
- Around line 22-23: The ternary assigning currencySymbol is redundant: replace
the expression using earnings.currency === "USDC" ? "USDC" : earnings.currency
with either a direct assignment const currencySymbol = earnings.currency or
implement a proper mapping from currency codes to display symbols (e.g.,
map["USD"] = "$", map["USDC"] = "USDC") and use that mapping to set
currencySymbol; update uses of currencySymbol accordingly in the
earnings-summary component.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/profile/`[userId]/page.tsx:
- Line 42: The UI shows incorrect earnings while bounties are still loading
because you only read loading/error from useContributorReputation; update the
component to also destructure isLoading and error from useBounties
(bountyResponse) and then combine loading states (e.g., combinedLoading =
isLoadingFromBounties || isLoadingFromReputation) or guard the main render so
pendingAmount and payoutHistory aren’t derived from an empty userBounties until
bountyResponse resolves; change the render path that computes userBounties,
pendingAmount, and payoutHistory so it waits for the combined loading state (or
shows a loading/skeleton) instead of using the reputation fallback branch
prematurely.
- Around line 111-113: When userBounties is empty the code only sets totalEarned
from reputation.stats but never sets pendingAmount, so populate pendingAmount
from the reputation object if present (e.g., reputation.stats.pendingEarnings or
reputation.stats.pendingAmount) by checking for its existence before assignment;
if the reputation API truly has no pending field today add a clear comment next
to the fallback (referencing userBounties, reputation, totalEarned,
pendingAmount) explaining the omission is intentional to avoid future confusion.
- Around line 88-96: The code currently increments totalEarned when status ===
"completed" even if bounty.claimExpiresAt is missing, causing payoutHistory to
lack a matching entry; update the logic in the status === "completed" branch to
keep totals consistent by either (A) only add amount to totalEarned when
bounty.claimExpiresAt exists (move totalEarned += amount inside the
bounty.claimExpiresAt check), or (B) always push a payoutHistory entry and use a
clear fallback for date (e.g., date: bounty.claimExpiresAt ?? bounty.updatedAt
or null) so payoutHistory and totalEarned remain aligned; modify the handling
around totalEarned, payoutHistory, bounty.claimExpiresAt, and the status ===
"completed" check accordingly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/profile/[userId]/page.tsx (2)

44-44: Extract MAX_MOCK_HISTORY to module scope.

This constant is stable across renders and does not belong inside the component body.

♻️ Proposed refactor
+const MAX_MOCK_HISTORY = 50;
+
 export default function ProfilePage() {
   const params = useParams();
   const userId = params.userId as string;
   // ...
-  const MAX_MOCK_HISTORY = 50;
-
   const mockHistory = useMemo(() => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx at line 44, Move the MAX_MOCK_HISTORY constant
out of the React component body into module scope so it isn’t re-created on
every render; locate the constant named MAX_MOCK_HISTORY in page.tsx (inside the
component function) and declare it at the top of the module (above the component
definition), leaving all references inside the component unchanged.

128-138: Full-page skeleton over-blocks — users wait for bounties before seeing the reputation card.

Gating on isBountiesLoading alongside isLoading prevents rendering the entire page (including the reputation sidebar, History tab, and Analytics tab) until both API calls complete. Only the Claims tab content depends on bounty data.

Consider rendering the page once reputation data is ready, and isolating the loading indicator to the Claims tab:

♻️ Suggested approach
- if (isLoading || isBountiesLoading) {
+ if (isLoading) {
    return (
      <div className="container mx-auto py-8">
        ...
      </div>
    );
  }

Then in the Claims tab:

  <TabsContent value="claims" className="mt-6">
    <h2 className="text-xl font-bold mb-4">My Claims</h2>
    <div className="space-y-6">
-     <EarningsSummary earnings={earningsSummary} />
-     <MyClaims claims={myClaims} />
+     {isBountiesLoading ? (
+       <Skeleton className="h-48 w-full" />
+     ) : (
+       <>
+         <EarningsSummary earnings={earningsSummary} />
+         <MyClaims claims={myClaims} />
+       </>
+     )}
    </div>
  </TabsContent>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 128 - 138, The current top-level
loading check uses both isLoading and isBountiesLoading which blocks rendering
the whole page (including the reputation sidebar, History and Analytics tabs);
change the logic so only isLoading (reputation/user data) gates the full-page
skeleton and render the page layout when isLoading is false, then move the
isBountiesLoading gating into the Claims tab component (or the Claims tab render
branch) so only the Claims panel shows a skeleton/spinner while bounty data
loads; update references to isLoading, isBountiesLoading, and the Claims tab
render path accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/profile/`[userId]/page.tsx:
- Around line 46-73: Replace the synthetic mockHistory useMemo with one that
sources real claim/bounty records from the already-fetched bountyResponse:
filter bountyResponse.data for items where claimedBy === userId, map each record
to the same shape currently expected (id, bountyId, bountyTitle, projectName,
projectLogoUrl, difficulty, rewardAmount, rewardCurrency, claimedAt,
completedAt, completionTimeHours, maintainerRating, maintainerFeedback,
pointsEarned), compute derived fields (e.g., completionTimeHours from
timestamps, fallback values where fields are missing), limit the result to
MAX_MOCK_HISTORY, and return [] if bountyResponse is not present; keep the hook
dependence array updated (reputation, bountyResponse, userId) and preserve the
useMemo wrapper and the mockHistory variable name so consumers remain unchanged.
- Line 42: Destructure the error from the useBounties() call (e.g., const {
data: bountyResponse, isLoading: isBountiesLoading, error: bountiesError } =
useBounties()) and propagate bountiesError into the UI paths that currently read
bountyResponse?.data and compute userBounties (the client-side filter using
bounty.claimedBy === userId). Update the earnings display and claims tab UI to
check isBountiesLoading and bountiesError before rendering reputation/earnings
fallbacks—show an error state or message when bountiesError is present instead
of silently substituting data; keep the client-side filtering since
BountyListParams does not support claimedBy server-side.

---

Nitpick comments:
In `@app/profile/`[userId]/page.tsx:
- Line 44: Move the MAX_MOCK_HISTORY constant out of the React component body
into module scope so it isn’t re-created on every render; locate the constant
named MAX_MOCK_HISTORY in page.tsx (inside the component function) and declare
it at the top of the module (above the component definition), leaving all
references inside the component unchanged.
- Around line 128-138: The current top-level loading check uses both isLoading
and isBountiesLoading which blocks rendering the whole page (including the
reputation sidebar, History and Analytics tabs); change the logic so only
isLoading (reputation/user data) gates the full-page skeleton and render the
page layout when isLoading is false, then move the isBountiesLoading gating into
the Claims tab component (or the Claims tab render branch) so only the Claims
panel shows a skeleton/spinner while bounty data loads; update references to
isLoading, isBountiesLoading, and the Claims tab render path accordingly.

@0xdevcollins
Copy link
Contributor

Hola @Oluwatos94, Please resolve all the coderabbit review...

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
app/profile/[userId]/page.tsx (2)

126-136: Full-page skeleton unnecessarily blocks rendering the reputation sidebar.

isBountiesLoading can remain true long after reputation resolves, since useBounties() fetches every bounty and filters client-side. Tying the entire page skeleton to both loading states hides the ReputationCard unnecessarily.

Consider rendering the page skeleton only on isLoading, and showing per-tab skeleton/placeholder within each TabsContent while isBountiesLoading is true:

♻️ Sketch of the decoupled approach
-  if (isLoading || isBountiesLoading) {
+  if (isLoading) {
     return (
       <div className="container mx-auto py-8">
         ...full-page skeleton...
       </div>
     );
   }

Then inside the tabs that depend on bounty data:

  <TabsContent value="history" className="mt-6">
    <h2 className="text-xl font-bold mb-4">Activity History</h2>
+   {isBountiesLoading ? (
+     <Skeleton className="h-48 w-full" />
+   ) : bountiesError ? (
      <div>...error...</div>
    ) : (
      <CompletionHistory ... />
    )}
  </TabsContent>

Apply the same pattern to the "claims" tab.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 126 - 136, The current full-page
skeleton is gated by both isLoading and isBountiesLoading, which hides the
ReputationCard unnecessarily; change the top-level early return to only check
isLoading so the main layout (including ReputationCard) still renders when
reputation is ready, and move handling of isBountiesLoading into the per-tab
content: inside the TabsContent for the "bounties" and "claims" tabs render a
local skeleton/placeholder while isBountiesLoading is true (and render the
bounty list when false). Update references around isLoading, isBountiesLoading,
ReputationCard, and the TabsContent blocks so bounties-loading is scoped to the
tabs instead of blocking the whole page.

66-66: completionTimeHours can be derived from available timestamps instead of hardcoding 0.

Both b.claimedAt (line 64) and b.updatedAt (line 65) are already pulled from the bounty; the hours can be computed from their difference.

♻️ Proposed change
-        completionTimeHours: 0,
+        completionTimeHours:
+          b.claimedAt && b.updatedAt
+            ? Math.max(
+                0,
+                Math.round(
+                  (new Date(b.updatedAt).getTime() -
+                    new Date(b.claimedAt).getTime()) /
+                    (1000 * 60 * 60)
+                )
+              )
+            : 0,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx at line 66, Replace the hardcoded
completionTimeHours: 0 with a computed value derived from the bounty timestamps:
compute the difference between b.updatedAt and b.claimedAt (or vice-versa
depending on semantics) in hours (e.g., (new Date(b.updatedAt) - new
Date(b.claimedAt)) / 36e5), guard for missing/null timestamps and non-date
values, and assign that result to completionTimeHours so the rendered data uses
the actual elapsed hours; update the object where completionTimeHours is set
(the bounty mapping that references b.claimedAt and b.updatedAt) to use this
computed value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/profile/`[userId]/page.tsx:
- Around line 99-101: The loop currently increments pendingAmount for bounties
that are neither "completed" nor "in-review" (e.g., "claimed") but doesn't add a
matching payoutHistory entry, causing the Pending card to exceed the processing
rows; update the logic that handles bounty statuses (the code manipulating
pendingAmount and payoutHistory) to detect "claimed" bounties with a future
claimExpiresAt and push a corresponding "processing"/"processing" history item
into payoutHistory (use claimExpiresAt as the date so it appears
forward-looking) instead of only incrementing pendingAmount, ensuring the same
amount is represented in both pending total and payoutHistory.

---

Nitpick comments:
In `@app/profile/`[userId]/page.tsx:
- Around line 126-136: The current full-page skeleton is gated by both isLoading
and isBountiesLoading, which hides the ReputationCard unnecessarily; change the
top-level early return to only check isLoading so the main layout (including
ReputationCard) still renders when reputation is ready, and move handling of
isBountiesLoading into the per-tab content: inside the TabsContent for the
"bounties" and "claims" tabs render a local skeleton/placeholder while
isBountiesLoading is true (and render the bounty list when false). Update
references around isLoading, isBountiesLoading, ReputationCard, and the
TabsContent blocks so bounties-loading is scoped to the tabs instead of blocking
the whole page.
- Line 66: Replace the hardcoded completionTimeHours: 0 with a computed value
derived from the bounty timestamps: compute the difference between b.updatedAt
and b.claimedAt (or vice-versa depending on semantics) in hours (e.g., (new
Date(b.updatedAt) - new Date(b.claimedAt)) / 36e5), guard for missing/null
timestamps and non-date values, and assign that result to completionTimeHours so
the rendered data uses the actual elapsed hours; update the object where
completionTimeHours is set (the bounty mapping that references b.claimedAt and
b.updatedAt) to use this computed value.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
app/profile/[userId]/page.tsx (4)

20-32: "claimed" bounties with a future expiry silently collapse to "active" in myClaims.

When bounty.status === "claimed" but claimExpiresAt is still in the future, deriveBountyStatus returns "active". In myClaims, these show as "active" — indistinguishable from open bounties — even though the user is actively working on them. If MyClaims renders the status label, this is confusing UX.

🔧 Suggested fix — add a `"claimed"` variant
-function deriveBountyStatus(bounty: {
-  status: string;
-  claimExpiresAt?: string | null;
-}): "completed" | "in-review" | "active" {
+function deriveBountyStatus(bounty: {
+  status: string;
+  claimExpiresAt?: string | null;
+}): "completed" | "in-review" | "claimed" | "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 "claimed"; // still within claim window
   }
   return "active";
 }

You'll need to update MyClaim.status and the earningsSummary else-branch to handle "claimed" consistently (it should still count toward pendingAmount).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 20 - 32, deriveBountyStatus
currently maps "claimed" bounties with a future claimExpiresAt to "active",
which hides them in MyClaims; change deriveBountyStatus to return a distinct
"claimed" status when bounty.status === "claimed" and claimExpiresAt is a valid
future date, keep "in-review" for expired claims and "completed" for closed;
then update the MyClaim.type/enum (or wherever MyClaim.status is defined) to
include "claimed" and adjust the earningsSummary else-branch to treat "claimed"
like a pending claim (i.e., count toward pendingAmount) so the UI and summaries
consistently reflect claimed-but-not-yet-expired bounties.

127-127: bounty.rewardAmount ?? undefined is a no-op.

null ?? undefined and undefined ?? undefined both yield undefined, so the nullish coalescing adds no value. Either pass the raw value (relying on the MyClaim type) or use ?? 0 for consistency with how earningsSummary handles the same field.

-        rewardAmount: bounty.rewardAmount ?? undefined,
+        rewardAmount: bounty.rewardAmount ?? 0,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx at line 127, The expression
bounty.rewardAmount ?? undefined is redundant; update the assignment for
rewardAmount in the object (where MyClaim is assembled) to either pass
bounty.rewardAmount directly or, to match earningsSummary behavior, coalesce to
a default number like 0 (e.g., use bounty.rewardAmount ?? 0) so the value is
meaningful and consistent with the MyClaim/earningsSummary handling.

131-141: Full-page skeleton blocks the reputation card while bounties load.

isLoading || isBountiesLoading means if reputation resolves quickly (a fast per-user call) but the broader bounties fetch is slow, users see a blank skeleton instead of the profile card. The two fetches are independent; consider showing the ReputationCard as soon as isLoading clears and rendering per-tab skeletons for the bounty-dependent content.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 131 - 141, The current check uses
a combined condition (isLoading || isBountiesLoading) to render a full-page
skeleton, which hides the ReputationCard even when reputation data (isLoading)
is ready; change the rendering so ReputationCard is shown when isLoading is
false regardless of isBountiesLoading, and only render skeleton placeholders
where bounty-dependent content lives (e.g., the grid that contains bounties)
while isBountiesLoading is true; update the conditional logic around the
ReputationCard and the bounty grid in page.tsx so that ReputationCard (and any
other user-level sections) render when isLoading is false and only the bounty
area checks isBountiesLoading.

64-70: completionTimeHours and pointsEarned are always 0, producing misleading display values.

completionTimeHours can be approximated from the available timestamps; hardcoding 0 will display "0 h" for every record if CompletionHistory renders it.

♻️ Derive completionTimeHours from timestamps
-        completionTimeHours: 0,
-        maintainerRating: null,
-        maintainerFeedback: null,
-        pointsEarned: 0,
+        completionTimeHours:
+          b.claimedAt && b.updatedAt
+            ? Math.max(
+                0,
+                Math.round(
+                  (new Date(b.updatedAt).getTime() -
+                    new Date(b.claimedAt).getTime()) /
+                    (1000 * 60 * 60)
+                )
+              )
+            : 0,
+        maintainerRating: null,
+        maintainerFeedback: null,
+        pointsEarned: 0, // not exposed by API yet
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 64 - 70, The code is hardcoding
completionTimeHours and pointsEarned to 0; compute completionTimeHours from
timestamps and avoid misleading zero points. Replace the static values with:
claimedAt = b.claimedAt ?? b.createdAt, completedAt = b.updatedAt, then compute
completionTimeHours = Math.max(0, (new Date(completedAt).getTime() - new
Date(claimedAt).getTime()) / (1000 * 60 * 60)) (round as desired), and set
pointsEarned to an actual source if available (e.g. b.pointsEarned ?? b.points)
or null if unknown instead of 0; update the mapping that constructs the object
(the block using completionTimeHours, pointsEarned, claimedAt, completedAt)
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/profile/`[userId]/page.tsx:
- Around line 73-76: The earningsSummary calculation uses
reputation?.stats.earningsCurrency but then sums userBounties without checking
each bounty's b.rewardCurrency, which mixes denominations; update the logic in
earningsSummary (and the userBounties derivation) to either 1) filter
userBounties to only include b.rewardCurrency ===
(reputation?.stats.earningsCurrency ?? "USDC") before computing
totalEarned/pendingAmount, or 2) aggregate totals grouped by b.rewardCurrency
(changing EarningsSummaryType to map currency→totals) so you accumulate
per-token amounts rather than summing mixed tokens.

---

Nitpick comments:
In `@app/profile/`[userId]/page.tsx:
- Around line 20-32: deriveBountyStatus currently maps "claimed" bounties with a
future claimExpiresAt to "active", which hides them in MyClaims; change
deriveBountyStatus to return a distinct "claimed" status when bounty.status ===
"claimed" and claimExpiresAt is a valid future date, keep "in-review" for
expired claims and "completed" for closed; then update the MyClaim.type/enum (or
wherever MyClaim.status is defined) to include "claimed" and adjust the
earningsSummary else-branch to treat "claimed" like a pending claim (i.e., count
toward pendingAmount) so the UI and summaries consistently reflect
claimed-but-not-yet-expired bounties.
- Line 127: The expression bounty.rewardAmount ?? undefined is redundant; update
the assignment for rewardAmount in the object (where MyClaim is assembled) to
either pass bounty.rewardAmount directly or, to match earningsSummary behavior,
coalesce to a default number like 0 (e.g., use bounty.rewardAmount ?? 0) so the
value is meaningful and consistent with the MyClaim/earningsSummary handling.
- Around line 131-141: The current check uses a combined condition (isLoading ||
isBountiesLoading) to render a full-page skeleton, which hides the
ReputationCard even when reputation data (isLoading) is ready; change the
rendering so ReputationCard is shown when isLoading is false regardless of
isBountiesLoading, and only render skeleton placeholders where bounty-dependent
content lives (e.g., the grid that contains bounties) while isBountiesLoading is
true; update the conditional logic around the ReputationCard and the bounty grid
in page.tsx so that ReputationCard (and any other user-level sections) render
when isLoading is false and only the bounty area checks isBountiesLoading.
- Around line 64-70: The code is hardcoding completionTimeHours and pointsEarned
to 0; compute completionTimeHours from timestamps and avoid misleading zero
points. Replace the static values with: claimedAt = b.claimedAt ?? b.createdAt,
completedAt = b.updatedAt, then compute completionTimeHours = Math.max(0, (new
Date(completedAt).getTime() - new Date(claimedAt).getTime()) / (1000 * 60 * 60))
(round as desired), and set pointsEarned to an actual source if available (e.g.
b.pointsEarned ?? b.points) or null if unknown instead of 0; update the mapping
that constructs the object (the block using completionTimeHours, pointsEarned,
claimedAt, completedAt) accordingly.

Comment on lines +73 to +76
const earningsSummary = useMemo<EarningsSummaryType>(() => {
const currency = reputation?.stats.earningsCurrency ?? "USDC";
const allBounties = bountyResponse?.data ?? [];
const userBounties = allBounties.filter((b) => b.claimedBy === userId);
Copy link

@coderabbitai coderabbitai bot Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Single currency silently sums bounties across mixed denominations.

currency is taken from reputation?.stats.earningsCurrency, but each bounty has its own b.rewardCurrency. If the platform ever hosts bounties in multiple tokens (ETH, DAI, etc.), totalEarned and pendingAmount would add up mismatched denominations without warning.

At minimum, consider filtering to only include bounties matching the resolved currency, or accumulate totals per currency:

 const currency = reputation?.stats.earningsCurrency ?? "USDC";
 const allBounties = bountyResponse?.data ?? [];
-const userBounties = allBounties.filter((b) => b.claimedBy === userId);
+const userBounties = allBounties.filter(
+  (b) => b.claimedBy === userId && (b.rewardCurrency ?? "USDC") === currency
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 73 - 76, The earningsSummary
calculation uses reputation?.stats.earningsCurrency but then sums userBounties
without checking each bounty's b.rewardCurrency, which mixes denominations;
update the logic in earningsSummary (and the userBounties derivation) to either
1) filter userBounties to only include b.rewardCurrency ===
(reputation?.stats.earningsCurrency ?? "USDC") before computing
totalEarned/pendingAmount, or 2) aggregate totals grouped by b.rewardCurrency
(changing EarningsSummaryType to map currency→totals) so you accumulate
per-token amounts rather than summing mixed tokens.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Oluwatos94 fix this and resolve conflict.

Once code rabbit approves your changes i will merge

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Earnings & Payout Tracking

2 participants