feat: badges implementation frontend #1310
feat: badges implementation frontend #1310kushagrasarathe merged 17 commits intopeanut-wallet-devfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 makingrightContenttruly optional inSearchResultCardor adding ahideRightContentprop.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. Fornitems with an inter-item gapg, the total width isn * itemWidth - g. Solving forngivenwidth:n ≤ (width + gap) / itemWidthWith
itemWidth = 64andgap = 16, the calculation becomesMath.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
unoptimizedprop 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 setunoptimized={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
📒 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-4addition 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
/badgespage.src/components/SearchUsers/SearchResultCard.tsx (1)
45-47: LGTM! Flexbox sizing improved for text overflow prevention.Adding
min-w-0andflex-1ensures 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
profileBadgesstate is properly populated fromapiUser.badgesand rendered viaBadgesRow. The type shapes are compatible for the fields used byBadgesRow.Also applies to: 73-73, 203-204
58-72: LGTM! Refactored to use apiUser for clarity.Renaming the parameter to
apiUserand consolidating field access improves readability and consistency.src/services/users.ts (1)
32-39: LGTM! ApiUser extended with badges.The
badgesfield is properly typed and aligns with the badge data used by UI components. Note the minor type inconsistency withUser.badgesflagged 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
visibleCountwith a reasonable default (4) andstartIdxat 0, preventing layout shifts on mount. TheuseReffor 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
useCallbackappropriately to prevent effect churn, and the clamping logic ensuresstartIdxremains valid after viewport or badge-list changes.
105-167: LGTM! Accessible and well-structured rendering.The JSX provides good accessibility with
aria-labelattributes, conditionally renders navigation buttons only when scrolling is possible, and includes helpful tooltips. The fallback for missingiconUrland the responsive layout are well implemented.
There was a problem hiding this comment.
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
handleClaimLinkcallback referencescampaignTag(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 ?? undefinedare redundant sincecampaignTag(extracted fromparams.get('campaign')) will already benullif not present, and passingnullto an optional parameter is equivalent to passingundefined.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
📒 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
campaignTagis 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
campaignTagand 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
claimmethod correctly accepts the optionalcampaignTagparameter and passes it through toclaimSendLink. The JSDoc comment helpfully explains its purpose for badge awarding.
168-203: LGTM! Consistent autoClaimLink implementation.The
autoClaimLinkmethod correctly threadscampaignTagthrough to the API route. The implementation is consistent with theclaimmethod above.
There was a problem hiding this comment.
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
undefinedearnedAtas0(epoch start) makes those badges appear as the oldest entries, not the newest. The sorting should treatundefinedasDate.now()to make them appear newest.
100-101: Pagination state in key causes unnecessary remounts.As flagged in a previous review, including
startIdx + idxin 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
📒 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=truepreserves backward compatibility, andtwMergecorrectly handles class merging.src/app/(mobile-ui)/history/page.tsx (2)
70-83: LGTM!The badge injection logic is well-structured:
- Filters badges by
earnedAtto 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
profileBadgesstate 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
BadgesRowintegration 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
earnedAtconversion and date formatting logic is safe and correctly handles optional values.
32-38: Verify empty string fallback for badge icon.The
iconUrlfalls 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
iconUrlis truthy, or verify that the API always provides a validiconUrl.
50-60: LGTM!The info display and share functionality are well-implemented. The use of
PaymentInfoRowis 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
BadgeHistoryEntrytype definition andisBadgeHistoryItemtype guard are well-designed. The discriminator pattern withisBadge: trueprovides type-safe history entry handling.
28-37: LGTM!The badge memoization is appropriate and correctly depends on the
entryobject. TheiconUrlfallback toundefinedis safe.
48-54: Verify empty string fallback for badge icon.Similar to
BadgeStatusDrawer.tsx, theiconUrlfalls 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
InvitesIconusage withanimate=falsedemonstrates proper use of the new prop API, and the drawer integration is clean.
|
@coderabbit_ai review this |
|
@Hugo0: I'll review the changes in this PR. 🧠 Learnings used✅ Actions performedReview triggered.
|
src/components/Badges/badge.utils.ts
Outdated
|
|
||
| // 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.`, |
There was a problem hiding this comment.
@Hugo0 3rd person pov copies here, lmk if looks good?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_PATHAnd:
-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 prefersiconUrlwhen 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
⛔ Files ignored due to path filters (1)
src/assets/illustrations/hand-thumbs-up-v2.svgis 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 likeearnedAt.Please confirm
badge.codeis unique within a user’s badge list.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/components/Badges/BadgesRow.tsx (1)
116-123: Confirm the need forunoptimizedon badge images.The
unoptimizedflag 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
📒 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)
Hugo0
left a comment
There was a problem hiding this comment.
no major issues, except for im worried about the history approach
maybe a bit more DRY
also, careful with merge conflicts!
src/components/Badges/badge.utils.ts
Outdated
|
|
||
| // 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.`, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
📒 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-4provides 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
campaignTagparameter 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
postJsonhelper
268-308: LGTM! Mutation correctly propagates campaignTag.The mutation function signature and implementation consistently handle the
campaignTagparameter, passing it through toexecuteClaimas expected.
351-375: LGTM! Legacy wrapper maintains backward compatibility.The
claimLinkwrapper correctly adds the optionalcampaignTagparameter while maintaining backward compatibility for existing callers that don't provide it.
442-479: LGTM! Cancellation flow includes campaignTag for consistency.The
cancelLinkAndClaimfunction properly threadscampaignTagthrough toclaimLink. The comment noting it's "usually not needed for cancellations" is helpful context.
141-183: Cross-chain claims lackcampaignTagsupport—verify if intentional or missing feature.The
claimLinkXchainwrapper function and underlyingclaimLinkXChainMutation(lines 381–398, 313–342) do not accept or passcampaignTag, while the same-chain flow does. TheexecuteClaimXChainfunction (lines 141–183) also omits this parameter entirely, and the backend payload sent to/claim-x-chainnever includes it.Since
campaignTagis 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
campaignTagparameter throughout the cross-chain call stack)
There was a problem hiding this comment.
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.badgesandApiUser.badgescould cause runtime issues:
earnedAtis required here but optional inApiUser.badgesisVisibleexists here but not inApiUser.badgesEnsure the API contract matches this type definition, or update
earnedAtto be optional and verify whetherisVisibleshould 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:
- Type safety: The
as anyassertion on line 106 bypasses type checking and could hide bugs.- Misleading comment: Line 94 mentions "newest first" but the forEach doesn't sort—sorting happens later at line 188.
- Redundant code: Using
?? undefinedon lines 104-105 is unnecessary since optional properties default to undefined.- 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
HistoryEntrytype 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
: stringannotation 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
📒 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.
There was a problem hiding this comment.
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:
- UUID concern: Using
b.idas the UUID assumes it exists and is unique. Ifb.idis undefined, multiple badge entries could have undefined UUIDs, breaking deduplication logic.- Type safety: The
as anyassertion still bypasses type checking.- Redundant code:
?? undefinedis unnecessary since optional properties default to undefined.- 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
📒 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).
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/components/Badges/index.tsx (2)
16-16: Add stable identifier to BadgeView type.The
BadgeViewtype 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
codeproperty:-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
keyprop 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
codeto theBadgeViewtype (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 theActionListCardcomponent acceptsundefinedornullforrightContentto hide it more cleanly.If
ActionListCardsupports 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
📒 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
useMemocorrectly derives the view model from the API data, and the previous type-cast issue has been resolved. The use ofgetBadgeIcon(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
unoptimizedprop 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
isBadgeModalOpenandselectedBadge.
Uh oh!
There was an error while loading. Please reload this page.