-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Changing partner hover data #2837
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
Instead of showing the payout connection status, show the rewards the partner is on.
|
@marcusljf is attempting to deploy a commit to the Dub Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughAPI GET /groups now enriches each group with click/lead/sale rewards and discount via per-group Prisma lookups. Frontend tables load groups via Changes
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)
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
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 |
Refined the language output and changed the order to match the settings.
Fixed formatting since there's no percentage rewards for clicks and leads.
| 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, | ||
| }; | ||
| }) | ||
| ); |
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.
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() callsResult&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.
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: 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()insidePromise.allwill not scale. Fetch rewards/discounts ingetGroups()(JOINs/relations) and returngroupsdirectly 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. UsecurrencyFormatterlike 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 mutatingrewardsin 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 ofstring.- 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/groupsrequest.-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
groupsByIdmap 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.
groupsisn’t used and causes an unnecessary/api/groupsrequest.-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
📒 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 lacksgroupId.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.
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: 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
📒 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"; | |||
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.
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.
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 (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
📒 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}`;
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
Refactor