feat: Implement Earnings & Payout Tracking#97
feat: Implement Earnings & Payout Tracking#97Oluwatos94 wants to merge 7 commits intoboundlessfi:mainfrom
Conversation
|
@Oluwatos94 is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughIntegrates 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
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
components/reputation/earnings-summary.tsx (2)
6-15: Type and component share the same nameEarningsSummary, forcing awkward aliasing at import sites.In
page.tsx(line 14), the consumer must alias the type asEarningsSummaryTypeto 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:currencySymbolternary is a no-op — both branches return the same value.
earnings.currency === "USDC" ? "USDC" : earnings.currencyalways evaluates toearnings.currency. If the intent was to map currency codes to symbols (e.g.,"USD"→"$"), that mapping is missing. Otherwise, simplify toconst 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 twouseMemoblocks within the same render. Captureconst 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
myClaimsmemo.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:mockHistoryis 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// TODOcomment 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:normalizeStatusis called repeatedly on static string lists inside the loop — precompute once.
completedStatusesandpendingStatusesare constant arrays, yetnormalizeStatus(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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
app/profile/[userId]/page.tsx (2)
44-44: ExtractMAX_MOCK_HISTORYto 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
isBountiesLoadingalongsideisLoadingprevents 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.
|
Hola @Oluwatos94, Please resolve all the coderabbit review... |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
app/profile/[userId]/page.tsx (2)
126-136: Full-page skeleton unnecessarily blocks rendering the reputation sidebar.
isBountiesLoadingcan remain true long afterreputationresolves, sinceuseBounties()fetches every bounty and filters client-side. Tying the entire page skeleton to both loading states hides theReputationCardunnecessarily.Consider rendering the page skeleton only on
isLoading, and showing per-tab skeleton/placeholder within eachTabsContentwhileisBountiesLoadingis 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:completionTimeHourscan be derived from available timestamps instead of hardcoding0.Both
b.claimedAt(line 64) andb.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.
There was a problem hiding this comment.
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"inmyClaims.When
bounty.status === "claimed"butclaimExpiresAtis still in the future,deriveBountyStatusreturns"active". InmyClaims, these show as"active"— indistinguishable from open bounties — even though the user is actively working on them. IfMyClaimsrenders 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.statusand theearningsSummaryelse-branch to handle"claimed"consistently (it should still count towardpendingAmount).🤖 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 ?? undefinedis a no-op.
null ?? undefinedandundefined ?? undefinedboth yieldundefined, so the nullish coalescing adds no value. Either pass the raw value (relying on theMyClaimtype) or use?? 0for consistency with howearningsSummaryhandles 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 || isBountiesLoadingmeans 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 theReputationCardas soon asisLoadingclears 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:completionTimeHoursandpointsEarnedare always0, producing misleading display values.
completionTimeHourscan be approximated from the available timestamps; hardcoding0will display "0 h" for every record ifCompletionHistoryrenders 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.
| const earningsSummary = useMemo<EarningsSummaryType>(() => { | ||
| const currency = reputation?.stats.earningsCurrency ?? "USDC"; | ||
| const allBounties = bountyResponse?.data ?? []; | ||
| const userBounties = allBounties.filter((b) => b.claimedBy === userId); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@Oluwatos94 fix this and resolve conflict.
Once code rabbit approves your changes i will merge
There was a problem hiding this comment.
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!
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.
closes #84
Summary by CodeRabbit
New Features
Improvements