-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Partner Network #2886
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
Partner Network #2886
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds a Partner Network feature: new APIs for listing and counting network partners, server actions for invite/star/dismiss, discoverable profile support, zod schemas/types, SWR hooks, UI pages/components (network list, sheet, filters, empty/upsell), Prisma model additions, icons, and a backfill script. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant UI as Program Network Page (Client)
participant API as GET /api/network/partners
participant DB as Prisma/DB
User->>UI: Load page or change filters
UI->>API: Fetch partners (query: status, filters, page)
API->>DB: $queryRaw join partners + metrics
DB-->>API: Rows
API-->>UI: JSON (NetworkPartnerSchema[])
UI->>UI: Render list, counts, pagination
sequenceDiagram
autonumber
actor Admin
participant Sheet as NetworkPartnerSheet
participant Invite as invitePartnerFromNetworkAction
participant DB as Prisma
participant Mail as Email Service
participant Audit as Audit Logger
Admin->>Sheet: Click "Send invite"
Sheet->>Invite: partnerId, (groupId)
Invite->>DB: Verify program/partner, upsert DiscoveredPartner.invitedAt
Invite-->>Sheet: Success
par Async side-effects
Invite->>Mail: Send invitation email
Invite->>Audit: Record "partner_invited"
end
Sheet->>Sheet: Toast + close + mutate caches
sequenceDiagram
autonumber
actor Partner
participant Profile as Profile Page (Client)
participant Update as updatePartnerProfileAction
participant DB as Prisma
participant Util as getPartnerDiscoveryRequirements
Partner->>Profile: Toggle "Discoverable"
Profile->>Update: discoverable: true/false + profile data
Update->>DB: Update partner fields (+/- discoverableAt)
Update->>DB: Aggregate commissions (exclude ACME)
Update->>Util: Check requirements
alt Requirements incomplete
Update->>DB: Clear discoverableAt
end
Update-->>Profile: Updated partner
Profile->>Profile: mutatePrefix("/api/partner-profile")
Estimated code review effort🎯 5 (Critical) | ⏱️ ~150 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page-client.tsx (1)
690-701
: Fix type mismatch in SWR hook.The
useSWR
hook is typed as returning a singlePartnerNetworkPartnerProps
, but line 700 accesses it as an array withfetchedPartners?.[0]
. This type mismatch could lead to runtime errors.Apply this diff to fix the type:
- const { data: fetchedPartners, isLoading } = - useSWR<PartnerNetworkPartnerProps>( + const { data: fetchedPartners, isLoading } = + useSWR<PartnerNetworkPartnerProps[]>( fetchPartnerId && `/api/network/partners?workspaceId=${workspaceId}&partnerIds=${fetchPartnerId}`, fetcher, { keepPreviousData: true, }, );
🧹 Nitpick comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page-client.tsx (2)
260-296
: Document the SWR typing limitation.The
@ts-ignore
comment mentions SWR's incomplete typing forpopulateCache
with partial data. While this is a known limitation, consider adding a more detailed comment explaining the expected behavior and why it's safe to ignore.Apply this diff to improve the comment:
mutatePartners( - // @ts-ignore SWR doesn't seem to have proper typing for partial data results w/ `populateCache` + // @ts-ignore SWR's populateCache typing doesn't support partial data results. + // Safe: optimisticData and populateCache both map over the existing array. async () => {
584-604
: Simplify resize logic dependencies.The
isReady
state is reset tofalse
on line 587 but checked for early return on line 585. This creates unnecessary re-renders. Consider simplifying the logic to avoid the redundant state update.Apply this diff:
useEffect(() => { - if (isReady) return; - - setIsReady(false); setShownItems(items); + setIsReady(false); }, [items]);apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/use-partner-network-filters.tsx (1)
156-162
: Consider extracting filter keys constant.The
onRemoveAll
function hardcodes the multi-filter keys. Consider extracting these to a constant to avoid duplication with the multiFilters object definition.Add a constant at the top of the hook:
+ const MULTI_FILTER_KEYS = [ + "industryInterests", + "salesChannels", + "preferredEarningStructures", + ] as const; + const multiFilters = useMemo( () => ({ - industryInterests: + [MULTI_FILTER_KEYS[0]]: searchParamsObj.industryInterests?.split(",")?.filter(Boolean) ?? [], // ... update other keys similarly }), [searchParamsObj], ); const onRemoveAll = useCallback( () => queryParams({ - del: [...Object.keys(multiFilters), "country", "starred"], + del: [...MULTI_FILTER_KEYS, "country", "starred"], }), [queryParams], );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/layout.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/network-empty-state.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/network-upsell.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page-client.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/use-partner-network-filters.tsx
(1 hunks)apps/web/lib/middleware/utils/app-redirect.ts
(1 hunks)apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
(5 hunks)apps/web/ui/partners/partner-network-partner-sheet.tsx
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- apps/web/lib/middleware/utils/app-redirect.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/ui/partners/partner-network-partner-sheet.tsx
🧰 Additional context used
🧬 Code graph analysis (7)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page.tsx (2)
apps/web/ui/layout/page-content/index.tsx (1)
PageContent
(11-100)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page-client.tsx (1)
ProgramPartnerNetworkPageClient
(65-320)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/layout.tsx (3)
apps/web/lib/swr/use-program.ts (1)
useProgram
(6-40)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/network-upsell.tsx (1)
NetworkUpsell
(8-45)apps/web/lib/plan-capabilities.ts (1)
getPlanCapabilities
(4-20)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/use-partner-network-filters.tsx (2)
apps/web/lib/swr/use-partner-network-partners-count.ts (1)
usePartnerNetworkPartnersCount
(8-54)apps/web/lib/partners/partner-profile.ts (3)
industryInterests
(35-160)salesChannels
(229-258)preferredEarningStructures
(197-221)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page-client.tsx (8)
apps/web/lib/swr/use-partner-network-partners-count.ts (1)
usePartnerNetworkPartnersCount
(8-54)apps/web/lib/types.ts (1)
PartnerNetworkPartnerProps
(453-455)apps/web/lib/actions/partners/update-discovered-partner.ts (1)
updateDiscoveredPartnerAction
(9-44)apps/web/lib/zod/schemas/partner-network.ts (1)
PARTNER_NETWORK_MAX_PAGE_SIZE
(33-33)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/use-partner-network-filters.tsx (1)
usePartnerNetworkFilters
(12-174)apps/web/ui/partners/partner-network-partner-sheet.tsx (1)
PartnerNetworkPartnerSheet
(126-149)apps/web/lib/partners/online-presence.ts (1)
ONLINE_PRESENCE_FIELDS
(29-103)apps/web/lib/partners/partner-profile.ts (2)
industryInterestsMap
(162-166)salesChannelsMap
(260-262)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (1)
apps/web/lib/swr/use-program.ts (1)
useProgram
(6-40)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/network-empty-state.tsx (3)
apps/web/ui/shared/animated-empty-state.tsx (1)
AnimatedEmptyState
(8-81)packages/ui/src/icons/nucleo/star-fill.tsx (1)
StarFill
(3-20)packages/ui/src/icons/nucleo/star.tsx (1)
Star
(3-24)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/network-upsell.tsx (1)
apps/web/ui/layout/page-content/index.tsx (1)
PageContent
(11-100)
⏰ 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 (6)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (2)
266-270
: LGTM!The Partner Network navigation entry follows the established pattern and correctly uses the UserPlus icon. The href properly constructs the route path with the slug parameter.
490-492
: LGTM!The useProgram hook is correctly enabled only when in the program area and a defaultProgramId exists, preventing unnecessary data fetching.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/network-empty-state.tsx (1)
60-93
: LGTM!The DemoAvatar component correctly uses
useId()
to generate a unique identifier for the clipPath, preventing ID collisions when multiple instances are rendered.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page.tsx (1)
5-13
: LGTM!The page component follows Next.js App Router conventions and properly composes the layout primitives with the client component.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/layout.tsx (1)
10-26
: LGTM!The layout correctly implements two-level gating:
- Program-level feature flag (
partnerNetworkEnabledAt
)- Plan capability check (
canDiscoverPartners
)The conditional rendering of
NetworkUpsell
with appropriate props aligns with the enterprise gating strategy.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/network-upsell.tsx (1)
82-86
: Index values validated: All EXAMPLE_PARTNERS indices (0, 4, 3, 1) are within the valid range (0–13).
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: 2
🧹 Nitpick comments (1)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (1)
245-260
: Navigation structure looks good.The updated
isActive
logic correctly excludes both "applications" and "network" paths, ensuring "All Partners" is active only for the base partners list. The new navigation entries for "Groups" and "Partner Network" are properly structured.Consider removing the type casting on line 259 if it's not necessary:
- href: `/${slug}/program/partners/network` as `/${string}`, + href: `/${slug}/program/partners/network`,Other navigation entries don't require this type assertion, so verify if this is addressing a specific TypeScript error or can be safely removed.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
(4 hunks)packages/prisma/schema/partner.prisma
(3 hunks)packages/prisma/schema/program.prisma
(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (1)
apps/web/lib/swr/use-program.ts (1)
useProgram
(6-40)
⏰ 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 (2)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (2)
11-11
: LGTM!The new imports for
useProgram
andUserPlus
are correctly added and utilized within the component.Also applies to: 37-37
490-492
: Verify preloading behavior is as intended.The
program
variable fromuseProgram
is not directly used in this component, which appears to be intentional for preloading/caching purposes (as suggested by the AI summary). The enabled flag correctly restricts the fetch to whencurrentArea
is "program" anddefaultProgramId
exists.If the
program
data is not needed for caching and won't be used in the near future, consider removing this hook call to avoid unnecessary data fetching.
model DiscoveredPartner { | ||
programId String | ||
partnerId String | ||
starredAt DateTime? | ||
ignoredAt DateTime? | ||
invitedAt DateTime? | ||
program Program @relation(fields: [programId], references: [id], onDelete: Cascade) | ||
partner Partner @relation(fields: [partnerId], references: [id], onDelete: Cascade) | ||
@@unique([programId, partnerId]) | ||
@@index(partnerId) |
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.
Add a primary key to DiscoveredPartner
.
DiscoveredPartner
currently has no @id
/@@id
. Prisma will reject the schema and migrations without a primary key. Use the composite key as the identifier.
model DiscoveredPartner {
programId String
partnerId String
starredAt DateTime?
ignoredAt DateTime?
invitedAt DateTime?
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
partner Partner @relation(fields: [partnerId], references: [id], onDelete: Cascade)
- @@unique([programId, partnerId])
+ @@id([programId, partnerId])
@@index(partnerId)
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
model DiscoveredPartner { | |
programId String | |
partnerId String | |
starredAt DateTime? | |
ignoredAt DateTime? | |
invitedAt DateTime? | |
program Program @relation(fields: [programId], references: [id], onDelete: Cascade) | |
partner Partner @relation(fields: [partnerId], references: [id], onDelete: Cascade) | |
@@unique([programId, partnerId]) | |
@@index(partnerId) | |
model DiscoveredPartner { | |
programId String | |
partnerId String | |
starredAt DateTime? | |
ignoredAt DateTime? | |
invitedAt DateTime? | |
program Program @relation(fields: [programId], references: [id], onDelete: Cascade) | |
partner Partner @relation(fields: [partnerId], references: [id], onDelete: Cascade) | |
@@id([programId, partnerId]) | |
@@index(partnerId) | |
} |
🤖 Prompt for AI Agents
In packages/prisma/schema/partner.prisma around lines 185 to 197, the
DiscoveredPartner model lacks a primary key; replace the existing
@@unique([programId, partnerId]) with a composite primary key declaration
@@id([programId, partnerId]) (and remove the redundant @@unique), keeping or
adjusting @@index(partnerId) as needed so Prisma has a valid identifier for the
model.
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: 3
♻️ Duplicate comments (4)
apps/web/lib/swr/use-network-partners-count.ts (1)
30-42
: Handle undefined/array params when building the SWR key.Passing
{ workspaceId, ...query }
straight intoURLSearchParams
turns optional fields into"undefined"
and flattens arrays incorrectly, so/count
rejects the request. Iterate entries, skipundefined
/null
, and append array items individually before stringifying.- ignoreParams - ? `?${new URLSearchParams({ workspaceId, ...(query as any) }).toString()}` + ignoreParams + ? (() => { + const params = new URLSearchParams(); + Object.entries({ + workspaceId, + ...(query as Record<string, unknown>), + }).forEach(([key, value]) => { + if (value === undefined || value === null) return; + if (Array.isArray(value)) { + value.forEach((item) => params.append(key, String(item))); + } else { + params.append(key, String(value)); + } + }); + return `?${params.toString()}`; + })()apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page-client.tsx (1)
689-699
: Fix SWR return type for partner lookup.
/api/network/partners
returns an array, yet this hook still typesuseSWR
asNetworkPartnerProps
. That undermines type safety (and tooling) and was already flagged earlier. Keep the data typed as an array before indexing.- const { data: fetchedPartners, isLoading } = useSWR<NetworkPartnerProps>( + const { data: fetchedPartners, isLoading } = + useSWR<NetworkPartnerProps[]>( fetchPartnerId && `/api/network/partners?workspaceId=${workspaceId}&partnerIds=${fetchPartnerId}`, fetcher, { keepPreviousData: true, }, );apps/web/lib/zod/schemas/partner-network.ts (1)
50-67
: Address the empty string filtering issue flagged in previous reviews.As noted in previous review comments, the preprocess functions split comma-separated strings but don't filter out empty strings. This remains unresolved and can cause enum validation failures when trailing commas are present (e.g.,
"foo,bar,"
→["foo", "bar", ""]
).Apply the suggested fix from the previous review:
industryInterests: z .preprocess( - (v) => (typeof v === "string" ? v.split(",") : v), + (v) => (typeof v === "string" ? v.split(",").filter(Boolean) : v), z.array(z.nativeEnum(IndustryInterest)), ) .optional(), salesChannels: z .preprocess( - (v) => (typeof v === "string" ? v.split(",") : v), + (v) => (typeof v === "string" ? v.split(",").filter(Boolean) : v), z.array(z.nativeEnum(SalesChannel)), ) .optional(), preferredEarningStructures: z .preprocess( - (v) => (typeof v === "string" ? v.split(",") : v), + (v) => (typeof v === "string" ? v.split(",").filter(Boolean) : v), z.array(z.nativeEnum(PreferredEarningStructure)), ) .optional(),apps/web/app/(ee)/api/network/partners/route.ts (1)
84-84
: Critical: Division by zero remains unresolved from previous reviews.As flagged in previous review comments, the division
SUM(conversions) / SUM(clicks)
will throw a runtime error whenSUM(clicks)
is 0. TheCOALESCE
wrapper won't help because the division error occurs beforeCOALESCE
can act.Apply the previously suggested fix:
- COALESCE(SUM(conversions) / SUM(clicks), 0) as conversionRate + SUM(conversions) / NULLIF(SUM(clicks), 0) as conversionRateThis will return
NULL
when clicks are zero, whichCOALESCE
can then handle safely.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
apps/web/app/(ee)/api/network/partners/count/route.ts
(1 hunks)apps/web/app/(ee)/api/network/partners/route.ts
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page-client.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/use-partner-network-filters.tsx
(1 hunks)apps/web/lib/swr/use-network-partners-count.ts
(1 hunks)apps/web/lib/types.ts
(2 hunks)apps/web/lib/zod/schemas/partner-network.ts
(1 hunks)apps/web/ui/partners/network-partner-sheet.tsx
(1 hunks)apps/web/ui/partners/partner-info-cards.tsx
(5 hunks)packages/prisma/schema/network.prisma
(1 hunks)packages/prisma/schema/partner.prisma
(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/types.ts
🧰 Additional context used
🧬 Code graph analysis (8)
apps/web/lib/swr/use-network-partners-count.ts (1)
apps/web/lib/zod/schemas/partner-network.ts (1)
getNetworkPartnersCountQuerySchema
(75-84)
apps/web/ui/partners/network-partner-sheet.tsx (11)
apps/web/lib/types.ts (1)
NetworkPartnerProps
(453-453)packages/ui/src/sheet.tsx (1)
Sheet
(74-78)apps/web/ui/partners/partner-info-cards.tsx (1)
PartnerInfoCards
(49-334)apps/web/ui/partners/partner-sheet-tabs.tsx (1)
PartnerSheetTabs
(7-90)apps/web/ui/partners/partner-about.tsx (1)
PartnerAbout
(13-137)apps/web/ui/partners/partner-comments.tsx (1)
PartnerComments
(29-127)apps/web/lib/swr/use-program.ts (1)
useProgram
(6-40)apps/web/lib/actions/partners/invite-partner-from-network.ts (1)
invitePartnerFromNetworkAction
(14-107)apps/web/ui/modals/confirm-modal.tsx (1)
useConfirmModal
(105-118)packages/utils/src/functions/time-ago.ts (1)
timeAgo
(3-32)apps/web/lib/actions/partners/update-discovered-partner.ts (1)
updateDiscoveredPartnerAction
(9-44)
apps/web/ui/partners/partner-info-cards.tsx (6)
apps/web/lib/types.ts (2)
EnrolledPartnerExtendedProps
(459-461)NetworkPartnerProps
(453-453)apps/web/lib/swr/use-group.ts (1)
useGroup
(7-43)apps/web/lib/zod/schemas/groups.ts (1)
DEFAULT_PARTNER_GROUP
(15-19)packages/utils/src/functions/time-ago.ts (1)
timeAgo
(3-32)apps/web/ui/partners/conversion-score-icon.tsx (1)
ConversionScoreIcon
(6-83)apps/web/ui/partners/partner-network/conversion-score-tooltip.tsx (1)
ConversionScoreTooltip
(10-66)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/page-client.tsx (9)
apps/web/lib/swr/use-network-partners-count.ts (1)
useNetworkPartnersCount
(8-54)apps/web/lib/types.ts (1)
NetworkPartnerProps
(453-453)apps/web/lib/actions/partners/update-discovered-partner.ts (1)
updateDiscoveredPartnerAction
(9-44)apps/web/lib/zod/schemas/partner-network.ts (1)
PARTNER_NETWORK_MAX_PAGE_SIZE
(33-33)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/use-partner-network-filters.tsx (1)
usePartnerNetworkFilters
(12-174)apps/web/ui/partners/network-partner-sheet.tsx (1)
NetworkPartnerSheet
(126-149)apps/web/lib/partners/online-presence.ts (1)
ONLINE_PRESENCE_FIELDS
(29-103)apps/web/lib/partners/partner-profile.ts (2)
industryInterestsMap
(162-166)salesChannelsMap
(260-262)packages/ui/src/hooks/use-resize-observer.ts (1)
useResizeObserver
(8-29)
apps/web/lib/zod/schemas/partner-network.ts (2)
packages/prisma/client.ts (3)
IndustryInterest
(13-13)SalesChannel
(29-29)PreferredEarningStructure
(24-24)apps/web/lib/zod/schemas/partners.ts (1)
PartnerSchema
(271-342)
apps/web/app/(ee)/api/network/partners/count/route.ts (4)
apps/web/app/(ee)/api/network/partners/route.ts (1)
GET
(16-156)apps/web/lib/auth/workspace.ts (1)
withWorkspace
(42-436)apps/web/lib/api/errors.ts (1)
DubApiError
(75-92)apps/web/lib/zod/schemas/partner-network.ts (1)
getNetworkPartnersCountQuerySchema
(75-84)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/network/use-partner-network-filters.tsx (3)
apps/web/lib/swr/use-network-partners-count.ts (1)
useNetworkPartnersCount
(8-54)apps/web/lib/partners/partner-profile.ts (3)
industryInterests
(35-160)salesChannels
(229-258)preferredEarningStructures
(197-221)packages/ui/src/icons/index.tsx (1)
Icon
(79-79)
apps/web/app/(ee)/api/network/partners/route.ts (5)
apps/web/lib/auth/workspace.ts (1)
withWorkspace
(42-436)apps/web/lib/api/errors.ts (1)
DubApiError
(75-92)apps/web/lib/zod/schemas/partner-network.ts (2)
getNetworkPartnersQuerySchema
(41-73)NetworkPartnerSchema
(86-124)packages/utils/src/constants/main.ts (1)
ACME_PROGRAM_ID
(77-77)apps/web/lib/actions/partners/get-conversion-score.ts (1)
getConversionScore
(4-18)
⏰ 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 (10)
packages/prisma/schema/partner.prisma (1)
42-81
: IndexingdiscoverableAt
looks good.Optional discoverability timestamp plus an index is a solid foundation for network filters. 👍
apps/web/ui/partners/partner-info-cards.tsx (3)
37-47
: LGTM! Well-designed discriminated union.The discriminated union pattern with
type
and correspondingpartner
props is a clean approach that ensures type safety and prevents misuse. This design makes it clear which partner type is being used and allows TypeScript to narrow the types correctly in each branch.
64-66
: LGTM! Safe groupId resolution with proper fallbacks.The groupId resolution logic correctly uses a type guard (
"groupId" in partner
) before accessing the property, with appropriate fallbacks toselectedGroupId
andDEFAULT_PARTNER_GROUP.slug
. This handles both enrolled and network partner types safely.
222-234
: Nice pattern for conditional wrapping.The use of a
wrapper
prop (defaulting to a plaindiv
) to conditionally wrap elements with components likeConversionScoreTooltip
is a clean and flexible approach. This avoids nesting logic and keeps the rendering logic declarative.apps/web/lib/zod/schemas/partner-network.ts (2)
10-29
: LGTM! Well-structured conversion score constants.The conversion score levels and their corresponding rates are clearly defined. The progression from
unknown
(0%) toexcellent
(5%+) provides a reasonable scale for partner performance classification.
86-124
: LGTM! Well-designed NetworkPartnerSchema.The schema appropriately picks the necessary fields from
PartnerSchema
for the network context (excluding sensitive fields like email, payment info) and merges with additional network-specific fields (conversion metrics, discovery timestamps). This ensures a clean separation between enrolled partner data and publicly visible network partner data.apps/web/app/(ee)/api/network/partners/route.ts (4)
29-33
: LGTM! Appropriate feature gate.The check for
partnerNetworkEnabledAt
ensures that only programs with the partner network feature enabled can access this endpoint, providing proper feature gating at the program level.
68-72
: LGTM! Clean match score calculation.The conditional match score calculation correctly handles both cases: when program industry interests exist (counting matches) and when they don't (defaulting to 0). The use of
Prisma.sql
for safe SQL interpolation is appropriate.
145-145
: Verify getConversionScore handles NULL/undefined conversionRate.The code calls
getConversionScore(partner.conversionRate)
, but if the division by zero issue (Line 84) is fixed to returnNULL
, theconversionRate
value could beNULL
/undefined
.Please verify that
getConversionScore
handlesNULL
/undefined
inputs gracefully. From the relevant code snippets, the function signature is:export function getConversionScore( conversionRate: number, ): PartnerConversionScoreThe function expects a
number
, so passingNULL
/undefined
would cause a type error. After fixing Line 84 to useNULLIF
, you'll need to handle theNULL
case here:conversionScore: partner.conversionRate != null ? getConversionScore(partner.conversionRate) : "unknown",
119-119
: Verify NULL handling on conversionRate filter
The conditionconversionRate < 1
excludes rows whereconversionRate
is NULL. If you intend to include partners with no click data, update the filter to:AND (conversionRate < 1 OR conversionRate IS NULL)Otherwise, add a comment clarifying that NULL values are intentionally excluded.
const countries = await prisma.partner.groupBy({ | ||
by: ["country"], | ||
_count: true, | ||
where: { ...commonWhere, ...statusWheres[status || "discover"] }, | ||
orderBy: { | ||
_count: { | ||
country: "desc", | ||
}, | ||
}, | ||
}); | ||
|
||
return NextResponse.json(countries); | ||
} |
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.
Fix groupBy count shape before returning.
prisma.partner.groupBy
with _count: true
returns an object (e.g. { _all: number }
), but the client treats _count
as a number and pipes it into nFormatter
, which will yield NaN/[object Object]
. Map the result to expose a numeric count (or return _count._all
) and order by that numeric value.
- const countries = await prisma.partner.groupBy({
+ const countries = await prisma.partner.groupBy({
by: ["country"],
- _count: true,
+ _count: {
+ _all: true,
+ },
where: { ...commonWhere, ...statusWheres[status || "discover"] },
orderBy: {
_count: {
- country: "desc",
+ _all: "desc",
},
},
});
- return NextResponse.json(countries);
+ return NextResponse.json(
+ countries.map(({ country, _count }) => ({
+ country,
+ _count: _count._all,
+ })),
+ );
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const countries = await prisma.partner.groupBy({ | |
by: ["country"], | |
_count: true, | |
where: { ...commonWhere, ...statusWheres[status || "discover"] }, | |
orderBy: { | |
_count: { | |
country: "desc", | |
}, | |
}, | |
}); | |
return NextResponse.json(countries); | |
} | |
const countries = await prisma.partner.groupBy({ | |
by: ["country"], | |
_count: { | |
_all: true, | |
}, | |
where: { ...commonWhere, ...statusWheres[status || "discover"] }, | |
orderBy: { | |
_count: { | |
_all: "desc", | |
}, | |
}, | |
}); | |
return NextResponse.json( | |
countries.map(({ country, _count }) => ({ | |
country, | |
_count: _count._all, | |
})), | |
); | |
} |
id: "listedAt", | ||
icon: <CalendarIcon className="size-3.5" />, | ||
text: partner | ||
? `Listed ${formatDate(partner.discoverableAt!)}` |
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.
Remove non-null assertion on discoverableAt.
The non-null assertion (partner.discoverableAt!
) is risky. While network partners should have a non-null discoverableAt
to be listed, runtime data can be inconsistent. According to the schema in apps/web/lib/zod/schemas/partners.ts
, discoverableAt
is nullable.
Apply this diff to handle the null case safely:
text: partner
- ? `Listed ${formatDate(partner.discoverableAt!)}`
+ ? partner.discoverableAt
+ ? `Listed ${formatDate(partner.discoverableAt)}`
+ : "Recently listed"
: undefined,
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
? `Listed ${formatDate(partner.discoverableAt!)}` | |
text: partner | |
? partner.discoverableAt | |
? `Listed ${formatDate(partner.discoverableAt)}` | |
: "Recently listed" | |
: undefined, |
🤖 Prompt for AI Agents
In apps/web/ui/partners/partner-info-cards.tsx around line 147, remove the
non-null assertion on partner.discoverableAt and handle the nullable case
safely; check if partner.discoverableAt is present before calling formatDate
(e.g., conditional rendering or fallback text like "Listed date unavailable"),
ensuring you don't call formatDate with null/undefined and that the UI shows a
sensible fallback when discoverableAt is null.
@@unique([programId, partnerId]) | ||
@@index(partnerId) |
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.
Add a primary key to DiscoveredPartner
.
Prisma rejects models without an @id
or @@id
; keeping only @@unique
will block migrations. Please switch the composite unique to a composite primary key.
- @@unique([programId, partnerId])
+ @@id([programId, partnerId])
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
@@unique([programId, partnerId]) | |
@@index(partnerId) | |
@@id([programId, partnerId]) | |
@@index(partnerId) |
🤖 Prompt for AI Agents
In packages/prisma/schema/network.prisma around lines 13 to 14, the
DiscoveredPartner model currently uses a composite @@unique([programId,
partnerId]) which Prisma rejects because models require an @id or @@id; replace
the composite @@unique with a composite primary key by changing it to
@@id([programId, partnerId]) (you can keep the existing @@index(partnerId) if
you still need an index on partnerId).
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: 3
♻️ Duplicate comments (1)
apps/web/ui/partners/partner-info-cards.tsx (1)
147-147
: Remove non-null assertion on discoverableAt.The non-null assertion (
partner.discoverableAt!
) remains unaddressed from a previous review. While network partners should have a non-nulldiscoverableAt
to be listed, runtime data can be inconsistent.Apply this diff to handle the null case safely:
text: partner - ? `Joined ${formatDate(partner.discoverableAt!)}` + ? partner.discoverableAt + ? `Joined ${formatDate(partner.discoverableAt)}` + : "Recently joined" : undefined,
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/layout.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/network-empty-state.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/network-upsell.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/use-partner-network-filters.tsx
(1 hunks)apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
(4 hunks)apps/web/ui/partners/partner-info-cards.tsx
(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/network-upsell.tsx (1)
apps/web/ui/layout/page-content/index.tsx (1)
PageContent
(11-100)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page.tsx (2)
apps/web/ui/layout/page-content/index.tsx (1)
PageContent
(11-100)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (1)
ProgramPartnerNetworkPageClient
(65-319)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/use-partner-network-filters.tsx (3)
apps/web/lib/swr/use-network-partners-count.ts (1)
useNetworkPartnersCount
(8-54)apps/web/lib/partners/partner-profile.ts (3)
industryInterests
(35-160)salesChannels
(229-258)preferredEarningStructures
(197-221)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/use-partner-filters.tsx (1)
usePartnerFilters
(11-174)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (9)
apps/web/lib/swr/use-network-partners-count.ts (1)
useNetworkPartnersCount
(8-54)apps/web/lib/types.ts (1)
NetworkPartnerProps
(453-453)apps/web/lib/actions/partners/update-discovered-partner.ts (1)
updateDiscoveredPartnerAction
(9-44)apps/web/lib/zod/schemas/partner-network.ts (1)
PARTNER_NETWORK_MAX_PAGE_SIZE
(33-33)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/use-partner-network-filters.tsx (1)
usePartnerNetworkFilters
(12-174)apps/web/ui/partners/network-partner-sheet.tsx (1)
NetworkPartnerSheet
(126-149)apps/web/lib/partners/online-presence.ts (1)
ONLINE_PRESENCE_FIELDS
(29-103)apps/web/lib/partners/partner-profile.ts (2)
industryInterestsMap
(162-166)salesChannelsMap
(260-262)packages/ui/src/hooks/use-resize-observer.ts (1)
useResizeObserver
(8-29)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (1)
apps/web/lib/swr/use-program.ts (1)
useProgram
(6-40)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/layout.tsx (3)
apps/web/lib/swr/use-program.ts (1)
useProgram
(6-40)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/network-upsell.tsx (1)
NetworkUpsell
(8-45)apps/web/lib/plan-capabilities.ts (1)
getPlanCapabilities
(4-20)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/network-empty-state.tsx (3)
apps/web/ui/shared/animated-empty-state.tsx (1)
AnimatedEmptyState
(8-81)packages/ui/src/icons/nucleo/star-fill.tsx (1)
StarFill
(3-20)packages/ui/src/icons/nucleo/star.tsx (1)
Star
(3-24)
apps/web/ui/partners/partner-info-cards.tsx (6)
apps/web/lib/types.ts (2)
EnrolledPartnerExtendedProps
(459-461)NetworkPartnerProps
(453-453)apps/web/lib/swr/use-group.ts (1)
useGroup
(7-43)apps/web/lib/zod/schemas/groups.ts (1)
DEFAULT_PARTNER_GROUP
(15-19)packages/utils/src/functions/time-ago.ts (1)
timeAgo
(3-32)apps/web/ui/partners/conversion-score-icon.tsx (1)
ConversionScoreIcon
(6-83)apps/web/ui/partners/partner-network/conversion-score-tooltip.tsx (1)
ConversionScoreTooltip
(10-66)
⏰ 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 (11)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (3)
11-11
: LGTM!The new imports are used appropriately in the component.
Also applies to: 37-37
247-249
: LGTM!The active detection logic correctly excludes both "applications" and "network" paths, ensuring "All Partners" is only active when viewing the main partners list.
251-260
: LGTM!The new "Groups" and "Partner Network" navigation items follow the established pattern and are properly integrated into the sidebar structure.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/network-empty-state.tsx (5)
1-4
: LGTM!The imports are clean and appropriate for the component's functionality.
6-14
: LGTM!The component props are well-defined and appropriately typed.
18-30
: LGTM!The conditional description provides clear guidance to users based on the filter state.
34-47
: LGTM!The card content rendering creates appropriate visual variety with the star icons, correctly showing all filled stars when viewing starred partners and alternating between filled/outline stars otherwise.
60-93
: LGTM!The
DemoAvatar
component correctly usesuseId
to generate unique clip-path IDs, preventing potential ID collisions when multiple instances are rendered. The SVG structure is appropriate for a placeholder avatar.apps/web/ui/partners/partner-info-cards.tsx (3)
37-60
: LGTM! Well-structured discriminated union API.The refactoring to support both enrolled and network partner contexts is well-designed:
- Clear discriminated union with
type
prop- Type-safe partner prop that changes based on context
- Proper fallback handling when type is undefined
61-70
: LGTM! Correct handling of optional groupId.The runtime type guard
"groupId" in partner
is the appropriate way to handle the discriminated union where only enrolled partners may have a groupId property. The fallback chain is well-structured.
222-235
: LGTM! Elegant dynamic wrapper pattern.The per-field wrapper pattern is well-implemented:
- Defaults to
"div"
for standard fields- Accepts components like
ConversionScoreTooltip
for specialized wrapping- Preserves skeleton loading states correctly
This provides flexibility without cluttering the field definitions.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/network-empty-state.tsx
Show resolved
Hide resolved
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
Show resolved
Hide resolved
const { program } = useProgram({ | ||
enabled: Boolean(currentArea === "program" && defaultProgramId), | ||
}); |
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.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Verify the purpose of fetching unused program data.
The useProgram
hook is called but the program
data is never used in this component. None of the subsequent hooks (usePayoutsCount
, useProgramApplicationsCount
, etc.) appear to depend on this data being loaded first.
Is this intentional for caching/side effects, or should the fetched program
data be used somewhere in this component?
🏁 Script executed:
#!/bin/bash
# Description: Check if program data is used elsewhere or if hooks depend on it
# Search for program variable usage after it's declared
rg -nP -A 50 'const \{ program \} = useProgram' apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
# Check if other hooks in this file accept program as a parameter
ast-grep --pattern $'use$_Count({
$$$
program: $_,
$$$
})'
Length of output: 1725
Remove or utilize unused program
data from useProgram
The program
object returned by useProgram
at lines 490–492 is never referenced in this component. Either remove the useProgram
call (and its options) or apply the program
data where needed (e.g., pass its id to dependent hooks or render program details).
🤖 Prompt for AI Agents
In apps/web/ui/layout/sidebar/app-sidebar-nav.tsx around lines 490 to 492, the
useProgram call returns a `program` value that is never used; remove the unused
`useProgram` invocation and its options if program data is not needed, or
alternatively thread the `program` (or program.id) into the component where
required (for example pass to dependent hooks, conditional rendering, or props).
Update any tests or type usage accordingly and ensure any side-effect logic in
useProgram (like enabled flag) is preserved only if necessary; otherwise delete
the import/usage to eliminate the dead code.
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: 2
♻️ Duplicate comments (2)
apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-discovery-guide.tsx (1)
16-23
: Fix hook order before early return.
useState
andusePartnerDiscoveryRequirements
sit behind thepartner.discoverableAt
guard, so hook order changes once a partner becomes discoverable and React throws. Call the hooks unconditionally, then short-circuit on the guard.-export function ProfileDiscoveryGuide({ partner }: { partner: PartnerProps }) { - if (partner.discoverableAt) return null; - - const [isExpanded, setIsExpanded] = useState(false); - - const tasks = usePartnerDiscoveryRequirements(); - - if (!tasks) return null; +export function ProfileDiscoveryGuide({ partner }: { partner: PartnerProps }) { + const [isExpanded, setIsExpanded] = useState(false); + + const tasks = usePartnerDiscoveryRequirements(); + + if (partner.discoverableAt || !tasks) return null;apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (1)
687-697
: Fix SWR typing for partner fetch.The SWR hook is typed as
useSWR<NetworkPartnerProps>
, but the code at line 696 accessesfetchedPartners?.[0]
, indicating the API returns an array. Update the generic type toNetworkPartnerProps[]
to match the actual response shape.Apply this diff:
- const { data: fetchedPartners, isLoading } = useSWR<NetworkPartnerProps>( + const { data: fetchedPartners, isLoading } = useSWR<NetworkPartnerProps[]>(
🧹 Nitpick comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (3)
245-245
: Clarify the conditional logic.The condition
!partners || partners?.length
is unclear. It evaluates to true whenpartners
is undefined or when the array has any items. The intent appears to be "show content if loading or if there are partners."Apply this diff for clarity:
- ) : !partners || partners?.length ? ( + ) : !partners || partners.length > 0 ? (Or use explicit boolean conversion:
- ) : !partners || partners?.length ? ( + ) : !partners || partners.length > 0 ? (
259-296
: Consider wrapping optimistic update logic in a helper.The inline
mutatePartners
call with complex optimistic update logic reduces readability. While the@ts-ignore
comment explains the SWR typing limitation, extracting this pattern into a reusable helper would improve maintainability and reduce repetition if this pattern is used elsewhere.Consider creating a helper function:
async function togglePartnerStar( partnerId: string, starred: boolean, currentPartners: NetworkPartnerProps[], ) { const result = await updateDiscoveredPartner({ workspaceId: workspaceId!, partnerId, starred, }); if (!result?.data) { toast.error("Failed to star partner"); throw new Error("Failed to star partner"); } return { result: result.data, optimisticData: currentPartners.map((p) => p.id === partnerId ? { ...p, starredAt: starred ? new Date() : null } : p, ), }; }Then use it in the mutate call with proper typing.
581-610
: Clarify the effect dependency chain.The effects at lines 581-586 and 605-610 form a complex dependency chain:
- When
items
change orisReady
is false, reset state and setshownItems = items
- After render, check overflow and trim
shownItems
(lines 588-601)- On resize, if overflow detected, reset
isReady
to trigger the cycle again (lines 605-610)This works but is difficult to follow. Consider adding comments or simplifying the logic.
Add inline comments to document the flow:
useEffect(() => { + // Reset shown items when items change or we're not ready if (isReady) return; setIsReady(false); setShownItems(items); }, [items, isReady]); useEffect(() => { if (!containerRef.current) return; - // Determine if we need to show less items + // Progressively hide items until content fits container width if ( shownItems?.length && containerRef.current.scrollWidth > containerRef.current.clientWidth ) { setIsReady(false); setShownItems(shownItems?.slice(0, -1)); } else { setIsReady(true); } }, [shownItems]); - // Show less items if needed after resizing + // Reset ready state on resize to re-trigger overflow check const entry = useResizeObserver(containerRef);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/page-client.tsx
(3 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-discovery-guide.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
(1 hunks)apps/web/lib/actions/partners/update-partner-profile.ts
(6 hunks)apps/web/lib/partners/discoverability.ts
(1 hunks)apps/web/lib/zod/schemas/partner-network.ts
(1 hunks)apps/web/scripts/migrations/backfill-discoverableat.ts
(1 hunks)apps/web/ui/partners/partner-info-cards.tsx
(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/lib/zod/schemas/partner-network.ts
- apps/web/scripts/migrations/backfill-discoverableat.ts
🧰 Additional context used
🧬 Code graph analysis (6)
apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/page-client.tsx (7)
apps/web/lib/swr/use-partner-profile.ts (1)
usePartnerProfile
(6-30)apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/use-partner-discovery-requirements.ts (1)
usePartnerDiscoveryRequirements
(7-27)apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-discovery-guide.tsx (1)
ProfileDiscoveryGuide
(16-120)apps/web/ui/partners/merge-accounts/merge-partner-accounts-modal.tsx (1)
useMergePartnerAccountsModal
(153-173)apps/web/lib/actions/partners/update-partner-profile.ts (1)
updatePartnerProfileAction
(45-244)packages/ui/src/icons/nucleo/user-search.tsx (1)
UserSearch
(3-55)packages/ui/src/popover.tsx (1)
Popover
(25-102)
apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-discovery-guide.tsx (4)
apps/web/lib/types.ts (1)
PartnerProps
(441-441)apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/use-partner-discovery-requirements.ts (1)
usePartnerDiscoveryRequirements
(7-27)packages/ui/src/progress-circle.tsx (1)
ProgressCircle
(3-55)packages/ui/src/icons/nucleo/chevron-up.tsx (1)
ChevronUp
(3-24)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (8)
apps/web/lib/swr/use-network-partners-count.ts (1)
useNetworkPartnersCount
(8-54)apps/web/lib/actions/partners/update-discovered-partner.ts (1)
updateDiscoveredPartnerAction
(9-44)apps/web/lib/zod/schemas/partner-network.ts (1)
PARTNER_NETWORK_MAX_PAGE_SIZE
(33-33)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/use-partner-network-filters.tsx (1)
usePartnerNetworkFilters
(12-174)apps/web/ui/partners/network-partner-sheet.tsx (1)
NetworkPartnerSheet
(126-149)apps/web/lib/partners/online-presence.ts (1)
ONLINE_PRESENCE_FIELDS
(29-103)apps/web/lib/partners/partner-profile.ts (2)
industryInterestsMap
(162-166)salesChannelsMap
(260-262)packages/ui/src/hooks/use-resize-observer.ts (1)
useResizeObserver
(8-29)
apps/web/lib/actions/partners/update-partner-profile.ts (2)
apps/web/lib/partners/discoverability.ts (1)
getPartnerDiscoveryRequirements
(9-54)packages/utils/src/constants/main.ts (2)
ACME_PROGRAM_ID
(77-77)APP_DOMAIN_WITH_NGROK
(20-25)
apps/web/ui/partners/partner-info-cards.tsx (6)
apps/web/lib/types.ts (2)
EnrolledPartnerExtendedProps
(459-461)NetworkPartnerProps
(453-453)apps/web/lib/swr/use-group.ts (1)
useGroup
(7-43)apps/web/lib/zod/schemas/groups.ts (1)
DEFAULT_PARTNER_GROUP
(15-19)packages/utils/src/functions/time-ago.ts (1)
timeAgo
(3-32)apps/web/ui/partners/conversion-score-icon.tsx (1)
ConversionScoreIcon
(6-83)apps/web/ui/partners/partner-network/conversion-score-tooltip.tsx (1)
ConversionScoreTooltip
(10-66)
apps/web/lib/partners/discoverability.ts (2)
apps/web/lib/types.ts (1)
PartnerProps
(441-441)apps/web/lib/partners/online-presence.ts (2)
PartnerOnlinePresenceFields
(13-27)ONLINE_PRESENCE_FIELDS
(29-103)
🪛 Biome (2.1.2)
apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-discovery-guide.tsx
[error] 19-19: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 21-21: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ 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 (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (1)
456-472
: Fix inconsistent null/undefined checking.Line 458 filters out items where
text !== null
, but line 462 checkstext !== undefined
. This inconsistency could cause rendering issues iftext
isnull
(passed through filter) but the check expectsundefined
.Apply this diff to use consistent checking:
{basicFields - .filter(({ text }) => text !== null) + .filter(({ text }) => text !== undefined) .map(({ id, icon, text, wrapper: Wrapper = "div" }) => (Or align both checks to use null:
{text !== undefined ? ( + {text !== null ? (
Likely an incorrect or invalid review comment.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
Show resolved
Hide resolved
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 (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (2)
703-713
: Fix SWR typing for partner fetch.The past review comment correctly identified that
useSWR<NetworkPartnerProps>
should beuseSWR<NetworkPartnerProps[]>
since line 712 accessesfetchedPartners?.[0]
, proving the API returns an array.Apply this diff to fix the typing:
- const { data: fetchedPartners, isLoading } = useSWR<NetworkPartnerProps>( + const { data: fetchedPartners, isLoading } = useSWR<NetworkPartnerProps[]>(
105-113
: Removeas any
type assertion.The past review comment correctly identified that the
as any
assertion on line 140 bypasses TypeScript's discriminated union type checking.Apply this diff to properly handle the state update:
- setIsOpen={(open) => - setDetailsSheetState((s) => ({ ...s, open }) as any) - } + setIsOpen={(open) => { + if (!open) { + setDetailsSheetState({ open: false, partnerId: null }); + } else if (detailsSheetState.partnerId) { + setDetailsSheetState({ open: true, partnerId: detailsSheetState.partnerId }); + } + }}Alternatively, simplify the state type:
- const [detailsSheetState, setDetailsSheetState] = useState< - | { open: false; partnerId: string | null } - | { open: true; partnerId: string } - >({ open: false, partnerId: null }); + const [detailsSheetState, setDetailsSheetState] = useState<{ + open: boolean; + partnerId: string | null; + }>({ open: false, partnerId: null });Also applies to: 139-141
apps/web/app/(ee)/api/network/partners/route.ts (1)
142-153
: Fix division by zero in conversion-rate aggregation.The past review comment correctly identified that
COALESCE(SUM(conversions) / SUM(clicks), 0)
on line 146 will raise a division-by-zero error whenSUM(clicks)
is 0. UseNULLIF
to handle this case.Apply this diff:
- COALESCE(SUM(conversions) / SUM(clicks), 0) as conversionRate, + SUM(conversions) / NULLIF(SUM(clicks), 0) as conversionRate,
🧹 Nitpick comments (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (2)
245-245
: Clarify boolean logic for partner rendering.The condition
!partners || partners?.length
is correct but hard to parse. It renders the grid when partners is undefined/null (loading) or when partners has items, but shows empty state when partners is an empty array.Consider adding a comment or extracting to a named variable for clarity:
+ const shouldShowGrid = !partners || partners.length > 0; - ) : !partners || partners?.length ? ( + ) : shouldShowGrid ? (
259-296
: Document the type assertion more clearly.The
@ts-ignore
comment on line 260 mentions SWR typing limitations. The implementation is correct, but the comment could be more specific.Consider expanding the comment:
- // @ts-ignore SWR doesn't seem to have proper typing for partial data results w/ `populateCache` + // @ts-ignore - SWR's types don't properly infer the mutator return type when using populateCache with partial updates async () => {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/app/(ee)/api/network/partners/route.ts
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/use-partner-network-filters.tsx
(1 hunks)apps/web/scripts/migrations/backfill-discoverableat.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/scripts/migrations/backfill-discoverableat.ts
- apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/use-partner-network-filters.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/app/(ee)/api/network/partners/route.ts (5)
apps/web/lib/auth/workspace.ts (1)
withWorkspace
(42-436)apps/web/lib/api/errors.ts (1)
DubApiError
(75-92)apps/web/lib/zod/schemas/partner-network.ts (2)
getNetworkPartnersQuerySchema
(41-73)NetworkPartnerSchema
(86-124)packages/utils/src/constants/main.ts (1)
ACME_PROGRAM_ID
(77-77)apps/web/lib/actions/partners/get-conversion-score.ts (1)
getConversionScore
(4-18)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (9)
apps/web/lib/swr/use-network-partners-count.ts (1)
useNetworkPartnersCount
(8-54)apps/web/lib/types.ts (1)
NetworkPartnerProps
(453-453)apps/web/lib/actions/partners/update-discovered-partner.ts (1)
updateDiscoveredPartnerAction
(9-44)apps/web/lib/zod/schemas/partner-network.ts (1)
PARTNER_NETWORK_MAX_PAGE_SIZE
(33-33)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/use-partner-network-filters.tsx (1)
usePartnerNetworkFilters
(12-174)apps/web/ui/partners/network-partner-sheet.tsx (1)
NetworkPartnerSheet
(126-149)apps/web/lib/partners/online-presence.ts (1)
ONLINE_PRESENCE_FIELDS
(29-103)apps/web/lib/partners/partner-profile.ts (2)
industryInterestsMap
(162-166)salesChannelsMap
(260-262)packages/ui/src/hooks/use-resize-observer.ts (1)
useResizeObserver
(8-29)
⏰ 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
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/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (1)
703-713
: Fix SWR typing for partner fetch.The hook types
fetchedPartners
asNetworkPartnerProps
but indexes it as an array on line 712 (fetchedPartners?.[0]
). This type mismatch will cause TypeScript errors.Apply this diff:
- const { data: fetchedPartners, isLoading } = useSWR<NetworkPartnerProps>( + const { data: fetchedPartners, isLoading } = useSWR<NetworkPartnerProps[]>(
🧹 Nitpick comments (5)
apps/web/lib/middleware/utils/app-redirect.ts (1)
64-67
: Inconsistency between AI summary and code.The AI-generated summary states the exclusion path changed from "/directory" to "/network", but the visible comment on line 64 mentions "/applications". This discrepancy suggests either:
- The summary is incorrect about which paths were involved in the change
- The change affects multiple paths not fully reflected in the comment
- There's a mismatch between what was changed and what was documented
The code logic itself is correct—the regex pattern
(pn_[^\/]+)
properly excludes all non-partner-ID paths (including /applications, /network, and /directory).Optionally, consider making the comment more comprehensive to clarify all special paths being excluded:
- // Only applies when partnerId starts with "pn_" (exclude /applications) + // Only applies when partnerId starts with "pn_" (excludes special paths like /applications, /network, etc.)apps/web/lib/actions/partners/invite-partner-from-network.ts (4)
23-39
: Consider pre-validating tenantId enrollment.The partner query correctly filters out email-based enrollments, but
createAndEnrollPartner
also checks for tenantId enrollment (see line 52-61 in create-and-enroll-partner.ts). If the partner has a tenantId that's already enrolled in this program, the action will fail later with a less helpful error.Consider adding tenantId enrollment check to the partner query:
prisma.partner.findFirst({ where: { id: partnerId, programs: { none: { programId, + OR: [ + { partner: { email: { not: null } } }, + { tenantId: { not: null } }, + ], }, }, }, }),Alternatively, enhance the error message on line 42 to indicate which enrollment type (email or tenantId) caused the conflict.
41-42
: Clarify error message.The error message mentions "already enrolled in this program," but the query on line 29-38 already filters out enrolled partners using
programs: { none: { programId } }
. This condition can only trigger if the partner doesn't exist or lacks an email.Update the error message to reflect the actual failure cases:
-throw new Error("Partner not found or already enrolled in this program."); +throw new Error("Partner not found or does not have an email address.");
59-75
: Consider preserving initial invitation timestamp.The upsert updates
invitedAt
to the current timestamp even if the partner was previously invited. This overwrites the original invitation date.If you need to track the original invitation date, consider preserving it:
update: { - invitedAt: new Date(), + // Only update invitedAt if it was null (dismissed and re-invited) + invitedAt: { + set: new Date(), + }, },Or add a
lastInvitedAt
field to track re-invitations while preserving the original timestamp.
77-108
: Consider logging side effect failures.The use of
Promise.allSettled
withwaitUntil
correctly allows side effects to execute without blocking the response. However, failed side effects (email or audit log) will fail silently.Add error logging to track failures:
waitUntil( - Promise.allSettled([ + Promise.allSettled([ sendEmail({ subject: `${program.name} invited you to join on Dub Partners`, variant: "notifications", to: partner.email, react: PartnerInvite({ email: partner.email, program: { name: program.name, slug: program.slug, logo: program.logo, }, }), }), recordAuditLog({ workspaceId: workspace.id, programId, action: "partner.invited", description: `Partner ${enrolledPartner.id} invited from network`, actor: user, targets: [ { type: "partner", id: enrolledPartner.id, metadata: enrolledPartner, }, ], }), - ]), + ]).then((results) => { + results.forEach((result, idx) => { + if (result.status === "rejected") { + console.error( + `Side effect ${idx === 0 ? 'email' : 'audit log'} failed:`, + result.reason + ); + } + }); + }), );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
(1 hunks)apps/web/lib/actions/partners/invite-partner-from-network.ts
(1 hunks)apps/web/lib/actions/partners/update-discovered-partner.ts
(1 hunks)apps/web/lib/api/create-id.ts
(1 hunks)apps/web/lib/middleware/utils/app-redirect.ts
(1 hunks)packages/prisma/schema/network.prisma
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/prisma/schema/network.prisma
🧰 Additional context used
🧬 Code graph analysis (3)
apps/web/lib/actions/partners/invite-partner-from-network.ts (6)
apps/web/lib/actions/safe-action.ts (1)
authActionClient
(33-82)apps/web/lib/zod/schemas/partner-network.ts (1)
invitePartnerFromNetworkSchema
(133-137)apps/web/lib/api/partners/create-and-enroll-partner.ts (1)
createAndEnrollPartner
(27-196)apps/web/lib/api/create-id.ts (1)
createId
(66-71)packages/email/src/index.ts (1)
sendEmail
(6-29)apps/web/lib/api/audit-logs/record-audit-log.ts (1)
recordAuditLog
(47-73)
apps/web/lib/actions/partners/update-discovered-partner.ts (3)
apps/web/lib/actions/safe-action.ts (1)
authActionClient
(33-82)apps/web/lib/zod/schemas/partner-network.ts (1)
updateDiscoveredPartnerSchema
(126-131)apps/web/lib/api/create-id.ts (1)
createId
(66-71)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (8)
apps/web/lib/swr/use-network-partners-count.ts (1)
useNetworkPartnersCount
(8-54)apps/web/lib/types.ts (1)
NetworkPartnerProps
(453-453)apps/web/lib/actions/partners/update-discovered-partner.ts (1)
updateDiscoveredPartnerAction
(10-46)apps/web/lib/zod/schemas/partner-network.ts (1)
PARTNER_NETWORK_MAX_PAGE_SIZE
(33-33)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/use-partner-network-filters.tsx (1)
usePartnerNetworkFilters
(12-174)apps/web/ui/partners/network-partner-sheet.tsx (1)
NetworkPartnerSheet
(126-149)apps/web/lib/partners/online-presence.ts (1)
ONLINE_PRESENCE_FIELDS
(29-103)packages/ui/src/hooks/use-resize-observer.ts (1)
useResizeObserver
(8-29)
⏰ 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/lib/api/create-id.ts (1)
19-19
: Approve "dpn_" prefix addition: The new prefix is used inupdate-discovered-partner.ts
andinvite-partner-from-network.ts
, and follows the established pattern.apps/web/lib/actions/partners/invite-partner-from-network.ts (3)
1-13
: LGTM!Imports are clean and all necessary dependencies are included for the server action functionality.
15-21
: LGTM!Action setup follows the standard pattern with proper authentication and schema validation. Context extraction is clean and uses the workspace's default program ID.
47-57
: LGTM!The enrollment flow correctly uses
skipEnrollmentCheck: true
since the query already verified non-enrollment, and appropriately sets status to "invited" for the invitation workflow.apps/web/lib/actions/partners/update-discovered-partner.ts (1)
9-46
: LGTM!The server action correctly implements the upsert logic for starring/dismissing partners. The conditional update logic (lines 33-38) ensures fields are only modified when explicitly provided, while the create path properly initializes both fields.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (2)
258-296
: LGTM!The optimistic update implementation correctly uses SWR's
populateCache
pattern. The mutation function returns the action result ({ starredAt, ignoredAt }
), whichpopulateCache
merges with the existing partner data. The@ts-ignore
on line 260 is warranted due to SWR's complex generic constraints aroundpopulateCache
.
321-572
: LGTM!The
PartnerCard
component is well-structured with properly memoized data transformations, correct click event handling (including interactive child detection), and appropriate loading states throughout.
Summary by CodeRabbit
New Features
Improvements