Skip to content

feat: implement private earnings dashboard with summary cards, source breakdown, activity feed, and claim action#410

Open
System625 wants to merge 1 commit intoboundlessfi:mainfrom
System625:feat/me-earnings-dashboard
Open

feat: implement private earnings dashboard with summary cards, source breakdown, activity feed, and claim action#410
System625 wants to merge 1 commit intoboundlessfi:mainfrom
System625:feat/me-earnings-dashboard

Conversation

@System625
Copy link

@System625 System625 commented Feb 25, 2026

Implement Private Earnings Dashboard (/me/earnings)

Description:

Implements the /me/earnings page as specified.

What was done

  • lib/api/types.ts — Added EarningSource, EarningStatus, EarningActivity, EarningSourceBreakdown, and EarningsData interfaces. Zero any types.
  • lib/api/user/earnings.ts — New API module following the lib/api/hackathons/rewards.ts pattern. Exports getUserEarnings() (GET /user/earnings) and claimEarning(id) (POST /user/earnings/:id/claim).
  • app/me/earnings/page.tsx — Client component with:
    • Skeleton loading state using existing Skeleton component patterns
    • Three summary cards: Total Earned, Pending Withdrawal, Completed Withdrawal
    • Earnings breakdown grid by source (Hackathons, Grants, Crowdfunding, Bounties)
    • Activity feed with per-item Claim button that calls the withdrawal endpoint and refreshes on success
    • Proper error state when the API is unavailable
  • components/app-sidebar.tsx — Added IconCurrencyDollar and an Earnings link after Analytics in the main nav section.
  • npm run build passes with no type errors. /me/earnings is confirmed in the route output.

Why no screenshot

The GET /user/earnings endpoint does not yet exist on the backend (staging or otherwise). The page renders its error state correctly when the API returns a non-success response — this is expected and intentional behavior. A screenshot/recording can be provided as soon as the backend implements the endpoint and returns data matching the EarningsData shape defined in lib/api/types.ts.
Closes #390

Summary by CodeRabbit

  • New Features
    • Earnings dashboard for authenticated users displaying total earned, pending withdrawals, and completed withdrawals with currency information
    • Earnings breakdown organized by source (Hackathons, Grants, Crowdfunding, Bounties) with per-source totals and pending amounts
    • Activity feed showing individual earning activities with status and claim functionality
    • Direct navigation access to earnings page from sidebar menu

@vercel
Copy link

vercel bot commented Feb 25, 2026

@System625 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 25, 2026

📝 Walkthrough

Walkthrough

Implements a new earnings dashboard feature for authenticated users at /me/earnings, including a React page component with summary cards and activity feed, sidebar navigation link, TypeScript type definitions for earnings data, and API client functions for fetching and claiming earnings.

Changes

Cohort / File(s) Summary
Earnings Page & Navigation
app/me/earnings/page.tsx, components/app-sidebar.tsx
New client-side earnings dashboard with summary cards, per-source breakdown, and activity feed with claim functionality; sidebar navigation link added with currency icon.
Earnings Type Definitions
lib/api/types.ts
New exported types: EarningSource (union), EarningStatus (union), EarningActivity, EarningSourceBreakdown, and EarningsData interfaces modeling earnings data structure.
Earnings API Helpers
lib/api/user/earnings.ts
New exported API functions getUserEarnings() and claimEarning(activityId) with response interfaces GetUserEarningsResponse and ClaimEarningResponse.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant EarningsPage as EarningsPage<br/>(React Component)
    participant API as Backend API
    participant DB as Database

    User->>EarningsPage: Visit /me/earnings
    activate EarningsPage
    EarningsPage->>API: GET /user/earnings
    activate API
    API->>DB: Query user earnings data
    activate DB
    DB-->>API: Return earnings breakdown & activities
    deactivate DB
    API-->>EarningsPage: GetUserEarningsResponse
    deactivate API
    EarningsPage->>EarningsPage: Render summary cards,<br/>breakdown, activity feed
    deactivate EarningsPage
    EarningsPage-->>User: Display earnings dashboard

    User->>EarningsPage: Click Claim button on activity
    activate EarningsPage
    EarningsPage->>EarningsPage: Show "Claiming..." state
    EarningsPage->>API: POST /user/earnings/{activityId}/claim
    activate API
    API->>DB: Update activity status to claimed
    activate DB
    DB-->>API: Confirmation
    deactivate DB
    API-->>EarningsPage: ClaimEarningResponse (success)
    deactivate API
    EarningsPage->>API: GET /user/earnings (refresh)
    activate API
    API->>DB: Query updated earnings data
    activate DB
    DB-->>API: Return updated data
    deactivate DB
    API-->>EarningsPage: GetUserEarningsResponse
    deactivate API
    EarningsPage->>EarningsPage: Update earnings state,<br/>refresh display
    deactivate EarningsPage
    EarningsPage-->>User: Show updated dashboard
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • Implement Private Earnings Dashboard #390 — These changes directly implement the /me/earnings dashboard requested in the issue, including the new page, sidebar navigation, earnings type definitions, and API client functions.
  • Implement Public Earnings Section in Profile #391 — The new earnings types (EarningsData, EarningActivity, etc.) and API client functions (getUserEarnings, claimEarning) introduced here provide the foundation and shared data models that a public profile earnings view would reuse with visibility filtering.

Poem

🐰 Earnings grow like carrots in the soil,
A dashboard blooms from developer's toil,
Claims spring forth with a hoppy delight,
Where breakdowns by source shine so bright! ✨💰

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing a private earnings dashboard with its key components (summary cards, source breakdown, activity feed, and claim action).

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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 (4)
lib/api/user/earnings.ts (1)

10-14: Use EarningStatus instead of string for status.

ClaimEarningData.status should reuse the existing EarningStatus union to keep the contract strict and consistent.

♻️ Proposed refactor
-import { ApiResponse, EarningsData } from '../types';
+import { ApiResponse, EarningsData, EarningStatus } from '../types';
@@
 export interface ClaimEarningData {
   activityId: string;
-  status: string;
+  status: EarningStatus;
   transactionHash?: string;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/user/earnings.ts` around lines 10 - 14, Update the ClaimEarningData
interface to use the existing EarningStatus union instead of string: change the
status property type to EarningStatus on the ClaimEarningData interface and add
the appropriate import for EarningStatus at the top of the file; ensure any code
creating or validating ClaimEarningData values is adjusted to use or accept
EarningStatus values (e.g., where ClaimEarningData is constructed/validated) so
types remain consistent with the EarningStatus union.
app/me/earnings/page.tsx (3)

27-78: Use typed const arrow components/functions to match repo standard.

This file introduces multiple function declarations in TSX; convert them to const arrow functions with explicit annotations for consistency with the codebase rule.

As per coding guidelines **/*.{ts,tsx}: Prefer const arrow functions with explicit type annotations over function declarations.

Also applies to: 80-143, 145-194, 196-310

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

In `@app/me/earnings/page.tsx` around lines 27 - 78, Change the plain function
declarations to typed const arrow components: replace "function
EarningsSkeleton() { ... }" with a const arrow form (for example "const
EarningsSkeleton: React.FC = () => { ... }" or "const EarningsSkeleton = ():
JSX.Element => { ... }"), and do the same for the other function components in
this file (the ones in ranges 80-143, 145-194, 196-310). Ensure each component
uses a const binding, an explicit return type annotation, and preserves the
original implementation and exported names.

13-25: Narrow map keys to domain unions for stronger type safety.

Record<string, string> weakens checks here. Prefer Record<EarningSource, string> and Record<EarningStatus, string> to enforce completeness and catch typos at compile time.

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

In `@app/me/earnings/page.tsx` around lines 13 - 25, SOURCE_LABELS and
STATUS_CLASSES are typed too broadly as Record<string,string>; change their
types to Record<EarningSource, string> and Record<EarningStatus, string>
respectively (import or reference the EarningSource and EarningStatus union
types used elsewhere) so the compiler enforces all keys and prevents typos, then
update the object literals to include every union member or adjust the unions if
a key is intentionally omitted; ensure any lookup sites still accept the
narrowed key types (or cast explicitly if necessary).

175-178: Prefer a class helper for conditional badge classes.

Inline string interpolation for conditional classes is harder to scale; use a class helper (clsx/cn) for consistency.

♻️ Proposed refactor
-<Badge
-  variant='outline'
-  className={`mt-0.5 text-xs ${STATUS_CLASSES[activity.status] ?? ''}`}
->
+<Badge
+  variant='outline'
+  className={cn('mt-0.5 text-xs', STATUS_CLASSES[activity.status])}
+>

As per coding guidelines **/*.{jsx,tsx}: For conditional classes, prefer clsx or similar helper functions over ternary operators in JSX.

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

In `@app/me/earnings/page.tsx` around lines 175 - 178, Replace the inline template
literal used for Badge's className with the project's class helper (e.g., cn or
clsx): change className={`mt-0.5 text-xs ${STATUS_CLASSES[activity.status] ??
''}`} to className={cn('mt-0.5 text-xs', STATUS_CLASSES[activity.status])} (or
the equivalent clsx call) so conditional classes come from the helper; update
the import if needed and keep references to STATUS_CLASSES and activity.status
intact and unchanged.
🤖 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/me/earnings/page.tsx`:
- Around line 203-211: The fetchEarnings success path doesn't clear a prior
error, so a past failure can keep the page in error state; inside fetchEarnings
after a successful getUserEarnings() and setEarnings(response.data) call, reset
the error state (e.g., call setError(null) or setError('')) so that error no
longer blocks the render logic that checks error || !earnings; ensure
setLoading(false) remains in finally.
- Around line 215-218: When the component detects that !isLoading && !authUser
and returns early it must also clear any private earnings state to avoid showing
stale data; update the unauthenticated branch (the conditional using isLoading
and authUser) to call the earnings state reset (e.g., setEarnings([]) or
setEarnings(null)) before setLoading(false) and return, and also clear any
related derived state (e.g., totalEarnings or pagination state) if present so
all private data is wiped when authUser becomes null.

In `@lib/api/user/earnings.ts`:
- Around line 24-27: The functions getUserEarnings and claimEarning currently
return res.data (the inner payload) but their declared return types
GetUserEarningsResponse and ClaimEarningResponse are the envelope
ApiResponse<T>; fix by returning the full response object (res) instead of
res.data so the returned value matches the envelope types, or alternatively
change the function return types to the raw data types (e.g., EarningsData /
ClaimEarningData) if you intentionally want to unwrap; update the return
statement in getUserEarnings and claimEarning accordingly and ensure
imports/types reflect the chosen shape.

---

Nitpick comments:
In `@app/me/earnings/page.tsx`:
- Around line 27-78: Change the plain function declarations to typed const arrow
components: replace "function EarningsSkeleton() { ... }" with a const arrow
form (for example "const EarningsSkeleton: React.FC = () => { ... }" or "const
EarningsSkeleton = (): JSX.Element => { ... }"), and do the same for the other
function components in this file (the ones in ranges 80-143, 145-194, 196-310).
Ensure each component uses a const binding, an explicit return type annotation,
and preserves the original implementation and exported names.
- Around line 13-25: SOURCE_LABELS and STATUS_CLASSES are typed too broadly as
Record<string,string>; change their types to Record<EarningSource, string> and
Record<EarningStatus, string> respectively (import or reference the
EarningSource and EarningStatus union types used elsewhere) so the compiler
enforces all keys and prevents typos, then update the object literals to include
every union member or adjust the unions if a key is intentionally omitted;
ensure any lookup sites still accept the narrowed key types (or cast explicitly
if necessary).
- Around line 175-178: Replace the inline template literal used for Badge's
className with the project's class helper (e.g., cn or clsx): change
className={`mt-0.5 text-xs ${STATUS_CLASSES[activity.status] ?? ''}`} to
className={cn('mt-0.5 text-xs', STATUS_CLASSES[activity.status])} (or the
equivalent clsx call) so conditional classes come from the helper; update the
import if needed and keep references to STATUS_CLASSES and activity.status
intact and unchanged.

In `@lib/api/user/earnings.ts`:
- Around line 10-14: Update the ClaimEarningData interface to use the existing
EarningStatus union instead of string: change the status property type to
EarningStatus on the ClaimEarningData interface and add the appropriate import
for EarningStatus at the top of the file; ensure any code creating or validating
ClaimEarningData values is adjusted to use or accept EarningStatus values (e.g.,
where ClaimEarningData is constructed/validated) so types remain consistent with
the EarningStatus union.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c584ac3 and aae1ff5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • app/me/earnings/page.tsx
  • components/app-sidebar.tsx
  • lib/api/types.ts
  • lib/api/user/earnings.ts

Comment on lines +203 to +211
const fetchEarnings = async () => {
try {
const response = await getUserEarnings();
setEarnings(response.data);
} catch {
setError('Failed to load earnings data.');
} finally {
setLoading(false);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear stale error state on successful fetch.

Line 208 sets error, but success path never resets it. Because Line 239 checks error || !earnings, a previous failure can keep the page in error state even after later successful responses.

🐛 Proposed fix
 const fetchEarnings = async () => {
+  setError(null);
   try {
     const response = await getUserEarnings();
     setEarnings(response.data);
   } catch {
     setError('Failed to load earnings data.');
   } finally {
     setLoading(false);
   }
 };

Also applies to: 239-249

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

In `@app/me/earnings/page.tsx` around lines 203 - 211, The fetchEarnings success
path doesn't clear a prior error, so a past failure can keep the page in error
state; inside fetchEarnings after a successful getUserEarnings() and
setEarnings(response.data) call, reset the error state (e.g., call
setError(null) or setError('')) so that error no longer blocks the render logic
that checks error || !earnings; ensure setLoading(false) remains in finally.

Comment on lines +215 to +218
if (!isLoading && !authUser) {
setLoading(false);
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reset earnings state when user becomes unauthenticated.

The unauthenticated branch returns early without clearing earnings. If auth state flips from signed-in to signed-out, stale private earnings data may remain visible.

🔒 Proposed fix
 if (!isLoading && !authUser) {
+  setEarnings(null);
   setLoading(false);
   return;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!isLoading && !authUser) {
setLoading(false);
return;
}
if (!isLoading && !authUser) {
setEarnings(null);
setLoading(false);
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/me/earnings/page.tsx` around lines 215 - 218, When the component detects
that !isLoading && !authUser and returns early it must also clear any private
earnings state to avoid showing stale data; update the unauthenticated branch
(the conditional using isLoading and authUser) to call the earnings state reset
(e.g., setEarnings([]) or setEarnings(null)) before setLoading(false) and
return, and also clear any related derived state (e.g., totalEarnings or
pagination state) if present so all private data is wiped when authUser becomes
null.

Comment on lines +24 to +27
export const getUserEarnings = async (): Promise<GetUserEarningsResponse> => {
const res = await api.get('/user/earnings');
return res.data;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lib/api/api.ts return signatures =="
rg -nP --type=ts 'export const api|get:\s*<T|post:\s*<T|clientApi\.(get|post)' lib/api/api.ts

echo
echo "== user API modules returning res vs res.data =="
rg -nP --type=ts -C2 'const res = await api\.(get|post)\(|return\s+res(\.data)?;' lib/api/user

Repository: boundlessfi/boundless

Length of output: 985


🏁 Script executed:

#!/bin/bash
# Get full content of api.ts to understand the structure
echo "== Content of lib/api/api.ts =="
head -100 lib/api/api.ts

echo
echo "== Type definitions for GetUserEarningsResponse and ClaimEarningResponse =="
rg -nP --type=ts 'GetUserEarningsResponse|ClaimEarningResponse' lib/api/user/earnings.ts -A3 -B1

Repository: boundlessfi/boundless

Length of output: 3929


🏁 Script executed:

#!/bin/bash
# Check if these response types are ApiResponse wrappers
echo "== Search for type definitions =="
rg -nP --type=ts 'type.*GetUserEarningsResponse|type.*ClaimEarningResponse|interface.*GetUserEarningsResponse|interface.*ClaimEarningResponse' lib/api/

echo
echo "== Check ApiResponse type definition =="
rg -nP --type=ts 'type ApiResponse|interface ApiResponse' lib/

Repository: boundlessfi/boundless

Length of output: 581


Fix response-shape mismatch: functions declare envelope types but return unwrapped data.

getUserEarnings and claimEarning are declared to return GetUserEarningsResponse and ClaimEarningResponse (which extend ApiResponse<T> with success and message fields), but both return res.data instead. Since api.get() and api.post() return Promise<ApiResponse<T>>, the code unwraps one level and returns only the inner data, creating a type error. Either return the full res object or change return types to the data types (EarningsData, ClaimEarningData).

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

In `@lib/api/user/earnings.ts` around lines 24 - 27, The functions getUserEarnings
and claimEarning currently return res.data (the inner payload) but their
declared return types GetUserEarningsResponse and ClaimEarningResponse are the
envelope ApiResponse<T>; fix by returning the full response object (res) instead
of res.data so the returned value matches the envelope types, or alternatively
change the function return types to the raw data types (e.g., EarningsData /
ClaimEarningData) if you intentionally want to unwrap; update the return
statement in getUserEarnings and claimEarning accordingly and ensure
imports/types reflect the chosen shape.

Copy link
Collaborator

@Benjtalkshow Benjtalkshow left a comment

Choose a reason for hiding this comment

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

@System625,

Thank you for your contribution. I appreciate the effort.

However, I advise that you wait until the issue is unblocked. Did you see my comment here?
#390 (comment)

Please wait until the user earnings data and claiming functionality are returned through a new endpoint. That is why the issue is currently blocked.

Right now, your implementation is based on assumptions because the response format has not been defined yet.

Thanks.

@System625
Copy link
Author

Hello @Benjtalkshow thank you for the information, that was my bad
But it seems a bounty hunter already made a PR on the issue and it has been merged by another maintainer
You can close this PR upon confirmation
Thank you

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 Private Earnings Dashboard

2 participants