-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Workspace and partner bounty updates #3164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Updated bounty card and modal components to show eligible partner groups for owners, including a tooltip with scrollable group list when multiple groups are present. Adjusted layout and conditional reward display logic to improve clarity and consistency for both owners and partners.
Moved the 'truncate' class to apply only on medium screens and above for the bounty name, improving text display on smaller devices.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughRemoved bounty reward-description UI from partner surfaces, tightened card/modal layouts, added owner-aware group visibility using useWorkspace/useGroups, introduced a scrollable tooltip component for multi-group lists, and adjusted counts, skeletons, and responsive placement across bounty components. Changes
Sequence DiagramsequenceDiagram
participant User
participant BountyUI as Bounty Component
participant WS as useWorkspace
participant Groups as useGroups
participant Tooltip as ScrollableTooltipContent
User->>BountyUI: open / view bounty card or modal
BountyUI->>WS: read workspace data (isOwner)
alt isOwner = true
BountyUI->>Groups: fetch groups
Groups-->>BountyUI: return groups list
BountyUI->>BountyUI: compute eligibleGroups (useMemo)
alt eligibleGroups.length = 0
BountyUI->>User: render "All groups"
else eligibleGroups.length = 1
BountyUI->>User: render single group + color dot
else eligibleGroups.length > 1
BountyUI->>User: render first group + tooltip trigger
User->>Tooltip: open tooltip
Tooltip->>Tooltip: attach ResizeObserver & scroll listener
Tooltip-->>User: show scrollable list with gradient indicators
end
else isOwner = false
BountyUI->>User: render reward description (non-owner path)
end
Estimated code review effortπ― 3 (Moderate) | β±οΈ ~25 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touchesβ Failed checks (1 warning)
β Passed checks (2 passed)
β¨ Finishing touches
π§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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 (2)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx (1)
107-123: Skeleton dimensions don't match the updated card layout.The main
PartnerBountyCardwas updated to usep-3andh-[124px], but the skeleton still usesp-5andh-[132px]. This will cause a visual jump when the skeleton is replaced by the actual card.export const PartnerBountyCardSkeleton = () => { return ( - <div className="border-border-subtle rounded-xl border bg-white p-5"> - <div className="flex flex-col gap-5"> - <div className="flex h-[132px] animate-pulse items-center justify-center rounded-lg bg-neutral-100 px-32 py-4" /> + <div className="border-border-subtle rounded-xl border bg-white p-3"> + <div className="flex flex-col gap-2.5"> + <div className="flex h-[124px] animate-pulse items-center justify-center rounded-lg bg-neutral-100 py-3" /> <div className="flex flex-col gap-1.5">apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx (1)
232-250: Skeleton dimensions inconsistent with card layout.Same issue as partner-bounty-card: skeleton uses
p-5andh-[132px]while the actual card usesp-2andh-[124px].export function BountyCardSkeleton() { return ( - <div className="border-border-subtle rounded-xl border bg-white p-5"> - <div className="flex flex-col gap-5"> - <div className="flex h-[132px] animate-pulse items-center justify-center rounded-lg bg-neutral-100 px-32 py-4" /> + <div className="border-border-subtle rounded-xl border bg-white p-2"> + <div className="flex flex-col gap-3.5"> + <div className="flex h-[124px] animate-pulse items-center justify-center rounded-lg bg-neutral-100 py-3" />
β»οΈ Duplicate comments (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (1)
20-83: DuplicateScrollableTooltipContentβ see comment inbounty-card.tsx.This is an identical copy. Extract to a shared module as suggested earlier.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx (1)
235-298: DuplicateScrollableTooltipContentβ see comment inbounty-card.tsx.This is the third copy. Extract to a shared module.
π§Ή Nitpick comments (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx (1)
151-214:ScrollableTooltipContentis duplicated across three files.This component is copy-pasted identically in
bounty-card.tsx,bounty-info.tsx, andconfirm-create-bounty-modal.tsx. Extract it to a shared location (e.g.,@/ui/partners/bounties/scrollable-tooltip-content.tsxor within@dub/ui) to eliminate maintenance burden and ensure consistency.Create a shared component:
// apps/web/ui/partners/bounties/scrollable-tooltip-content.tsx "use client"; import { GroupColorCircle } from "@/ui/partners/groups/group-color-circle"; import { useEffect, useRef, useState } from "react"; export function ScrollableTooltipContent({ groups, }: { groups: Array<{ id: string; name: string; color: string | null }>; }) { // ... existing implementation }Then import it in all three files instead of duplicating.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (1)
112-119: Consider narrowing useMemo dependency tobounty?.groups.The current dependency
[groups, bounty]will recompute when any bounty property changes. Since onlybounty.groupsis used, narrowing the dependency improves memoization efficiency:const eligibleGroups = useMemo(() => { if (!groups || !bounty || bounty.groups.length === 0) { return []; } return bounty.groups .map((bountyGroup) => groups.find((g) => g.id === bountyGroup.id)) .filter((g): g is NonNullable<typeof g> => g !== undefined); - }, [groups, bounty]); + }, [groups, bounty?.groups]);
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (5)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx(3 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx(5 hunks)apps/web/ui/partners/bounties/claim-bounty-modal.tsx(0 hunks)
π€ Files with no reviewable changes (1)
- apps/web/ui/partners/bounties/claim-bounty-modal.tsx
π§° Additional context used
π§ Learnings (8)
π Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx
π Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsxapps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx
π Learning: 2025-08-25T17:39:38.965Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx:45-56
Timestamp: 2025-08-25T17:39:38.965Z
Learning: In the bounty system, each partner can only submit to the same bounty once. This means totalSubmissions (pending + approved + rejected) equals the number of unique partners who have submitted, making UI text like "X of Y partners completed" accurate when using totalSubmissions.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx
π Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
π Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
π Learning: 2025-10-08T21:33:23.553Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2936
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/add-hostname-modal.tsx:28-34
Timestamp: 2025-10-08T21:33:23.553Z
Learning: In the dub/ui Button component, when the `disabledTooltip` prop is set to a non-undefined value (e.g., a string), the button is automatically disabled. Therefore, it's not necessary to also add the same condition to the `disabled` propβsetting `disabledTooltip={permissionsError || undefined}` is sufficient to disable the button when there's a permissions error.
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
π Learning: 2025-08-26T15:38:48.173Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/api/bounties/get-bounty-or-throw.ts:53-63
Timestamp: 2025-08-26T15:38:48.173Z
Learning: In bounty-related code, getBountyOrThrow returns group objects with { id } field (transformed from BountyGroup.groupId), while other routes working directly with BountyGroup Prisma records use the actual groupId field. This is intentional - getBountyOrThrow abstracts the join table details.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx
𧬠Code graph analysis (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx (4)
apps/web/lib/types.ts (1)
BountyListProps(596-596)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(7-48)apps/web/lib/swr/use-partners-count-by-groupids.ts (1)
usePartnersCountByGroupIds(4-38)apps/web/lib/partners/get-bounty-reward-description.ts (1)
getBountyRewardDescription(4-20)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (5)
apps/web/lib/swr/use-bounty.ts (1)
useBounty(7-23)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(7-48)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-action-button.tsx (1)
BountyActionButton(25-124)apps/web/lib/partners/get-bounty-reward-description.ts (1)
getBountyRewardDescription(4-20)packages/ui/src/tooltip.tsx (1)
Tooltip(67-118)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx (3)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(7-48)apps/web/lib/partners/get-bounty-reward-description.ts (1)
getBountyRewardDescription(4-20)packages/ui/src/tooltip.tsx (1)
Tooltip(67-118)
β° 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: build
π Additional comments (7)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx (1)
42-101: LGTM on layout and padding adjustments.The updated card layout with tighter padding (
p-3,gap-2.5) and adjusted thumbnail height (h-[124px]) aligns with the PR objectives for improved spacing and mobile responsiveness. Thesm:truncateon the title correctly allows text wrapping on mobile.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx (2)
23-30: LGTM on eligibleGroups computation.The memoization correctly depends on
groupsandbounty.groups, with proper null checks and type narrowing via the filter predicate.
74-144: LGTM on owner-conditional rendering.The conditional logic correctly:
- Shows reward description only for non-owners
- Shows group information only for owners
- Handles the three group states (none β "All groups", single β name with color, multiple β tooltip)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (1)
129-240: LGTM on responsive layout and owner-aware rendering.Good improvements:
- Mobile action button positioning (Lines 133-135) ensures accessibility on small screens
- Responsive text handling with
break-wordsandsm:truncate(Line 139)- Owner-conditional group display mirrors the card component pattern
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx (3)
68-75: LGTM oneligibleGroupsmemoization.This file correctly uses
bounty?.groupsas the dependency rather than the entirebountyobject, which is the ideal pattern. The other files should follow this approach.
100-163: LGTM on the card-like confirmation layout.The bounty preview section mirrors the card component design, providing visual consistency and an extra confirmation layer as intended by the PR objectives.
126-160: TheisOwnerconditional logic appears inconsistent with the modal's confirmation purpose.This is a bounty creation confirmation modal (shown only when creating new bounties, per the code in
add-edit-bounty-sheet.tsx). The current logic hides the reward description from workspace owners, showing only group eligibility instead:
!isOwner: Shows reward description viagetBountyRewardDescription(bounty)isOwner: Shows group eligibility informationIn a confirmation context where the user is about to create a bounty, the person performing the action should see the reward amount they configured, regardless of ownership status. The current implementation makes it impossible for owners to confirm the exact reward they're setting.
Suggested fix: Show both the reward description AND group eligibility information in the confirmation modal regardless of the
isOwnerstatus, or reconsider whether this conditional is needed at all.
|
@coderabbitai full review |
β Actions performedFull review triggered. |
There was a problem hiding this 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)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (1)
20-83:ScrollableTooltipContentduplicates logic from other bounty componentsThis implementation is effectively identical to the one in
bounty-card.tsxandconfirm-create-bounty-modal.tsx(same scrollRef, gradients, ResizeObserver pattern). Iβve suggested centralizing it in the bounty card file review; this instance would then just consume the shared helper.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx (1)
235-298: Third copy ofScrollableTooltipContentβ candidate for shared abstractionThis is the third identical
ScrollableTooltipContentimplementation (also present in the bounty card and bounty info files). Once youβre happy with the behavior, consider extracting it into a shared component and reusing it here to avoid divergence.
π§Ή Nitpick comments (4)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx (1)
107-123: Consider updating skeleton to match new card dimensions
PartnerBountyCardSkeletonstill uses the older padding and height (p-5, h-[132px]), so the loading state will no longer visually match the updated card (p-3, h-[124px]). Not a blocker, but worth aligning when you touch this file next.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx (2)
21-30: Owner group display logic is correct but could handle βdata not yet loadedβ more explicitly
eligibleGroupscorrectly mapsbounty.groupsids to full group objects fromuseGroups, and the owner-only UI (βAll groupsβ / single group / tooltip for many) matches the intended behavior described in the PR and prior learnings about group id usage. Based on learnings, this is the right way to reconcile{ id }-only bounty groups with full group records.While groups are still loading (
groupsundefined),eligibleGroupsis empty and the owner row renders only the Users6 icon with no text. Thatβs acceptable, but if you want a slightly clearer UX you might add a short βLoading groupsβ¦β placeholder or skeleton for that brief state.Also applies to: 121-145
151-214: DeduplicateScrollableTooltipContentinto a shared componentThe
ScrollableTooltipContentimplementation (scrollRef + gradients + ResizeObserver) is repeated in this file and again inbounty-info.tsxandconfirm-create-bounty-modal.tsx. Keeping three copies will make future tweaks (styles, behavior, accessibility) easy to miss in one place.Consider extracting this into a shared component (either in
@dub/uior a localbounties/scrollable-tooltip-contenthelper) and reusing it across bounty card, bounty info, and the confirm modal.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx (1)
54-76:eligibleGroupsmemo logic looks safe; dep array can be simplifiedThe
eligibleGroupsmemo correctly guards against!groupsand!bountybefore accessingbounty.groups, and mapping ids to full group records matches the pattern used in other bounty views. Using[groups, bounty?.groups]works, though[groups, bounty]would also be sufficient and a bit more conventional since you dereferencebountydirectly inside the memo.Not a required change, just a possible cleanup.
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (5)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx(3 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx(5 hunks)apps/web/ui/partners/bounties/claim-bounty-modal.tsx(0 hunks)
π€ Files with no reviewable changes (1)
- apps/web/ui/partners/bounties/claim-bounty-modal.tsx
π§° Additional context used
π§ Learnings (7)
π Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx
π Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
π Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
π Learning: 2025-10-08T21:33:23.553Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2936
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/add-hostname-modal.tsx:28-34
Timestamp: 2025-10-08T21:33:23.553Z
Learning: In the dub/ui Button component, when the `disabledTooltip` prop is set to a non-undefined value (e.g., a string), the button is automatically disabled. Therefore, it's not necessary to also add the same condition to the `disabled` propβsetting `disabledTooltip={permissionsError || undefined}` is sufficient to disable the button when there's a permissions error.
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
π Learning: 2025-08-26T15:38:48.173Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/api/bounties/get-bounty-or-throw.ts:53-63
Timestamp: 2025-08-26T15:38:48.173Z
Learning: In bounty-related code, getBountyOrThrow returns group objects with { id } field (transformed from BountyGroup.groupId), while other routes working directly with BountyGroup Prisma records use the actual groupId field. This is intentional - getBountyOrThrow abstracts the join table details.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx
π Learning: 2025-08-25T17:39:38.965Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx:45-56
Timestamp: 2025-08-25T17:39:38.965Z
Learning: In the bounty system, each partner can only submit to the same bounty once. This means totalSubmissions (pending + approved + rejected) equals the number of unique partners who have submitted, making UI text like "X of Y partners completed" accurate when using totalSubmissions.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx
π Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx
𧬠Code graph analysis (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (5)
apps/web/lib/swr/use-bounty.ts (1)
useBounty(7-23)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(7-48)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-action-button.tsx (1)
BountyActionButton(25-124)apps/web/lib/partners/get-bounty-reward-description.ts (1)
getBountyRewardDescription(4-20)packages/ui/src/tooltip.tsx (1)
Tooltip(67-118)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx (3)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(7-48)apps/web/lib/partners/get-bounty-reward-description.ts (1)
getBountyRewardDescription(4-20)packages/ui/src/tooltip.tsx (1)
Tooltip(67-118)
π Additional comments (6)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx (1)
47-51: Card layout/date row changes look consistentThe reduced padding, adjusted thumbnail height, and new Calendar6-based date row align with the other bounty cards and donβt introduce functional risk. Interactions (disabled state, tooltip, click handler) are preserved.
Also applies to: 53-69, 95-100
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (2)
87-120: Partner counts text matches single-submission-per-partner modelThe derivation of
totalSubmissionsfromsubmitted + approvedand its use in the β0 / All / X of Y partners completed/submitted (N awaiting review)β copy is consistent with the documented constraint that each partner can submit to a bounty at most once, so total submissions equal unique partners. The new All/X-of-Y messaging is clear and accurate under that model.No changes needed here; just ensure that
usePartnersCountByGroupIdsmaintains a numerictotalPartnerswhentotalPartnersForBountyLoadingis false, as in the bounty card comment.Also applies to: 163-207
130-137: Owner vs non-owner reward/group visibility is consistent with the new UXHiding the reward description for owners while showing them eligible groups, and keeping the reward visible for non-owners, aligns this detail view with the updated card and confirm modal flows described in the PR. The mobile and desktop
BountyActionButtonplacement also looks correct for responsive layouts.Also applies to: 154-162, 211-235, 237-239
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx (2)
100-163: Confirmation card preview matches the updated bounty card UXThe embedded card (thumbnail, dates, conditional reward vs groups row) mirrors the main bounty card behavior, giving users an accurate preview before creation. Owner-only groups visibility and non-owner reward description conditions are consistent with the other components.
62-64: Partner notification count label correctly reflects selected groupsUsing
usePartnersCountByGroupIdswithgroupIds: bounty?.groups?.map((group) => group.id) ?? []ensures the βSend notification to N selected partnersβ copy is scoped to the same groups represented in the preview card. The loading skeleton and pluralization make the label robust for 0, 1, and many partners.Also applies to: 188-211
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx (1)
17-21: The review comment is incorrect;usePartnersCountByGroupIdsguaranteestotalPartnersis always numeric.The hook implementation at lines 21β35 of
use-partners-count-by-groupids.tsuses the nullish coalescing operator (?? 0) on both code paths (lines 24 and 33), ensuringtotalPartnersis always a number (either 0 or a sum), never undefined. Even when the underlyingusePartnersCounthook encounters an error andloadingbecomes false withpartnersCount === undefined, the?? 0fallback safely converts this to0. ThenFormatterandpluralizeutilities then receive numeric values as expected, so there is no risk of misbehavior.Likely an incorrect or invalid review comment.
There was a problem hiding this 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
π§Ή Nitpick comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (1)
47-54: Potential issue with emptyeligibleGroupswhengroupsis still loading.When
groupsisundefined(still loading),eligibleGroupsreturns an empty array. This is then used in the owner section (lines 151-178) whereeligibleGroups.length === 1oreligibleGroups.length > 1would both be false, leading to renderingnull.Consider adding a loading state for groups similar to the partner count loading skeleton, or the UI will show nothing for the groups section while groups are loading.
+ {isOwner && !groups && ( + <div className="text-content-subtle font-regular flex items-center gap-2 text-sm"> + <Users6 className="size-4 shrink-0" /> + <span className="inline-block h-4 w-24 animate-pulse rounded bg-neutral-200" /> + </div> + )} + - {isOwner && ( + {isOwner && groups && ( <div className="text-content-subtle font-regular flex items-center gap-2 text-sm">apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx (1)
67-74: InconsistentuseMemodependency array.The dependency array uses
bounty?.groupsbut the memoization function accessesbounty.groupsdirectly. For consistency withbounty-info.tsxand clarity, consider usingbountyas the dependency:const eligibleGroups = useMemo(() => { if (!groups || !bounty || bounty.groups.length === 0) { return []; } return bounty.groups .map((bountyGroup) => groups.find((g) => g.id === bountyGroup.id)) .filter((g): g is NonNullable<typeof g> => g !== undefined); - }, [groups, bounty?.groups]); + }, [groups, bounty]);apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx (1)
14-14: UnusedisOwnervariable.
isOwneris destructured fromuseWorkspace()but never used in this component. If it's not needed for the card view (where groups are shown to all users), consider removing it to avoid confusion.- const { slug: workspaceSlug, isOwner } = useWorkspace(); + const { slug: workspaceSlug } = useWorkspace();
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx(4 hunks)packages/ui/src/tooltip.tsx(2 hunks)
π§° Additional context used
π§ Learnings (5)
π Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx
π Learning: 2025-08-26T15:38:48.173Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/api/bounties/get-bounty-or-throw.ts:53-63
Timestamp: 2025-08-26T15:38:48.173Z
Learning: In bounty-related code, getBountyOrThrow returns group objects with { id } field (transformed from BountyGroup.groupId), while other routes working directly with BountyGroup Prisma records use the actual groupId field. This is intentional - getBountyOrThrow abstracts the join table details.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx
π Learning: 2025-08-25T17:39:38.965Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx:45-56
Timestamp: 2025-08-25T17:39:38.965Z
Learning: In the bounty system, each partner can only submit to the same bounty once. This means totalSubmissions (pending + approved + rejected) equals the number of unique partners who have submitted, making UI text like "X of Y partners completed" accurate when using totalSubmissions.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx
π Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx
π Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx
𧬠Code graph analysis (2)
packages/ui/src/tooltip.tsx (1)
packages/ui/src/hooks/use-scroll-progress.ts (1)
useScrollProgress(6-37)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx (4)
apps/web/lib/types.ts (1)
BountyListProps(596-596)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(7-48)apps/web/lib/swr/use-partners-count-by-groupids.ts (1)
usePartnersCountByGroupIds(4-38)packages/ui/src/tooltip.tsx (2)
DynamicTooltipWrapper(208-222)ScrollableTooltipContent(224-278)
β° 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: build
π Additional comments (7)
packages/ui/src/tooltip.tsx (1)
224-278: Well-implemented scrollable tooltip content component.The component correctly:
- Uses the existing
useScrollProgresshook for scroll tracking- Shows/hides fade gradients based on scroll position
- Handles the edge case where content doesn't overflow (
canScrollcheck)One minor observation: the initial
scrollProgressvalue fromuseScrollProgressis1, which means on first render before any scroll event,showBottomGradientwill befalseeven if content overflows. This is handled correctly by theuseEffectthat runs whenscrollProgresschanges, and theuseResizeObserverin the hook will triggerupdateScrollProgresson mount.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (2)
100-132: LGTM!The partner count display logic correctly handles all edge cases: loading state, zero partners, all partners completed/submitted, and partial completion. The conditional rendering is consistent with the bounty card implementation.
146-180: LGTM!The owner-only groups section handles all cases well: "All groups" when no groups are specified, single group display, and multiple groups with a scrollable tooltip. The implementation is consistent with the bounty card component.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx (1)
125-168: LGTM!The owner-aware rendering logic is consistent with the bounty-info.tsx implementation. Non-owners see the reward description while owners see the eligible groups display with proper tooltip handling for multiple groups.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx (3)
111-152: LGTM!The groups display section correctly handles:
- "All groups" when no groups are assigned
- Single group display without tooltip
- Multiple groups with a scrollable tooltip
- Loading skeleton when groups data is not yet available
The use of
DynamicTooltipWrapperwith conditionaltooltipPropsis a clean pattern for optionally wrapping content with a tooltip.
76-107: LGTM!The partner count display logic is well-implemented with proper loading state handling and consistent messaging across different scenarios.
175-199: LGTM!The skeleton component is properly updated to match the new card layout structure with the correct number of rows and element sizes.
Updated bounty card and modal components to show eligible partner groups for owners, including a tooltip with scrollable group list when multiple groups are present. Adjusted layout and conditional reward display logic to improve clarity and consistency for both owners and partners.
Workspace card view
Workspace single bounty
Bounty creation confirmation
Replicating the card view here to add another confirmation layer
Partner bounty
Mobile views

Summary by CodeRabbit
New Features
UI/UX Improvements
Bug Fixes
βοΈ Tip: You can customize this high-level summary in your review settings.