Skip to content

feat: badges implementation frontend #1310

Merged
kushagrasarathe merged 17 commits intopeanut-wallet-devfrom
feat/achievement-badges
Oct 28, 2025
Merged

feat: badges implementation frontend #1310
kushagrasarathe merged 17 commits intopeanut-wallet-devfrom
feat/achievement-badges

Conversation

@kushagrasarathe
Copy link
Contributor

@kushagrasarathe kushagrasarathe commented Oct 10, 2025

  • contributes to TASK-15407 : badges implementation ui

@vercel
Copy link

vercel bot commented Oct 10, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
peanut-wallet Ready Ready Preview Comment Oct 28, 2025 9:38am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 10, 2025

Walkthrough

Adds badge UI and data model support: new badge components and utilities, profile/history integration, a mobile badges page, and campaignTag propagation through the claim/send-link flow; also minor UI/asset tweaks and interface/service updates to include badges.

Changes

Cohort / File(s) Summary
Badge UI components
src/components/Badges/index.tsx, src/components/Badges/BadgesRow.tsx, src/components/Badges/BadgeStatusItem.tsx, src/components/Badges/BadgeStatusDrawer.tsx
New components for badge listing, horizontally scrollable badge row with navigation/tooltips, history-status item, and a detail drawer.
Badge utilities
src/components/Badges/badge.utils.ts
Adds badge icon mapping, public descriptions, and helper functions (getBadgeIcon, getPublicBadgeDescription, BADGE_ICON_MAP).
Mobile badges page
src/app/(mobile-ui)/badges/page.tsx
New mobile route exporting page metadata and rendering the Badges view.
Profile integration
src/components/Profile/components/PublicProfile.tsx, src/components/Profile/index.tsx
Renders BadgesRow on public profile, adjusts layout and header (adds HandThumbsUpV2), and adds "Your Badges" profile menu item.
History integration
src/app/(mobile-ui)/history/page.tsx, src/components/Home/HomeHistory.tsx
Injects badge-earned entries into history lists and renders them via BadgeStatusItem alongside transactions/KYC items.
Data model & services
src/interfaces/interfaces.ts, src/services/users.ts
Adds optional badges array to User and ApiUser with badge metadata fields (code, name, description, iconUrl, color, earnedAt, isVisible).
Claim/send-link flow (campaignTag)
src/components/Claim/useClaimLink.tsx, src/components/Claim/Link/Initial.view.tsx, src/components/Claim/Link/Onchain/Confirm.view.tsx, src/components/Claim/Link/views/BankFlowManager.view.tsx, src/services/sendLinks.ts
Extracts campaignTag from URL where applicable and threads it through claim mutations/wrappers and API calls to support badge assignment.
Claim actions import cleanup
src/app/actions/claimLinks.ts
Removes unused fetch/json utilities from imports while keeping link resolution logic.
UI tweaks & assets
src/components/Home/InvitesIcon.tsx, src/components/Global/EmptyStates/EmptyState.tsx, src/components/ActionListCard/index.tsx, src/assets/illustrations/index.ts
Adds animate/className props to InvitesIcon with twMerge usage, adds horizontal padding to EmptyState, layout constraints to ActionListCard, and exports HandThumbsUpV2 SVG.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Areas needing extra attention:
    • claim/send-link campaignTag propagation across hooks, views, and service API signatures.
    • Badge rendering components with responsive/scrolling logic (BadgesRow) and modal/drawer interactions.
    • Data model changes (ApiUserUser) to ensure nullable/optional fields are handled safely in consumers.

Possibly related PRs

Suggested reviewers

  • Hugo0
  • Zishan-7
  • jjramirezn

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: badges implementation frontend" directly and clearly describes the primary purpose of the changeset. The pull request introduces a comprehensive badges feature for the frontend, including new components (BadgesRow, Badges, BadgeStatusItem, BadgeStatusDrawer), a dedicated badges page, badge utilities, integration into profile and history views, and necessary API modifications. The title uses conventional commit format, is concise, specific, and avoids vague terminology or noise.
Description Check ✅ Passed The description "contributes to TASK-15407 : badges implementation ui" is related to the changeset. It references a specific task identifier and indicates the purpose is implementing a badges UI feature, which aligns with the substantial frontend changes present in the pull request. While the description is minimal, it does convey meaningful information linking the work to a tracked task and confirming the badge implementation objective.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/achievement-badges

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

@coderabbitai coderabbitai bot added the enhancement New feature or request label Oct 10, 2025
Copy link
Contributor

@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: 5

🧹 Nitpick comments (3)
src/components/Badges/index.tsx (1)

69-69: Prefer cleaner rightContent handling.

Using <div className="hidden" /> to suppress the default rightContent is a workaround. Consider making rightContent truly optional in SearchResultCard or adding a hideRightContent prop.

src/components/Badges/BadgesRow.tsx (2)

64-73: Refine the viewport width calculation for accuracy.

The current formula Math.floor(width / itemWidth) is conservative but slightly underestimates capacity. For n items with an inter-item gap g, the total width is n * itemWidth - g. Solving for n given width:

n ≤ (width + gap) / itemWidth

With itemWidth = 64 and gap = 16, the calculation becomes Math.floor((width + 16) / 80). For example, a 384px viewport currently shows 4 badges but could fit 5.

Apply this diff to improve accuracy:

 const calculateVisibleCount = useCallback(() => {
     const width = viewportRef.current?.clientWidth || 0
-    const itemWidth = 80
+    const itemWidth = 64
+    const gap = 16
-    const count = Math.max(1, Math.floor(width / itemWidth))
+    const count = Math.max(1, Math.floor((width + gap) / (itemWidth + gap)))
     setVisibleCount(count)
     setStartIdx((prev) => Math.max(0, Math.min(prev, Math.max(0, sortedBadges.length - count))))
 }, [sortedBadges.length])

126-133: Consider conditionally applying the unoptimized prop.

The unoptimized prop skips Next.js image optimization, which is appropriate for external SVGs (like the Cloudinary mock URLs) but unnecessary for optimizable formats. If badge icons can be PNGs, JPGs, or internal assets, conditionally set unoptimized={badge.iconUrl?.endsWith('.svg')} to leverage optimization where possible.

Example implementation:

 <Image
     src={badge.iconUrl || '/logo-favicon.png'}
     alt={badge.name}
     className="min-h-12 min-w-12 object-contain"
     height={64}
     width={64}
-    unoptimized
+    unoptimized={badge.iconUrl?.endsWith('.svg') || !badge.iconUrl}
 />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2c49ba and bb20d27.

📒 Files selected for processing (9)
  • src/app/(mobile-ui)/badges/page.tsx (1 hunks)
  • src/components/Badges/BadgesRow.tsx (1 hunks)
  • src/components/Badges/index.tsx (1 hunks)
  • src/components/Global/EmptyStates/EmptyState.tsx (1 hunks)
  • src/components/Profile/components/PublicProfile.tsx (4 hunks)
  • src/components/Profile/index.tsx (1 hunks)
  • src/components/SearchUsers/SearchResultCard.tsx (1 hunks)
  • src/interfaces/interfaces.ts (1 hunks)
  • src/services/users.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/app/(mobile-ui)/badges/page.tsx (1)
src/components/Badges/index.tsx (1)
  • Badges (18-131)
src/components/Badges/index.tsx (5)
src/redux/hooks.ts (1)
  • useUserStore (13-13)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (13-28)
src/components/SearchUsers/SearchResultCard.tsx (1)
  • SearchResultCard (19-70)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (14-19)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (210-219)
src/components/Badges/BadgesRow.tsx (3)
src/components/Tooltip/index.tsx (1)
  • Tooltip (18-106)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (210-219)
src/components/Profile/components/PublicProfile.tsx (2)
src/services/users.ts (1)
  • usersApi (48-109)
src/components/Badges/BadgesRow.tsx (1)
  • BadgesRow (34-168)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (12)
src/components/Global/EmptyStates/EmptyState.tsx (1)

16-16: LGTM! Horizontal padding added for consistent spacing.

The px-4 addition aligns with the broader UI improvements for badge-related views while preserving existing vertical padding.

src/components/Profile/index.tsx (1)

71-71: LGTM! Badges menu item added correctly.

The new "Your Badges" menu entry follows the established pattern and properly links to the new /badges page.

src/components/SearchUsers/SearchResultCard.tsx (1)

45-47: LGTM! Flexbox sizing improved for text overflow prevention.

Adding min-w-0 and flex-1 ensures proper text truncation in flex containers, particularly for badge descriptions in the Badges UI.

src/app/(mobile-ui)/badges/page.tsx (1)

1-16: LGTM! Badges page wired correctly.

The page follows Next.js conventions with proper metadata and component structure.

src/components/Profile/components/PublicProfile.tsx (3)

22-22: LGTM! BadgesRow import added.

Proper integration of the new BadgesRow component.


39-47: LGTM! Badge state and rendering integrated correctly.

The profileBadges state is properly populated from apiUser.badges and rendered via BadgesRow. The type shapes are compatible for the fields used by BadgesRow.

Also applies to: 73-73, 203-204


58-72: LGTM! Refactored to use apiUser for clarity.

Renaming the parameter to apiUser and consolidating field access improves readability and consistency.

src/services/users.ts (1)

32-39: LGTM! ApiUser extended with badges.

The badges field is properly typed and aligns with the badge data used by UI components. Note the minor type inconsistency with User.badges flagged in the interfaces.ts review.

src/components/Badges/index.tsx (1)

35-53: LGTM! Component logic is sound.

The component correctly handles empty state rendering, badge mapping, modal interactions, and back navigation. The overall structure is clean and follows React best practices.

Also applies to: 55-130

src/components/Badges/BadgesRow.tsx (3)

34-37: LGTM! Clean state management.

The component correctly initializes visibleCount with a reasonable default (4) and startIdx at 0, preventing layout shifts on mount. The useRef for the viewport element is appropriate for DOM measurements without triggering re-renders.


75-95: LGTM! Robust lifecycle management.

The resize listener cleanup and startIdx reset logic are correct. The navigation handlers use useCallback appropriately to prevent effect churn, and the clamping logic ensures startIdx remains valid after viewport or badge-list changes.


105-167: LGTM! Accessible and well-structured rendering.

The JSX provides good accessibility with aria-label attributes, conditionally renders navigation buttons only when scrolling is possible, and includes helpful tooltips. The fallback for missing iconUrl and the responsive layout are well implemented.

Copy link
Contributor

@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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/Claim/Link/Initial.view.tsx (1)

154-255: Add campaignTag to useCallback dependencies.

The handleClaimLink callback references campaignTag (lines 178, 185) but doesn't include it in the dependency array (line 237). This could lead to stale closures where the callback uses an outdated campaignTag value if the URL parameter changes.

Apply this diff:

         [
             claimLinkData.link,
             claimLinkData.chainId,
             claimLinkData.tokenAddress,
             isPeanutWallet,
             fetchBalance,
             recipient.address,
             user,
             claimLink,
             claimLinkXchain,
             selectedTokenData,
             onCustom,
             setLoadingState,
             setClaimType,
             setTransactionHash,
             queryClient,
             isXChain,
+            campaignTag,
         ]
🧹 Nitpick comments (1)
src/components/Claim/Link/Initial.view.tsx (1)

175-179: Simplify null coalescing expressions.

The expressions campaignTag ?? undefined are redundant since campaignTag (extracted from params.get('campaign')) will already be null if not present, and passing null to an optional parameter is equivalent to passing undefined.

Simplify to:

-                        await sendLinksApi.autoClaimLink(
-                            user?.user.username ?? address,
-                            claimLinkData.link,
-                            campaignTag ?? undefined
-                        )
+                        await sendLinksApi.autoClaimLink(
+                            user?.user.username ?? address,
+                            claimLinkData.link,
+                            campaignTag || undefined
+                        )
-                        await sendLinksApi.claim(
-                            user?.user.username ?? address,
-                            claimLinkData.link,
-                            false,
-                            campaignTag ?? undefined
-                        )
+                        await sendLinksApi.claim(
+                            user?.user.username ?? address,
+                            claimLinkData.link,
+                            false,
+                            campaignTag || undefined
+                        )

Also applies to: 181-186

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb20d27 and 155bc83.

📒 Files selected for processing (4)
  • src/app/actions/claimLinks.ts (2 hunks)
  • src/app/api/auto-claim/route.ts (2 hunks)
  • src/components/Claim/Link/Initial.view.tsx (2 hunks)
  • src/services/sendLinks.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-24T13:26:10.290Z
Learnt from: Hugo0
PR: peanutprotocol/peanut-ui#1014
File: src/components/Claim/Link/Initial.view.tsx:413-413
Timestamp: 2025-07-24T13:26:10.290Z
Learning: In the peanut-ui repository, the change from `${SQUID_API_URL}/route` to `${SQUID_API_URL}/v2/route` in src/components/Claim/Link/Initial.view.tsx was a typo fix, not an API migration, as the codebase was already using Squid API v2.

Applied to files:

  • src/components/Claim/Link/Initial.view.tsx
📚 Learning: 2025-04-30T21:31:27.790Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#827
File: src/components/Claim/Link/Initial.view.tsx:120-126
Timestamp: 2025-04-30T21:31:27.790Z
Learning: The `sendLinksApi.claim` function in the Peanut Protocol UI accepts both username and wallet address as the first parameter.

Applied to files:

  • src/components/Claim/Link/Initial.view.tsx
🧬 Code graph analysis (2)
src/services/sendLinks.ts (2)
src/services/services.types.ts (1)
  • SendLink (350-392)
src/app/actions/claimLinks.ts (1)
  • claimSendLink (86-114)
src/components/Claim/Link/Initial.view.tsx (1)
src/services/sendLinks.ts (1)
  • sendLinksApi (37-225)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (4)
src/app/api/auto-claim/route.ts (1)

16-16: Consider input validation for user-provided campaignTag.

The campaignTag is extracted from the request body and forwarded directly to the Peanut API without validation or sanitization. While the backend likely performs its own validation, it's a good practice to validate or sanitize user-controlled input at the API boundary to prevent potential injection attacks or malformed data.

Consider adding basic validation:

 const { pubKey, recipient, password, campaignTag } = body

+// Validate campaignTag format if provided
+if (campaignTag !== undefined && (typeof campaignTag !== 'string' || campaignTag.length > 100)) {
+    return NextResponse.json(
+        { error: 'Invalid campaignTag format' },
+        { status: 400 }
+    )
+}
+
 if (!pubKey || !recipient || !password) {

Alternatively, verify that the backend API properly validates this field by checking the API documentation or implementation.

Also applies to: 34-34

src/app/actions/claimLinks.ts (1)

86-104: LGTM! Clean integration of campaignTag parameter.

The function signature correctly accepts the optional campaignTag and forwards it to the API payload. The implementation is consistent with the existing parameter handling.

src/services/sendLinks.ts (2)

144-166: LGTM! Well-documented campaignTag integration.

The claim method correctly accepts the optional campaignTag parameter and passes it through to claimSendLink. The JSDoc comment helpfully explains its purpose for badge awarding.


168-203: LGTM! Consistent autoClaimLink implementation.

The autoClaimLink method correctly threads campaignTag through to the API route. The implementation is consistent with the claim method above.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (2)
src/components/Badges/BadgesRow.tsx (2)

39-45: Sort logic for undefined earnedAt needs correction.

As flagged in a previous review, treating undefined earnedAt as 0 (epoch start) makes those badges appear as the oldest entries, not the newest. The sorting should treat undefined as Date.now() to make them appear newest.


100-101: Pagination state in key causes unnecessary remounts.

As flagged in a previous review, including startIdx + idx in the key forces React to treat the same badge as a new element during pagination, causing unnecessary unmount/remount cycles and resetting component state.

🧹 Nitpick comments (1)
src/components/Home/HomeHistory.tsx (1)

88-101: Consider extracting badge entry creation to a shared utility.

The badge injection logic is correct but duplicated in src/app/(mobile-ui)/history/page.tsx (lines 70-83). For better maintainability, consider extracting this to a shared utility function.

Example utility:

// src/utils/badge.utils.ts
export function createBadgeHistoryEntries(badges: Badge[]) {
  return badges
    .filter((b) => b.earnedAt)
    .map((b) => ({
      isBadge: true,
      uuid: `badge-${b.code}-${new Date(b.earnedAt!).getTime()}`,
      timestamp: new Date(b.earnedAt!).toISOString(),
      code: b.code,
      name: b.name,
      description: b.description ?? undefined,
      iconUrl: b.iconUrl ?? undefined,
    }))
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 155bc83 and 86c144f.

📒 Files selected for processing (7)
  • src/app/(mobile-ui)/history/page.tsx (3 hunks)
  • src/components/Badges/BadgeStatusDrawer.tsx (1 hunks)
  • src/components/Badges/BadgeStatusItem.tsx (1 hunks)
  • src/components/Badges/BadgesRow.tsx (1 hunks)
  • src/components/Home/HomeHistory.tsx (3 hunks)
  • src/components/Home/InvitesIcon.tsx (1 hunks)
  • src/components/Profile/components/PublicProfile.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-15T14:47:26.891Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#857
File: src/hooks/useWebSocket.ts:77-82
Timestamp: 2025-05-15T14:47:26.891Z
Learning: The useWebSocket hook in src/hooks/useWebSocket.ts is designed to provide raw history entries, while the components using it (such as HomeHistory.tsx) are responsible for implementing deduplication logic based on UUID to prevent duplicate entries when combining WebSocket data with other data sources.

Applied to files:

  • src/components/Home/HomeHistory.tsx
🧬 Code graph analysis (6)
src/components/Profile/components/PublicProfile.tsx (2)
src/services/users.ts (1)
  • usersApi (48-109)
src/components/Badges/BadgesRow.tsx (1)
  • BadgesRow (33-151)
src/components/Home/HomeHistory.tsx (1)
src/components/Badges/BadgeStatusItem.tsx (2)
  • isBadgeHistoryItem (17-17)
  • BadgeStatusItem (19-73)
src/components/Badges/BadgeStatusItem.tsx (2)
src/components/Global/Card/index.tsx (1)
  • CardPosition (4-4)
src/components/Badges/BadgeStatusDrawer.tsx (1)
  • BadgeStatusDrawer (21-66)
src/components/Badges/BadgeStatusDrawer.tsx (2)
src/utils/general.utils.ts (1)
  • formatDate (934-946)
src/components/Payment/PaymentInfoRow.tsx (1)
  • PaymentInfoRow (17-83)
src/app/(mobile-ui)/history/page.tsx (1)
src/components/Badges/BadgeStatusItem.tsx (2)
  • isBadgeHistoryItem (17-17)
  • BadgeStatusItem (19-73)
src/components/Badges/BadgesRow.tsx (3)
src/components/Tooltip/index.tsx (1)
  • Tooltip (18-106)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (210-219)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (16)
src/components/Home/InvitesIcon.tsx (1)

5-20: LGTM!

The prop-driven enhancement is well-implemented. The default animate=true preserves backward compatibility, and twMerge correctly handles class merging.

src/app/(mobile-ui)/history/page.tsx (2)

70-83: LGTM!

The badge injection logic is well-structured:

  • Filters badges by earnedAt to ensure valid timestamps
  • Generates unique UUIDs combining badge code and timestamp
  • Properly maps badge fields to history entry format

186-187: LGTM!

The badge rendering branch is correctly integrated into the history rendering flow, using the type guard for type safety and maintaining consistency with the existing pattern.

src/components/Profile/components/PublicProfile.tsx (3)

39-47: LGTM!

The profileBadges state definition is well-typed and includes all required fields for badge display.


58-73: LGTM!

The badge data flow is well-implemented:

  • Clear variable naming with apiUser
  • Safe fallback to empty array
  • Proper state update in the effect

152-153: LGTM!

The BadgesRow integration is clean and properly positioned in the layout with the negative margin for spacing.

src/components/Home/HomeHistory.tsx (1)

320-323: LGTM!

The badge rendering logic is correctly integrated into the history flow.

src/components/Badges/BadgeStatusDrawer.tsx (3)

22-23: LGTM!

The earnedAt conversion and date formatting logic is safe and correctly handles optional values.


32-38: Verify empty string fallback for badge icon.

The iconUrl falls back to an empty string (badge.iconUrl ?? ''), which may cause a Next.js Image error if the badge has no icon. Consider using a placeholder image instead.

Apply this diff if you want to use a placeholder:

-                                <Image
-                                    src={badge.iconUrl ?? ''}
-                                    alt="Icon"
+                                <Image
+                                    src={badge.iconUrl ?? '/logo-favicon.png'}
+                                    alt={`${badge.name} icon`}
                                     className="size-full object-contain"
                                     width={160}
                                     height={160}
                                 />

Alternatively, conditionally render the Image only when iconUrl is truthy, or verify that the API always provides a valid iconUrl.


50-60: LGTM!

The info display and share functionality are well-implemented. The use of PaymentInfoRow is consistent with existing patterns, and the share text is appropriate.

src/components/Badges/BadgesRow.tsx (2)

49-78: LGTM!

The responsive width calculation, resize handling, and navigation logic are well-implemented:

  • Proper cleanup of event listeners
  • Bounds checking in navigation callbacks
  • Scroll position reset when badge list changes

88-149: LGTM!

The rendering logic is well-structured with proper accessibility attributes, conditional navigation buttons, and tooltip integration. The component provides a good user experience with responsive badge display.

src/components/Badges/BadgeStatusItem.tsx (4)

7-17: LGTM!

The BadgeHistoryEntry type definition and isBadgeHistoryItem type guard are well-designed. The discriminator pattern with isBadge: true provides type-safe history entry handling.


28-37: LGTM!

The badge memoization is appropriate and correctly depends on the entry object. The iconUrl fallback to undefined is safe.


48-54: Verify empty string fallback for badge icon.

Similar to BadgeStatusDrawer.tsx, the iconUrl falls back to an empty string (entry.iconUrl ?? ''), which may cause a Next.js Image error. Consider using a placeholder image for consistency.

Apply this diff to use a placeholder:

                 <Image
-                    src={entry.iconUrl ?? ''}
+                    src={entry.iconUrl ?? '/logo-favicon.png'}
                     alt={`${entry.name} icon`}
                     className="size-10 object-contain"
                     width={32}
                     height={32}
                 />

67-71: LGTM!

The InvitesIcon usage with animate=false demonstrates proper use of the new prop API, and the drawer integration is clean.

@kushagrasarathe kushagrasarathe changed the title [WIP] feat: achievement badges feat: badges implementation frontend Oct 15, 2025
@notion-workspace
Copy link

Frontend implementation

@Hugo0
Copy link
Contributor

Hugo0 commented Oct 16, 2025

@coderabbit_ai review this

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

@Hugo0: I'll review the changes in this PR.


🧠 Learnings used
Learnt from: Hugo0
PR: peanutprotocol/peanut-ui#594
File: src/components/Request/Pay/Views/Initial.view.tsx:188-213
Timestamp: 2024-12-31T15:08:15.640Z
Learning: Always resolve coderabbit comments when reviewing code, since they can contain useful suggestions.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.


// public-facing descriptions for badges (third-person perspective)
const PUBLIC_DESCRIPTIONS: Record<string, string> = {
BETA_TESTER: `They broke things so others don't have to. Welcome to the chaos club — Beta Tester badge unlocked.`,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Hugo0 3rd person pov copies here, lmk if looks good?

Copy link
Contributor

Choose a reason for hiding this comment

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

These suck a bit :(

Copy changes rec:

BETA_TESTER: They broke things so others don't have to. Welcome to the chaos club.
OG_2025_10_12: This is a real OG. They were with Peanut before it was cool.
DEVCONNECT_BA_2025: Not anon. Touched grass, shook hands, breathed the same air as Vitalik.
PRODUCT_HUNT: Hope Dealer. Their upvote felt like a VC term sheet!
MOST_RESTAURANTS_DEVCON: This person is a real gourmet!
BIG_SPENDER_5K: This person is a top spender.
MOST_PAYMENTS_DEVCON: Money Machine - They move money like it's light work. Most payments made!
MOST_INVITES: Onboarded more users than Coinbase ads!
BIGGEST_REQUEST_POT: High Roller or Master Beggar? They created the pot with the highest number of contributors.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (1)
src/components/Badges/BadgesRow.tsx (1)

40-47: Don’t mutate props; fix ordering for undefined earnedAt (treat as newest).

Current badges.sort(...) mutates the input array and defaults missing dates to epoch (oldest). Copy before sort and default to now.

Apply:

-const sortedBadges = useMemo(() => {
-    return badges.sort((a, b) => {
-        const at = a.earnedAt ? new Date(a.earnedAt).getTime() : 0
-        const bt = b.earnedAt ? new Date(b.earnedAt).getTime() : 0
-        return bt - at
-    })
-}, [badges])
+const sortedBadges = useMemo(() => {
+    const now = Date.now()
+    return [...badges].sort((a, b) => {
+        const at = a.earnedAt ? new Date(a.earnedAt).getTime() : now
+        const bt = b.earnedAt ? new Date(b.earnedAt).getTime() : now
+        return bt - at
+    })
+}, [badges])
🧹 Nitpick comments (6)
src/components/Profile/components/PublicProfile.tsx (3)

56-74: Add error handling and abort to the fetch effect.

Prevents state updates after unmount and surfaces failures gracefully.

Apply:

 useEffect(() => {
-    usersApi.getByUsername(username).then((apiUser) => {
+    let cancelled = false
+    usersApi
+      .getByUsername(username)
+      .then((apiUser) => {
+        if (cancelled) return
         if (apiUser?.fullName) setFullName(apiUser.fullName)
         if (
             apiUser?.bridgeKycStatus === 'approved' ||
             apiUser?.kycVerifications?.some((v) => v.status === MantecaKycStatus.ACTIVE)
         ) {
             setIsKycVerified(true)
         } else {
             setIsKycVerified(false)
         }
         // to check if the logged in user has sent money to the profile user,
         // we check the amount that the profile user has received from the logged in user.
-        if (apiUser?.totalUsdReceivedFromCurrentUser) {
-            setTotalSentByLoggedInUser(apiUser.totalUsdReceivedFromCurrentUser)
-        }
+        if (apiUser?.totalUsdReceivedFromCurrentUser != null) {
+            setTotalSentByLoggedInUser(String(apiUser.totalUsdReceivedFromCurrentUser))
+        }
         setProfileBadges(apiUser?.badges ?? [])
-    })
+      })
+      .catch(() => {
+        if (!cancelled) {
+          setIsKycVerified(false)
+          setProfileBadges([])
+        }
+      })
+    return () => {
+      cancelled = true
+    }
 }, [username])

69-71: Normalize totalSent type to number to avoid repeated Number() calls.

Store as number in state and compute once on assignment.

Apply:

-const [totalSentByLoggedInUser, setTotalSentByLoggedInUser] = useState<string>('0')
+const [totalSentByLoggedInUser, setTotalSentByLoggedInUser] = useState<number>(0)

And in the effect (shown above) set:

- setTotalSentByLoggedInUser(String(apiUser.totalUsdReceivedFromCurrentUser))
+ setTotalSentByLoggedInUser(Number(apiUser.totalUsdReceivedFromCurrentUser) || 0)

Then:

-const haveSentMoneyToUser = useMemo(() => Number(totalSentByLoggedInUser) > 0, [totalSentByLoggedInUser])
+const haveSentMoneyToUser = useMemo(() => totalSentByLoggedInUser > 0, [totalSentByLoggedInUser])

168-177: Prefer passing the static import directly to next/image instead of .src.

Passing the module object preserves automatic metadata; using .src is fine but unnecessary here.

Apply:

-<Image src={HandThumbsUpV2.src} alt="Join Peanut" width={20} height={20} />
+<Image src={HandThumbsUpV2} alt="Join Peanut" width={20} height={20} />
...
-    src={HandThumbsUpV2.src}
+    src={HandThumbsUpV2}
src/components/Badges/badge.utils.ts (2)

4-15: Strengthen types: freeze maps and export a BadgeCode union.

Improves safety and editor DX.

Apply:

-const CODE_TO_PATH: Record<string, string> = {
+export const CODE_TO_PATH = {
     BETA_TESTER: '/badges/beta_tester.svg',
     DEVCONNECT_BA_2025: '/badges/devconnect_2025.svg',
     PRODUCT_HUNT: '/badges/product_hunt.svg',
     OG_2025_10_12: '/badges/og_v1.svg',
     MOST_RESTAURANTS_DEVCON: '/badges/foodie.svg',
     BIG_SPENDER_5K: '/badges/big_spender.svg',
     MOST_PAYMENTS_DEVCON: '/badges/most_payments.svg',
     MOST_INVITES: '/badges/most_invites.svg',
     BIGGEST_REQUEST_POT: '/badges/biggest_request_pot.svg',
-}
+} as const
+
+export type BadgeCode = keyof typeof CODE_TO_PATH

And:

-const PUBLIC_DESCRIPTIONS: Record<string, string> = {
+const PUBLIC_DESCRIPTIONS = {
   ...
-}
+} as const

30-34: Type the return for next/image and support direct “/badges/...” inputs.

Ensures compatibility with Image src and allows pre-resolved paths.

Apply:

+import type { StaticImageData } from 'next/image'
+
-export function getBadgeIcon(code?: string) {
-    if (code && CODE_TO_PATH[code]) return CODE_TO_PATH[code]
-    // fallback to peanutman logo
-    return PEANUTMAN_LOGO
+export function getBadgeIcon(code?: string): string | StaticImageData {
+    if (code) {
+        // allow passing a pre-resolved public path like '/badges/foo.svg'
+        if (code.startsWith?.('/badges/')) return code
+        if (code in CODE_TO_PATH) return CODE_TO_PATH[code as keyof typeof CODE_TO_PATH]
+    }
+    return PEANUTMAN_LOGO
 }

Optional: if API provides iconUrl on the badge, consider a helper resolveBadgeIcon({ code, iconUrl }) that prefers iconUrl when present.

src/components/Badges/BadgesRow.tsx (1)

60-66: Optional: use ResizeObserver for more accurate viewport sizing.

Avoids global window listener and reacts to container changes (e.g., side panels).

Example:

useEffect(() => {
  const el = viewportRef.current
  if (!el) return
  const ro = new ResizeObserver(() => calculateVisibleCount())
  ro.observe(el)
  return () => ro.disconnect()
}, [calculateVisibleCount])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e850af and cbb82aa.

⛔ Files ignored due to path filters (1)
  • src/assets/illustrations/hand-thumbs-up-v2.svg is excluded by !**/*.svg
📒 Files selected for processing (4)
  • src/assets/illustrations/index.ts (1 hunks)
  • src/components/Badges/BadgesRow.tsx (1 hunks)
  • src/components/Badges/badge.utils.ts (1 hunks)
  • src/components/Profile/components/PublicProfile.tsx (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/Badges/BadgesRow.tsx (4)
src/components/Badges/badge.utils.ts (2)
  • getPublicBadgeDescription (37-40)
  • getBadgeIcon (30-34)
src/components/Tooltip/index.tsx (1)
  • Tooltip (18-106)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (210-219)
src/components/Profile/components/PublicProfile.tsx (2)
src/services/users.ts (1)
  • usersApi (48-109)
src/components/Badges/BadgesRow.tsx (1)
  • BadgesRow (35-160)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (2)
src/assets/illustrations/index.ts (1)

7-7: LGTM: new asset export wired.

HandThumbsUpV2 export looks correct and matches usage elsewhere.

src/components/Badges/BadgesRow.tsx (1)

108-125: Good: stable React keys.

Using key={badge.code} avoids remounts when paginating. If codes aren’t guaranteed unique, append a stable tie-breaker like earnedAt.

Please confirm badge.code is unique within a user’s badge list.

Copy link
Contributor

@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 (1)
src/components/Badges/BadgesRow.tsx (1)

116-123: Confirm the need for unoptimized on badge images.

The unoptimized flag disables Next.js automatic image optimization, which can impact performance for raster images. If badge icons are SVGs or already optimized, this is appropriate. Otherwise, removing the flag enables automatic optimization (WebP conversion, responsive sizing).

If these are SVGs, the current usage is fine. If they're PNGs/JPGs served from a CDN, test whether Next.js optimization improves load times and consider removing unoptimized.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbb82aa and 389de28.

📒 Files selected for processing (1)
  • src/components/Badges/BadgesRow.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/Badges/BadgesRow.tsx (4)
src/components/Badges/badge.utils.ts (2)
  • getPublicBadgeDescription (37-40)
  • getBadgeIcon (30-34)
src/components/Tooltip/index.tsx (1)
  • Tooltip (18-106)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (210-219)

Copy link
Contributor

@Hugo0 Hugo0 left a comment

Choose a reason for hiding this comment

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

no major issues, except for im worried about the history approach

maybe a bit more DRY

also, careful with merge conflicts!


// public-facing descriptions for badges (third-person perspective)
const PUBLIC_DESCRIPTIONS: Record<string, string> = {
BETA_TESTER: `They broke things so others don't have to. Welcome to the chaos club — Beta Tester badge unlocked.`,
Copy link
Contributor

Choose a reason for hiding this comment

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

These suck a bit :(

Copy changes rec:

BETA_TESTER: They broke things so others don't have to. Welcome to the chaos club.
OG_2025_10_12: This is a real OG. They were with Peanut before it was cool.
DEVCONNECT_BA_2025: Not anon. Touched grass, shook hands, breathed the same air as Vitalik.
PRODUCT_HUNT: Hope Dealer. Their upvote felt like a VC term sheet!
MOST_RESTAURANTS_DEVCON: This person is a real gourmet!
BIG_SPENDER_5K: This person is a top spender.
MOST_PAYMENTS_DEVCON: Money Machine - They move money like it's light work. Most payments made!
MOST_INVITES: Onboarded more users than Coinbase ads!
BIGGEST_REQUEST_POT: High Roller or Master Beggar? They created the pot with the highest number of contributors.

Copy link
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/Global/EmptyStates/EmptyState.tsx (1)

12-12: Fix typos in the comment.

The comment contains two typos: "dispalying" → "displaying" and "scneario" → "scenario".

Apply this diff:

-// EmptyState component - Used for dispalying when there's no data in a certain scneario and we want to inform users with a cta (optional)
+// EmptyState component - Used for displaying when there's no data in a certain scenario and we want to inform users with a cta (optional)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 389de28 and 35c461f.

📒 Files selected for processing (9)
  • src/app/(mobile-ui)/history/page.tsx (3 hunks)
  • src/app/actions/claimLinks.ts (1 hunks)
  • src/components/Claim/Link/Initial.view.tsx (8 hunks)
  • src/components/Claim/useClaimLink.tsx (8 hunks)
  • src/components/Global/EmptyStates/EmptyState.tsx (1 hunks)
  • src/components/Home/HomeHistory.tsx (3 hunks)
  • src/components/Profile/components/PublicProfile.tsx (7 hunks)
  • src/components/Profile/index.tsx (1 hunks)
  • src/components/SearchUsers/SearchResultCard.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/components/Profile/index.tsx
  • src/app/(mobile-ui)/history/page.tsx
  • src/components/Claim/Link/Initial.view.tsx
  • src/components/SearchUsers/SearchResultCard.tsx
  • src/components/Profile/components/PublicProfile.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-15T14:47:26.891Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#857
File: src/hooks/useWebSocket.ts:77-82
Timestamp: 2025-05-15T14:47:26.891Z
Learning: The useWebSocket hook in src/hooks/useWebSocket.ts is designed to provide raw history entries, while the components using it (such as HomeHistory.tsx) are responsible for implementing deduplication logic based on UUID to prevent duplicate entries when combining WebSocket data with other data sources.

Applied to files:

  • src/components/Home/HomeHistory.tsx
🧬 Code graph analysis (1)
src/components/Home/HomeHistory.tsx (1)
src/components/Badges/BadgeStatusItem.tsx (2)
  • isBadgeHistoryItem (18-18)
  • BadgeStatusItem (20-74)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (8)
src/components/Global/EmptyStates/EmptyState.tsx (1)

16-16: LGTM! Horizontal padding improves spacing.

The addition of px-4 provides appropriate horizontal spacing for the empty state content, preventing it from touching the container edges.

src/components/Home/HomeHistory.tsx (1)

362-365: LGTM! Badge rendering follows established patterns.

The badge history rendering logic is clean and consistent with the KYC item rendering pattern above it. The type guard ensures type safety, and the component receives the correct props.

src/app/actions/claimLinks.ts (1)

9-10: LGTM! Clean import refactor.

The removal of unused imports (fetchWithSentry, jsonParse, PEANUT_API_URL, SendLink) while retaining the necessary utilities aligns well with the refactor mentioned in the comments. The functions continue to work with the remaining imports.

src/components/Claim/useClaimLink.tsx (5)

89-136: LGTM! Well-structured campaignTag integration.

The campaignTag parameter is properly threaded through with:

  • Correct optional typing (campaignTag?: string)
  • Safe conditional spreading (...(campaignTag && { campaignTag }))
  • Clear documentation of the badge assignment use case
  • Consistent use of the postJson helper

268-308: LGTM! Mutation correctly propagates campaignTag.

The mutation function signature and implementation consistently handle the campaignTag parameter, passing it through to executeClaim as expected.


351-375: LGTM! Legacy wrapper maintains backward compatibility.

The claimLink wrapper correctly adds the optional campaignTag parameter while maintaining backward compatibility for existing callers that don't provide it.


442-479: LGTM! Cancellation flow includes campaignTag for consistency.

The cancelLinkAndClaim function properly threads campaignTag through to claimLink. The comment noting it's "usually not needed for cancellations" is helpful context.


141-183: Cross-chain claims lack campaignTag support—verify if intentional or missing feature.

The claimLinkXchain wrapper function and underlying claimLinkXChainMutation (lines 381–398, 313–342) do not accept or pass campaignTag, while the same-chain flow does. The executeClaimXChain function (lines 141–183) also omits this parameter entirely, and the backend payload sent to /claim-x-chain never includes it.

Since campaignTag is documented in same-chain code as "Optional campaign tag for badge assignment" but completely absent in the cross-chain flow, confirm whether this is:

  • Intentional: Badges awarded only for same-chain claims (if so, add a comment documenting this limitation)
  • Oversight: Cross-chain should support badges for consistency (if so, add campaignTag parameter throughout the cross-chain call stack)

Copy link
Contributor

@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: 0

♻️ Duplicate comments (2)
src/interfaces/interfaces.ts (1)

261-270: Align User.badges with ApiUser.badges type definition.

This concern was raised in a previous review and remains unresolved. The type mismatch between User.badges and ApiUser.badges could cause runtime issues:

  1. earnedAt is required here but optional in ApiUser.badges
  2. isVisible exists here but not in ApiUser.badges

Ensure the API contract matches this type definition, or update earnedAt to be optional and verify whether isVisible should be included.

src/components/Home/HomeHistory.tsx (1)

94-107: Address type safety and code quality issues from previous review.

This segment has multiple issues that were flagged in the previous review but remain unresolved:

  1. Type safety: The as any assertion on line 106 bypasses type checking and could hide bugs.
  2. Misleading comment: Line 94 mentions "newest first" but the forEach doesn't sort—sorting happens later at line 188.
  3. Redundant code: Using ?? undefined on lines 104-105 is unnecessary since optional properties default to undefined.
  4. Missing validation: No check that required fields (code, name) exist before creating entries.

Apply this diff to address all issues:

-                // inject badge entries using user's badges (newest first) and earnedAt chronology
+                // inject badge entries from user's badges
                 const badges = user?.user?.badges ?? []
-                badges.forEach((b) => {
-                    if (!b.earnedAt) return
+                badges
+                    .filter((b) => b.earnedAt && b.code && b.name)
+                    .forEach((b) => {
                     entries.push({
                         isBadge: true,
-                        uuid: b.id,
-                        timestamp: new Date(b.earnedAt).toISOString(),
+                        uuid: b.id ?? `badge-${b.code}-${new Date(b.earnedAt!).getTime()}`,
+                        timestamp: new Date(b.earnedAt!).toISOString(),
                         code: b.code,
                         name: b.name,
-                        description: b.description ?? undefined,
-                        iconUrl: b.iconUrl ?? undefined,
-                    } as any)
+                        description: b.description,
+                        iconUrl: b.iconUrl,
+                    })
                 })

Note: To fully remove the type assertion, define a proper union type for history entries that includes badge entries, or extend the existing HistoryEntry type to accommodate badge entries.

🧹 Nitpick comments (1)
src/components/Badges/badge.utils.ts (1)

29-33: Add explicit return type annotation.

While TypeScript can infer the return type, adding an explicit : string annotation improves readability and type safety.

Apply this diff:

-export function getBadgeIcon(code?: string) {
+export function getBadgeIcon(code?: string): string {
     if (code && CODE_TO_PATH[code]) return CODE_TO_PATH[code]
     // fallback to peanutman logo
     return PEANUTMAN_LOGO
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd25734 and 0f8afab.

📒 Files selected for processing (4)
  • src/app/(mobile-ui)/history/page.tsx (3 hunks)
  • src/components/Badges/badge.utils.ts (1 hunks)
  • src/components/Home/HomeHistory.tsx (3 hunks)
  • src/interfaces/interfaces.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(mobile-ui)/history/page.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-15T14:47:26.891Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#857
File: src/hooks/useWebSocket.ts:77-82
Timestamp: 2025-05-15T14:47:26.891Z
Learning: The useWebSocket hook in src/hooks/useWebSocket.ts is designed to provide raw history entries, while the components using it (such as HomeHistory.tsx) are responsible for implementing deduplication logic based on UUID to prevent duplicate entries when combining WebSocket data with other data sources.

Applied to files:

  • src/components/Home/HomeHistory.tsx
🧬 Code graph analysis (1)
src/components/Home/HomeHistory.tsx (1)
src/components/Badges/BadgeStatusItem.tsx (2)
  • isBadgeHistoryItem (18-18)
  • BadgeStatusItem (20-74)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (4)
src/components/Badges/badge.utils.ts (2)

17-27: LGTM! Descriptions align with approved feedback.

The badge descriptions match the approved copies from the previous review and maintain a consistent third-person perspective.


36-39: LGTM!

The function has proper type annotation and handles edge cases correctly.

src/components/Home/HomeHistory.tsx (2)

18-18: LGTM!

The import statement is clean and correct.


362-365: LGTM!

The badge rendering logic is clean, uses proper type guards, and follows the established pattern for special history items.

Copy link
Contributor

@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

♻️ Duplicate comments (1)
src/components/Home/HomeHistory.tsx (1)

94-107: The type safety and validation issues remain unresolved.

The issues flagged in the previous review are still present:

  1. UUID concern: Using b.id as the UUID assumes it exists and is unique. If b.id is undefined, multiple badge entries could have undefined UUIDs, breaking deduplication logic.
  2. Type safety: The as any assertion still bypasses type checking.
  3. Redundant code: ?? undefined is unnecessary since optional properties default to undefined.
  4. Missing validation: No checks ensure required fields (code, name, id) exist before creating entries.

Apply this diff to address these issues:

 // inject badge entries from user's badges
 const badges = user?.user?.badges ?? []
-badges.forEach((b) => {
-    if (!b.earnedAt) return
+badges
+    .filter((b) => b.earnedAt && b.id && b.code && b.name)
+    .forEach((b) => {
     entries.push({
         isBadge: true,
-        uuid: b.id,
+        uuid: b.id!,
         timestamp: new Date(b.earnedAt!).toISOString(),
         code: b.code,
         name: b.name,
-        description: b.description ?? undefined,
-        iconUrl: b.iconUrl ?? undefined,
-    } as any)
+        description: b.description,
+        iconUrl: b.iconUrl,
+    })
 })

Note: To fully remove the type assertion, define a proper union type for history entries that includes BadgeHistoryEntry.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f8afab and 4ca7d24.

📒 Files selected for processing (3)
  • src/app/(mobile-ui)/history/page.tsx (3 hunks)
  • src/components/ActionListCard/index.tsx (1 hunks)
  • src/components/Home/HomeHistory.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(mobile-ui)/history/page.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-15T14:47:26.891Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#857
File: src/hooks/useWebSocket.ts:77-82
Timestamp: 2025-05-15T14:47:26.891Z
Learning: The useWebSocket hook in src/hooks/useWebSocket.ts is designed to provide raw history entries, while the components using it (such as HomeHistory.tsx) are responsible for implementing deduplication logic based on UUID to prevent duplicate entries when combining WebSocket data with other data sources.

Applied to files:

  • src/components/Home/HomeHistory.tsx
🧬 Code graph analysis (1)
src/components/Home/HomeHistory.tsx (1)
src/components/Badges/BadgeStatusItem.tsx (2)
  • isBadgeHistoryItem (18-18)
  • BadgeStatusItem (20-74)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (1)
src/components/Home/HomeHistory.tsx (1)

362-365: LGTM!

The badge rendering logic correctly integrates badge entries into the history feed using the type guard and follows the established pattern for special history items (KYC, badges).

Copy link
Contributor

@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: 0

♻️ Duplicate comments (2)
src/components/Badges/index.tsx (2)

16-16: Add stable identifier to BadgeView type.

The BadgeView type lacks a stable identifier property (e.g., code) that can be used as a React key. Including this property would enable stable keys in the list rendering and prevent reconciliation issues.

Apply this diff to add the code property:

-type BadgeView = { title: string; description: string; logo: string | StaticImageData }
+type BadgeView = { code: string; title: string; description: string; logo: string | StaticImageData }

Then update the mapping on line 28-32 to include it:

 return raw.map((b) => ({
+    code: b.code,
     title: b.name,
     description: b.description || '',
     logo: getBadgeIcon(b.code),
 }))

67-67: Use stable key instead of array index.

Using the array index as the key prop can cause React to mismanage component state when badges are reordered or filtered. This issue was previously flagged but not yet addressed.

Once you've added code to the BadgeView type (as suggested in the earlier comment), apply this diff:

-                       key={idx}
+                       key={badge.code}
🧹 Nitpick comments (1)
src/components/Badges/index.tsx (1)

69-69: Simplify rightContent prop.

The hidden <div> is used to suppress the default chevron icon. Consider checking if the ActionListCard component accepts undefined or null for rightContent to hide it more cleanly.

If ActionListCard supports it, simplify to:

-                           rightContent={<div className="hidden" />}
+                           rightContent={undefined}

Otherwise, this approach is acceptable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ca7d24 and b6d8fad.

📒 Files selected for processing (1)
  • src/components/Badges/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/Badges/index.tsx (6)
src/redux/hooks.ts (1)
  • useUserStore (13-13)
src/components/Badges/badge.utils.ts (1)
  • getBadgeIcon (29-33)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (13-28)
src/components/ActionListCard/index.tsx (1)
  • ActionListCard (19-70)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (14-19)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (209-218)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (4)
src/components/Badges/index.tsx (4)

25-33: LGTM: Badge mapping is clean and correct.

The useMemo correctly derives the view model from the API data, and the previous type-cast issue has been resolved. The use of getBadgeIcon(b.code) is the right approach.


35-53: LGTM: Empty state implementation is clean.

The empty state provides clear guidance to users about how to earn badges, and the navigation is properly wired.


78-86: Verify necessity of unoptimized images.

The unoptimized prop disables Next.js automatic image optimization. This is acceptable if badge icons are dynamically loaded or already optimized, but confirm this is intentional.


96-130: LGTM: Modal implementation is solid.

The modal correctly displays badge details and properly manages state. The close handlers appropriately reset both isBadgeModalOpen and selectedBadge.

@kushagrasarathe kushagrasarathe merged commit c43a9e4 into peanut-wallet-dev Oct 28, 2025
5 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants