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

Skip to content

Conversation

@marcusljf
Copy link
Collaborator

@marcusljf marcusljf commented Sep 13, 2025

Instead of showing the payout connection status, show the rewards the partner is on.

CleanShot.2025-09-18.at.08.18.34.mp4

Summary by CodeRabbit

  • New Features

    • Partner rows show a tooltip with group-specific rewards and discount details when a group is available.
    • API now returns groups enriched with associated rewards and discounts, enabling richer displays across the UI.
  • Refactor

    • Removed the previous payouts status indicator from partner avatars.
    • Partner cells pass only minimal partner info (id, name, image) and include group context when available.
    • Analytics and Payouts tables preload groups but won’t show rewards where group context is absent.

Instead of showing the payout connection status, show the rewards the partner is on.
@vercel
Copy link
Contributor

vercel bot commented Sep 13, 2025

@marcusljf is attempting to deploy a commit to the Dub Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 13, 2025

Walkthrough

API GET /groups now enriches each group with click/lead/sale rewards and discount via per-group Prisma lookups. Frontend tables load groups via useGroups(), pass trimmed partner objects and a resolved group (or null) to PartnerRowItem, which now shows a rewards/discount tooltip instead of the old payouts UI.

Changes

Cohort / File(s) Summary of Changes
API: Enrich groups with rewards/discount
apps/web/app/(ee)/api/groups/route.ts
GET handler now queries Prisma per group (partnerGroup.findUnique include: clickReward, leadReward, saleReward, discount), merges those fields into each group concurrently (Promise.all), and returns extended groups parsed with GroupSchemaExtended.
UI Component: PartnerRowItem overhaul
apps/web/ui/partners/partner-row-item.tsx
Replaced payouts UI with a rewards-and-discount tooltip driven by an optional group prop. Added icons, formatting helpers, reward sorting (sale < lead < click), conditional tooltip rendering, and removed payoutsEnabledAt from partner type.
Dashboard tables: load groups & pass explicit props
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
Imported and invoked useGroups() in tables. Partner objects trimmed to { id, name, image }. partners-table resolves and passes group (lookup by groupId); analytics and payouts pass group={null} with comments explaining missing groupId.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant API as GET /api/groups
    participant DB as Prisma (partnerGroup)

    Client->>API: Request groups
    API->>DB: findMany(partnerGroups)
    DB-->>API: base groups[]
    rect rgba(200,220,255,0.18)
      note over API: For each group (concurrent)
      API->>DB: findUnique(groupId) include: click/lead/saleReward, discount
      DB-->>API: group rewards + discount
    end
    API-->>Client: groupsWithRewards[] (GroupSchemaExtended)
Loading
sequenceDiagram
    autonumber
    participant Table as Partners/Payouts/Analytics Table
    participant Hook as useGroups()
    participant Row as PartnerRowItem
    participant Util as getGroupRewardsAndDiscount

    Table->>Hook: Load groups
    Hook-->>Table: { groups }
    Table->>Row: partner {id,name,image}, group (resolved or null)
    Row->>Util: derive rewards + discount from group
    Util-->>Row: { rewards[], discount? }
    Row-->>Table: render avatar + tooltip (if rewards/discount)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • steven-tey

Poem

I twitch my nose at groups so bright,
Rewards and gifts hop into sight—
Tooltips nibble carrots sweet,
Avatars dance on nimble feet,
Code and clover, bound and tight 🥕🐇

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 "Changing partner hover data" is concise and correctly targets the partner-hover UI change described in the PR (replacing payout connection status with group rewards), so it meaningfully reflects the main change while remaining brief. It gives a reviewer enough context to understand the primary intent without listing implementation details.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Refined the language output and changed the order to match the settings.
Fixed formatting since there's no percentage rewards for clicks and leads.
Comment on lines +31 to +51
const groupsWithRewards = await Promise.all(
groups.map(async (group) => {
const groupWithRewards = await prisma.partnerGroup.findUnique({
where: { id: group.id },
include: {
clickReward: true,
leadReward: true,
saleReward: true,
discount: true,
},
});

return {
...group,
clickReward: groupWithRewards?.clickReward,
leadReward: groupWithRewards?.leadReward,
saleReward: groupWithRewards?.saleReward,
discount: groupWithRewards?.discount,
};
})
);
Copy link
Contributor

Choose a reason for hiding this comment

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

The API route introduces a severe N+1 query performance issue by making individual database queries for each group to fetch reward&2f;discount data that may already be available from the original query.

View Details
📝 Patch Details
diff --git a/apps/web/app/(ee)/api/groups/route.ts b/apps/web/app/(ee)/api/groups/route.ts
index 8c40358f0..a61bc59c7 100644
--- a/apps/web/app/(ee)/api/groups/route.ts
+++ b/apps/web/app/(ee)/api/groups/route.ts
@@ -27,30 +27,8 @@ export const GET = withWorkspace(
       programId,
     });
 
-    // Fetch rewards and discount data for each group
-    const groupsWithRewards = await Promise.all(
-      groups.map(async (group) => {
-        const groupWithRewards = await prisma.partnerGroup.findUnique({
-          where: { id: group.id },
-          include: {
-            clickReward: true,
-            leadReward: true,
-            saleReward: true,
-            discount: true,
-          },
-        });
-
-        return {
-          ...group,
-          clickReward: groupWithRewards?.clickReward,
-          leadReward: groupWithRewards?.leadReward,
-          saleReward: groupWithRewards?.saleReward,
-          discount: groupWithRewards?.discount,
-        };
-      })
-    );
-
-    return NextResponse.json(z.array(GroupSchemaExtended).parse(groupsWithRewards));
+    // Groups now include reward and discount data from the optimized query
+    return NextResponse.json(z.array(GroupSchemaExtended).parse(groups));
   },
   {
     requiredPermissions: ["groups.read"],
diff --git a/apps/web/lib/api/groups/get-groups.ts b/apps/web/lib/api/groups/get-groups.ts
index 3a36b744f..8dc003227 100644
--- a/apps/web/lib/api/groups/get-groups.ts
+++ b/apps/web/lib/api/groups/get-groups.ts
@@ -40,6 +40,36 @@ export async function getGroups(filters: GroupFilters) {
       pg.name,
       pg.slug,
       pg.color,
+      -- Reward data
+      cr.id as clickRewardId,
+      cr.description as clickRewardDescription,
+      cr.event as clickRewardEvent,
+      cr.type as clickRewardType,
+      cr.amount as clickRewardAmount,
+      cr.maxDuration as clickRewardMaxDuration,
+      cr.modifiers as clickRewardModifiers,
+      lr.id as leadRewardId,
+      lr.description as leadRewardDescription,
+      lr.event as leadRewardEvent,
+      lr.type as leadRewardType,
+      lr.amount as leadRewardAmount,
+      lr.maxDuration as leadRewardMaxDuration,
+      lr.modifiers as leadRewardModifiers,
+      sr.id as saleRewardId,
+      sr.description as saleRewardDescription,
+      sr.event as saleRewardEvent,
+      sr.type as saleRewardType,
+      sr.amount as saleRewardAmount,
+      sr.maxDuration as saleRewardMaxDuration,
+      sr.modifiers as saleRewardModifiers,
+      -- Discount data
+      d.id as discountId,
+      d.amount as discountAmount,
+      d.type as discountType,
+      d.maxDuration as discountMaxDuration,
+      d.description as discountDescription,
+      d.couponId as discountCouponId,
+      d.couponTestId as discountCouponTestId,
       ${
         includeExpandedFields
           ? Prisma.sql`
@@ -64,6 +94,10 @@ export async function getGroups(filters: GroupFilters) {
         `
       }
     FROM PartnerGroup pg
+    LEFT JOIN Reward cr ON cr.id = pg.clickRewardId
+    LEFT JOIN Reward lr ON lr.id = pg.leadRewardId
+    LEFT JOIN Reward sr ON sr.id = pg.saleRewardId
+    LEFT JOIN Discount d ON d.id = pg.discountId
     LEFT JOIN ProgramEnrollment pe ON pe.groupId = pg.id AND pe.status IN ('approved', 'invited')
     ${
       includeExpandedFields
@@ -105,5 +139,43 @@ export async function getGroups(filters: GroupFilters) {
     conversions: Number(group.totalConversions),
     commissions: Number(group.totalCommissions),
     netRevenue: Number(group.netRevenue),
+    // Transform reward data
+    clickReward: group.clickRewardId ? {
+      id: group.clickRewardId,
+      description: group.clickRewardDescription,
+      event: group.clickRewardEvent,
+      type: group.clickRewardType,
+      amount: Number(group.clickRewardAmount),
+      maxDuration: group.clickRewardMaxDuration,
+      modifiers: group.clickRewardModifiers,
+    } : null,
+    leadReward: group.leadRewardId ? {
+      id: group.leadRewardId,
+      description: group.leadRewardDescription,
+      event: group.leadRewardEvent,
+      type: group.leadRewardType,
+      amount: Number(group.leadRewardAmount),
+      maxDuration: group.leadRewardMaxDuration,
+      modifiers: group.leadRewardModifiers,
+    } : null,
+    saleReward: group.saleRewardId ? {
+      id: group.saleRewardId,
+      description: group.saleRewardDescription,
+      event: group.saleRewardEvent,
+      type: group.saleRewardType,
+      amount: Number(group.saleRewardAmount),
+      maxDuration: group.saleRewardMaxDuration,
+      modifiers: group.saleRewardModifiers,
+    } : null,
+    // Transform discount data
+    discount: group.discountId ? {
+      id: group.discountId,
+      amount: Number(group.discountAmount),
+      type: group.discountType,
+      maxDuration: group.discountMaxDuration,
+      description: group.discountDescription,
+      couponId: group.discountCouponId,
+      couponTestId: group.discountCouponTestId,
+    } : null,
   }));
 }

Analysis

N+1 query performance issue in GET &2f;api&2f;groups route

What fails&3a; The getGroups() API route executes N+1 database queries when fetching reward and discount data for groups, causing significant performance degradation as the number of groups scales.

How to reproduce&3a;

# Make API request to groups endpoint
curl -H "Authorization: Bearer $TOKEN" https://app.dub.co/api/groups

# With 100 groups, this executes 101 database queries:
# 1 query from getGroups() + 100 individual prisma.partnerGroup.findUnique() calls

Result&3a; Each group triggers a separate prisma.partnerGroup.findUnique() query in Promise.all() loop &28;lines 31-51 in route.ts&29;. For N groups, this creates N+1 total queries instead of a single optimized query.

Expected&3a; Single query with JOINs to fetch all group data including rewards and discounts in one database round-trip, eliminating the N+1 query anti-pattern.

@marcusljf marcusljf marked this pull request as ready for review September 18, 2025 16:00
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/web/app/(ee)/api/groups/route.ts (1)

30-54: Remove N+1 queries; rely on getGroups to return rewards/discounts.

Per‑group findUnique() inside Promise.all will not scale. Fetch rewards/discounts in getGroups() (JOINs/relations) and return groups directly here.

Apply this diff:

-    // Fetch rewards and discount data for each group
-    const groupsWithRewards = await Promise.all(
-      groups.map(async (group) => {
-        const groupWithRewards = await prisma.partnerGroup.findUnique({
-          where: { id: group.id },
-          include: {
-            clickReward: true,
-            leadReward: true,
-            saleReward: true,
-            discount: true,
-          },
-        });
-
-        return {
-          ...group,
-          clickReward: groupWithRewards?.clickReward,
-          leadReward: groupWithRewards?.leadReward,
-          saleReward: groupWithRewards?.saleReward,
-          discount: groupWithRewards?.discount,
-        };
-      })
-    );
-
-    return NextResponse.json(z.array(GroupSchemaExtended).parse(groupsWithRewards));
+    return NextResponse.json(z.array(GroupSchemaExtended).parse(groups));
🧹 Nitpick comments (7)
apps/web/ui/partners/partner-row-item.tsx (4)

26-33: Use shared currency formatter for consistency and i18n.

Avoid manual $ + toFixed. Use currencyFormatter like elsewhere.

-import { cn } from "@dub/utils";
+import { cn, currencyFormatter } from "@dub/utils";
...
-    return `$${(reward.amount / 100).toFixed(2)}`;
+    return currencyFormatter(reward.amount / 100);
...
-                          New users get {discount.type === "percentage" ? `${discount.amount}%` : `$${(discount.amount / 100).toFixed(0)}`} off {getDurationText(discount.maxDuration)}
+                          New users get {discount.type === "percentage" ? `${discount.amount}%` : currencyFormatter(discount.amount / 100)} off {getDurationText(discount.maxDuration)}

Also applies to: 109-111


61-65: Don’t mutate props; sorting is redundant.

getGroupRewardsAndDiscount() already returns sorted rewards. Also avoid mutating rewards in place.

-  const sortedRewards = rewards.sort((a, b) => {
-    const order = { sale: 0, lead: 1, click: 2 } as const;
-    return (order[a.event as keyof typeof order] ?? 999) - (order[b.event as keyof typeof order] ?? 999);
-  });
+  const sortedRewards = rewards; // already ordered upstream

46-55: Include duration for lead/click rewards for consistency.

Align copy with sale rewards by appending duration.

       case "sale":
         return `Up to ${amount} per sale ${duration}`;
       case "lead":
-        return `${amount} per lead`;
+        return `${amount} per lead ${duration}`;
       case "click":
-        return `${amount} per click`;
+        return `${amount} per click ${duration}`;

68-79: Tighten types for event.

Use the union from RewardProps["event"] instead of string.

-  const getRewardIcon = (event: string) => {
+  const getRewardIcon = (event: RewardProps["event"]) => {
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1)

3-3: Remove unused groups fetch to avoid extra network call.

useGroups() is imported and invoked but not used; it triggers an unnecessary /api/groups request.

-import useGroups from "@/lib/swr/use-groups";
...
-    const { groups } = useGroups();

Also applies to: 56-56

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1)

169-179: Avoid O(n) group lookups per row; index groups by id.

Build a groupsById map once and reuse in both columns to cut repeated scans.

   const { groups } = useGroups();
+  const groupsById = useMemo(
+    () => Object.fromEntries((groups ?? []).map((g) => [g.id, g])),
+    [groups],
+  );
...
-            const group = groups?.find((g) => g.id === row.original.groupId);
+            const group = groupsById[row.original.groupId as string];
...
-            const group = groups.find((g) => g.id === row.original.groupId);
+            const group = groupsById[row.original.groupId as string];

Also applies to: 189-191

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx (1)

3-3: Remove unused useGroups to prevent redundant fetch.

groups isn’t used and causes an unnecessary /api/groups request.

-import useGroups from "@/lib/swr/use-groups";
...
-  const { groups } = useGroups();

Also applies to: 21-21

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c0c661 and fb58a60.

📒 Files selected for processing (5)
  • apps/web/app/(ee)/api/groups/route.ts (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx (3 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (3 hunks)
  • apps/web/ui/partners/partner-row-item.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: devkiran
PR: dubinc/dub#2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.
📚 Learning: 2025-08-14T05:17:51.825Z
Learnt from: devkiran
PR: dubinc/dub#2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.

Applied to files:

  • apps/web/app/(ee)/api/groups/route.ts
📚 Learning: 2025-06-16T19:21:23.506Z
Learnt from: TWilson023
PR: dubinc/dub#2519
File: apps/web/ui/analytics/utils.ts:35-37
Timestamp: 2025-06-16T19:21:23.506Z
Learning: In the `useAnalyticsFilterOption` function in `apps/web/ui/analytics/utils.ts`, the pattern `options?.context ?? useContext(AnalyticsContext)` is intentionally designed as a complete replacement strategy, not a merge. When `options.context` is provided, it should contain all required fields (`baseApiPath`, `queryString`, `selectedTab`, `requiresUpgrade`) and completely replace the React context, not be merged with it. This is used for dependency injection or testing scenarios.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx
🧬 Code graph analysis (5)
apps/web/app/(ee)/api/groups/route.ts (1)
apps/web/lib/zod/schemas/groups.ts (1)
  • GroupSchemaExtended (27-37)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx (2)
apps/web/lib/swr/use-groups.ts (1)
  • useGroups (10-38)
apps/web/ui/partners/partner-row-item.tsx (1)
  • PartnerRowItem (10-141)
apps/web/ui/partners/partner-row-item.tsx (3)
apps/web/lib/types.ts (2)
  • GroupProps (526-526)
  • RewardProps (491-491)
apps/web/lib/partners/get-group-rewards-and-discount.ts (1)
  • getGroupRewardsAndDiscount (7-17)
packages/ui/src/tooltip.tsx (1)
  • DynamicTooltipWrapper (280-294)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (2)
apps/web/lib/swr/use-groups.ts (1)
  • useGroups (10-38)
apps/web/ui/partners/partner-row-item.tsx (1)
  • PartnerRowItem (10-141)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1)
apps/web/ui/partners/partner-row-item.tsx (1)
  • PartnerRowItem (10-141)
🔇 Additional comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1)

117-125: PartnerRowItem usage looks good.

Passing group={null} is correct given payouts data lacks groupId.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx (1)

58-66: Partner cell usage is correct.

Sanitizing partner props and passing group={null} aligns with the component API.

apps/web/app/(ee)/api/groups/route.ts (1)

53-53: No change needed — schema includes reward/discount fields.

apps/web/lib/zod/schemas/groups.ts defines clickReward, leadReward, saleReward as RewardSchema.nullish() and discount as DiscountSchema.nullish() (import at line 5; fields at lines 21–24), so z.array(GroupSchemaExtended).parse(groupsWithRewards) will preserve those fields.

Corrects the handling of null and undefined maxDuration values in getDurationText to ensure 'for the customer's lifetime' is shown only when appropriate.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
apps/web/ui/partners/partner-row-item.tsx (6)

110-110: Preserve cents for flat discounts.

Using toFixed(0) drops cents (e.g., $2.50 becomes $2). Align with reward formatting.

-                          New users get {discount.type === "percentage" ? `${discount.amount}%` : `$${(discount.amount / 100).toFixed(0)}`} off {getDurationText(discount.maxDuration)}
+                          New users get {discount.type === "percentage" ? `${discount.amount}%` : `$${(discount.amount / 100).toFixed(2)}`} off {getDurationText(discount.maxDuration)}

61-65: Avoid mutating rewards; sort a copy.

Prevent side effects by not sorting the original array.

-  const sortedRewards = rewards.sort((a, b) => {
+  const sortedRewards = [...rewards].sort((a, b) => {
     const order = { sale: 0, lead: 1, click: 2 } as const;
     return (order[a.event as keyof typeof order] ?? 999) - (order[b.event as keyof typeof order] ?? 999);
   });

68-79: Tighten typing for getRewardIcon.

Use the event union from RewardProps to catch typos at compile time.

-  const getRewardIcon = (event: string) => {
+  const getRewardIcon = (event: RewardProps["event"]) => {
     switch (event) {
       case "sale":
         return InvoiceDollar;
       case "lead":
         return UserPlus;
       case "click":
         return CursorRays;
       default:
         return CursorRays;
     }
   };

47-49: Only prepend “Up to” for percentage sale rewards.

Flat sale payouts aren’t “up to”.

-        return `Up to ${amount} per sale ${duration}`;
+        return `${reward.type === "percentage" ? "Up to " : ""}${amount} per sale ${duration}`;

121-125: Lazy‑load the avatar.

Small perf win, especially in tables.

-          <img
+          <img
+            loading="lazy"
             src={partner.image || `${OG_AVATAR_URL}${partner.name}`}
             alt={partner.name}
             className="size-5 shrink-0 rounded-full"
           />

4-4: Avoid deep import if possible.

If OG_AVATAR_URL is re‑exported from @dub/utils, prefer that path for stability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb58a60 and 0e321ad.

📒 Files selected for processing (1)
  • apps/web/ui/partners/partner-row-item.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: devkiran
PR: dubinc/dub#2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.
🧬 Code graph analysis (1)
apps/web/ui/partners/partner-row-item.tsx (3)
apps/web/lib/types.ts (2)
  • GroupProps (526-526)
  • RewardProps (491-491)
apps/web/lib/partners/get-group-rewards-and-discount.ts (1)
  • getGroupRewardsAndDiscount (7-17)
packages/ui/src/tooltip.tsx (1)
  • DynamicTooltipWrapper (280-294)
⏰ 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). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build
🔇 Additional comments (1)
apps/web/ui/partners/partner-row-item.tsx (1)

83-89: Confirm hover target (avatar only vs. entire item).

Tooltip currently triggers on the avatar only. If spec expects hover on the avatar + name, wrap both in DynamicTooltipWrapper.

Also applies to: 127-139

@@ -1,5 +1,6 @@
import { AnalyticsResponse } from "@/lib/analytics/types";
import { editQueryString } from "@/lib/analytics/utils";
import useGroups from "@/lib/swr/use-groups";
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused import&3a; useGroups is imported but never used in the analytics partners table component.

View Details
📝 Patch Details
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx
index c7ee1f128..47b6d3f11 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx
@@ -1,6 +1,5 @@
 import { AnalyticsResponse } from "@/lib/analytics/types";
 import { editQueryString } from "@/lib/analytics/utils";
-import useGroups from "@/lib/swr/use-groups";
 import { AnalyticsContext } from "@/ui/analytics/analytics-provider";
 import { PartnerRowItem } from "@/ui/partners/partner-row-item";
 import { FilterButtonTableRow } from "@/ui/shared/filter-button-table-row";
@@ -18,7 +17,6 @@ import useSWR from "swr";
 
 export function AnalyticsPartnersTable() {
   const { selectedTab, queryString } = useContext(AnalyticsContext);
-  const { groups } = useGroups();
 
   const { pagination, setPagination } = usePagination(10);
 

Analysis

Unused import&3a; useGroups in analytics partners table component

What fails&3a; TypeScript compiler reports error TS6133: 'groups' is declared but its value is never read in apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx:21

How to reproduce&3a;

cd apps/web && pnpm exec tsc --noEmit --noUnusedLocals --noUnusedParameters "app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx"

Result&3a; TypeScript error about unused groups variable from useGroups() hook

Expected&3a; No unused import warnings - the component explicitly passes group={null} to all PartnerRowItem components and includes a comment that &22;groupId not available in analytics data, so no group rewards will be shown&22;

Enhances security by adding rel="noopener noreferrer" to partner permalink links when opened in a new tab. This prevents potential security vulnerabilities related to window.opener.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/web/ui/partners/partner-row-item.tsx (1)

24-25: Prefer explicit conditional over ElementType union for Link.

Avoids prop mismatch/typing pitfalls and matches prior guidance.

-  const As = showPermalink ? Link : "div";
+  // render Link only when permalink is enabled
-      <As
-        {...(showPermalink && {
-          href: `/${slug}/program/partners?partnerId=${partner.id}`,
-          target: "_blank",
-          rel: "noopener noreferrer",
-        })}
-        className={cn(
-          "min-w-0 truncate",
-          showPermalink && "cursor-alias decoration-dotted hover:underline",
-        )}
-        title={partner.name}
-      >
-        {partner.name}
-      </As>
+      {showPermalink ? (
+        <Link
+          href={`/${slug}/program/partners?partnerId=${partner.id}`}
+          target="_blank"
+          rel="noopener noreferrer"
+          className={cn("min-w-0 truncate", "cursor-alias decoration-dotted hover:underline")}
+          title={partner.name}
+        >
+          {partner.name}
+        </Link>
+      ) : (
+        <div className="min-w-0 truncate" title={partner.name}>
+          {partner.name}
+        </div>
+      )}

Also applies to: 128-141

🧹 Nitpick comments (6)
apps/web/ui/partners/partner-row-item.tsx (6)

7-8: Remove unused type and make imports type‑only.

DiscountProps isn’t used. Also mark these as type imports to avoid any runtime weight.

-import { GroupProps, RewardProps, DiscountProps } from "@/lib/types";
+import type { GroupProps, RewardProps } from "@/lib/types";

23-23: Type the route param for safety.

Slug can be inferred as string | string[] without a generic; constrain it.

-  const { slug } = useParams();
+  const { slug } = useParams<{ slug: string }>();

61-66: Avoid double‑sorting; util already returns sorted rewards.

getGroupRewardsAndDiscount() sorts; drop the extra sort and map directly.

-  // Sort rewards in the specified order: sale, lead, click
-  const sortedRewards = rewards.sort((a, b) => {
-    const order = { sale: 0, lead: 1, click: 2 } as const;
-    return (order[a.event as keyof typeof order] ?? 999) - (order[b.event as keyof typeof order] ?? 999);
-  });
+  // Rewards already sorted by getGroupRewardsAndDiscount
-                    {sortedRewards.map((reward) => {
+                    {rewards.map((reward) => {

Also applies to: 90-103


121-123: URL‑encode fallback avatar name.

Prevents broken URLs for names with spaces/special chars.

-            src={partner.image || `${OG_AVATAR_URL}${partner.name}`}
+            src={partner.image || `${OG_AVATAR_URL}${encodeURIComponent(partner.name)}`}

109-111: Keep currency precision for discounts.

Use two decimals like rewards for consistency.

-                          New users get {discount.type === "percentage" ? `${discount.amount}%` : `$${(discount.amount / 100).toFixed(0)}`} off {getDurationText(discount.maxDuration)}
+                          New users get {discount.type === "percentage" ? `${discount.amount}%` : `$${(discount.amount / 100).toFixed(2)}`} off {getDurationText(discount.maxDuration)}

68-69: Tighten icon helper signature.

Use the event union from RewardProps for better type safety.

-  const getRewardIcon = (event: string) => {
+  const getRewardIcon = (event: RewardProps["event"]) => {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e321ad and 5e50c59.

📒 Files selected for processing (1)
  • apps/web/ui/partners/partner-row-item.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: devkiran
PR: dubinc/dub#2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.
📚 Learning: 2025-06-18T20:23:38.835Z
Learnt from: TWilson023
PR: dubinc/dub#2538
File: apps/web/ui/partners/overview/blocks/traffic-sources-block.tsx:50-82
Timestamp: 2025-06-18T20:23:38.835Z
Learning: Internal links within the same application that use target="_blank" may not require rel="noopener noreferrer" according to the team's security standards, even though it's generally considered a best practice for any target="_blank" link.

Applied to files:

  • apps/web/ui/partners/partner-row-item.tsx
🧬 Code graph analysis (1)
apps/web/ui/partners/partner-row-item.tsx (3)
apps/web/lib/types.ts (2)
  • GroupProps (526-526)
  • RewardProps (491-491)
apps/web/lib/partners/get-group-rewards-and-discount.ts (1)
  • getGroupRewardsAndDiscount (7-17)
packages/ui/src/tooltip.tsx (1)
  • DynamicTooltipWrapper (280-294)
⏰ 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: Vade Review
🔇 Additional comments (1)
apps/web/ui/partners/partner-row-item.tsx (1)

46-55: Confirm “Up to” copy for sale rewards.

If flat sale rewards aren’t capped, “Up to” may be misleading. Consider gating “Up to ” on percentage type or a cap flag.

-      case "sale":
-        return `Up to ${amount} per sale ${duration}`;
+      case "sale":
+        return `${reward.type === "percentage" ? "Up to " : ""}${amount} per sale ${duration}`;

@marcusljf marcusljf closed this Sep 29, 2025
@marcusljf marcusljf deleted the partner-hover branch September 29, 2025 02:03
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.

1 participant