Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@marcusljf
Copy link
Collaborator

@marcusljf marcusljf commented Nov 27, 2025

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

CleanShot 2025-11-26 at 19 46 35@2x
  • Adjusted the padding on all elements in the card
  • Removed the "reward amount" since we're showing the amount twice
  • Added the groups eligible for the bounty without having to click into the bounty -> edit bounty
  • On hover of the group, shows all eligible groups if the count is > 1
CleanShot 2025-11-26 at 19 49 50@2x
  • on mobile the bounty.name wraps
CleanShot 2025-11-26 at 20 18 53@2x z

Workspace single bounty

  • added the eligible groups to match the card view
  • improved the mobile layout (see image)
  • replaced the lucide icons with the nucleo set
CleanShot 2025-11-26 at 20 20 57@2x CleanShot 2025-11-26 at 20 22 04@2x

Bounty creation confirmation

Replicating the card view here to add another confirmation layer

CleanShot 2025-11-26 at 20 23 56@2x

Partner bounty

  • Remove the reward line
  • Adjusted the padding the spacing throughout
  • Other mobile improvements to ensure the content isn't hidden
CleanShot 2025-11-26 at 20 24 15@2x

Mobile views
CleanShot 2025-11-26 at 20 25 03@2x

Summary by CodeRabbit

  • New Features

    • Owner-only group visibility: shows eligible groups (All/one/multiple) with color indicators and a tooltip list.
    • Scrollable tooltip with gradient scroll indicators for long group lists.
  • UI/UX Improvements

    • Refined card, modal and thumbnail spacing, sizing and responsive placement of action controls.
    • Improved partner/submission counts with loading and zero-state messaging and clearer date rendering.
  • Bug Fixes

    • Removed redundant reward description from select views for cleaner layouts.

✏️ Tip: You can customize this high-level summary in your review settings.

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.
@vercel
Copy link
Contributor

vercel bot commented Nov 27, 2025

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

Project Deployment Preview Updated (UTC)
dub Canceled Canceled Nov 27, 2025 9:32pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 27, 2025

Walkthrough

Removed 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

Cohort / File(s) Change Summary
Partner-facing bounty UI
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx, apps/web/ui/partners/bounties/claim-bounty-modal.tsx
Removed getBountyRewardDescription import and reward-description UI (Gift icon). Compacted layout/spacing (reduced gaps/padding), adjusted thumbnail height, title padding/truncation, and removed reward description block.
Dashboard bounty cards & info
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-info.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx
Added owner-aware behavior via useWorkspace (isOwner) and useGroups; compute eligibleGroups (useMemo) and render owner-specific group UI (All/one/many with tooltip). Reworked counts/loading states, adjusted layout, replaced date icon, and updated responsive placement of action button.
Bounty creation modal
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/confirm-create-bounty-modal.tsx
Added isOwner usage, fetch/use groups and eligibleGroups computation; owner-specific group UI (All / single with color / multi with ScrollableTooltipContent). Reworked modal layout and conditional reward vs group rendering.
New reusable tooltip content
packages/ui/src/tooltip.tsx
Added ScrollableTooltipContent export: scrollable container with maxHeight prop, scroll progress tracking, top/bottom fade gradients, and ResizeObserver/scroll listeners to show gradients when content overflows. Also added related useEffect/useRef imports and hook usage.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Focus areas:
    • packages/ui/src/tooltip.tsx β€” ResizeObserver/scroll listener lifecycles and cleanup.
    • useWorkspace usage β€” confirm isOwner is present across hook implementation and callers.
    • eligibleGroups filtering and null-safety (matching bounty.groups β†’ fetched groups).
    • Partner/submission count logic and loading skeleton correctness across responsive breakpoints.

Possibly related PRs

Poem

πŸ‡ I hopped through margins, trimmed a line or two,
Group dots gleam for owners, tooltips scroll anew,
Gifts tucked away for partner eyes no more,
Gradients whisper lists when the group count soars,
A tiny rabbit celebrates the UI that's light and true.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
βœ… Passed checks (2 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title 'Workspace and partner bounty updates' accurately reflects the main changes across workspace and partner bounty components, including UI adjustments, group eligibility display, and spacing refinements.
✨ 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 bounty-updates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

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

Copy link
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 (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 PartnerBountyCard was updated to use p-3 and h-[124px], but the skeleton still uses p-5 and h-[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-5 and h-[132px] while the actual card uses p-2 and h-[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: Duplicate ScrollableTooltipContent β€” see comment in bounty-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: Duplicate ScrollableTooltipContent β€” see comment in bounty-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: ScrollableTooltipContent is duplicated across three files.

This component is copy-pasted identically in bounty-card.tsx, bounty-info.tsx, and confirm-create-bounty-modal.tsx. Extract it to a shared location (e.g., @/ui/partners/bounties/scrollable-tooltip-content.tsx or 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 to bounty?.groups.

The current dependency [groups, bounty] will recompute when any bounty property changes. Since only bounty.groups is 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

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between beac3b7 and e3a2df1.

πŸ“’ 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.tsx
  • apps/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.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
  • apps/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.tsx
  • apps/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.tsx
  • apps/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. The sm:truncate on 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 groups and bounty.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-words and sm: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 on eligibleGroups memoization.

This file correctly uses bounty?.groups as the dependency rather than the entire bounty object, 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: The isOwner conditional 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 via getBountyRewardDescription(bounty)
  • isOwner: Shows group eligibility information

In 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 isOwner status, or reconsider whether this conditional is needed at all.

@steven-tey
Copy link
Collaborator

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 27, 2025

βœ… Actions performed

Full review triggered.

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)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (1)

20-83: ScrollableTooltipContent duplicates logic from other bounty components

This implementation is effectively identical to the one in bounty-card.tsx and confirm-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 of ScrollableTooltipContent – candidate for shared abstraction

This is the third identical ScrollableTooltipContent implementation (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

PartnerBountyCardSkeleton still 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

eligibleGroups correctly maps bounty.groups ids to full group objects from useGroups, 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 (groups undefined), eligibleGroups is 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: Deduplicate ScrollableTooltipContent into a shared component

The ScrollableTooltipContent implementation (scrollRef + gradients + ResizeObserver) is repeated in this file and again in bounty-info.tsx and confirm-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/ui or a local bounties/scrollable-tooltip-content helper) 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: eligibleGroups memo logic looks safe; dep array can be simplified

The eligibleGroups memo correctly guards against !groups and !bounty before accessing bounty.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 dereference bounty directly inside the memo.

Not a required change, just a possible cleanup.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between beac3b7 and e3a2df1.

πŸ“’ 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.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx
  • apps/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.tsx
  • apps/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.tsx
  • apps/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 consistent

The 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 model

The derivation of totalSubmissions from submitted + approved and 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 usePartnersCountByGroupIds maintains a numeric totalPartners when totalPartnersForBountyLoading is 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 UX

Hiding 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 BountyActionButton placement 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 UX

The 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 groups

Using usePartnersCountByGroupIds with groupIds: 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; usePartnersCountByGroupIds guarantees totalPartners is always numeric.

The hook implementation at lines 21–35 of use-partners-count-by-groupids.ts uses the nullish coalescing operator (?? 0) on both code paths (lines 24 and 33), ensuring totalPartners is always a number (either 0 or a sum), never undefined. Even when the underlying usePartnersCount hook encounters an error and loading becomes false with partnersCount === undefined, the ?? 0 fallback safely converts this to 0. The nFormatter and pluralize utilities then receive numeric values as expected, so there is no risk of misbehavior.

Likely an incorrect or invalid review comment.

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

🧹 Nitpick comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (1)

47-54: Potential issue with empty eligibleGroups when groups is still loading.

When groups is undefined (still loading), eligibleGroups returns an empty array. This is then used in the owner section (lines 151-178) where eligibleGroups.length === 1 or eligibleGroups.length > 1 would both be false, leading to rendering null.

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: Inconsistent useMemo dependency array.

The dependency array uses bounty?.groups but the memoization function accesses bounty.groups directly. For consistency with bounty-info.tsx and clarity, consider using bounty as 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: Unused isOwner variable.

isOwner is destructured from useWorkspace() 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

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between e3a2df1 and 2a3fa22.

πŸ“’ 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.tsx
  • apps/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.tsx
  • apps/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
  • apps/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.tsx
  • apps/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 useScrollProgress hook for scroll tracking
  • Shows/hides fade gradients based on scroll position
  • Handles the edge case where content doesn't overflow (canScroll check)

One minor observation: the initial scrollProgress value from useScrollProgress is 1, which means on first render before any scroll event, showBottomGradient will be false even if content overflows. This is handled correctly by the useEffect that runs when scrollProgress changes, and the useResizeObserver in the hook will trigger updateScrollProgress on 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 DynamicTooltipWrapper with conditional tooltipProps is 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.

@steven-tey steven-tey merged commit ab2e64f into main Nov 27, 2025
5 of 7 checks passed
@steven-tey steven-tey deleted the bounty-updates branch November 27, 2025 21:33
@coderabbitai coderabbitai bot mentioned this pull request Dec 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants