+
Skip to content

Conversation

TWilson023
Copy link
Collaborator

@TWilson023 TWilson023 commented Sep 24, 2025

Summary by CodeRabbit

  • New Features

    • Partner Network (Enterprise): discover, filter (industry, channels, earnings, country), star/dismiss, invite/recruit, tabbed views, counts, pagination, detailed partner sheet with keyboard shortcuts.
    • Partner Discovery UX: discovery progress guide, Discoverable toggle, upsell flow and Partner Discovery page.
    • Partner actions: invite from network and update discovered partner.
  • Improvements

    • Conversion score visuals & tooltip, centralized online-presence display, improved empty/loader states, optimistic updates and retained previous data, plan gating for discovery, sidebar navigation updates.

Copy link
Contributor

vercel bot commented Sep 24, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Oct 6, 2025 3:29am

Copy link
Contributor

coderabbitai bot commented Sep 24, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
API: Partner Network Endpoints
apps/web/app/(ee)/api/network/partners/route.ts, apps/web/app/(ee)/api/network/partners/count/route.ts
New enterprise-gated GET endpoints: list partners (raw SQL, filters, pagination, conversionScore) and count/group partners (status/country).
Server Actions: Network
apps/web/lib/actions/partners/invite-partner-from-network.ts, apps/web/lib/actions/partners/update-discovered-partner.ts
New server actions to invite a partner (enroll/upsert + email + audit) and to star/dismiss discovered partners (upsert DiscoveredPartner).
Server Action: Update Partner Profile
apps/web/lib/actions/partners/update-partner-profile.ts
Adds discoverable input handling, sets/clears discoverableAt based on eligibility, recomputes discovery requirements, and adjusts cache invalidation flows.
Schemas: Zod (Partner Network & Entities)
apps/web/lib/zod/schemas/partner-network.ts, apps/web/lib/zod/schemas/partners.ts, apps/web/lib/zod/schemas/programs.ts
New partner-network schemas (queries, counts, NetworkPartnerSchema, conversion scores); added discoverableAt to PartnerSchema and partnerNetworkEnabledAt to ProgramSchema.
Types & Utilities
apps/web/lib/types.ts, apps/web/lib/plan-capabilities.ts, apps/web/lib/actions/partners/get-conversion-score.ts
New type aliases NetworkPartnerProps and PartnerConversionScore; canDiscoverPartners plan flag; conversion score calculator.
SWR Hooks
apps/web/lib/swr/use-network-partners-count.ts, apps/web/lib/swr/use-partner-profile.ts, apps/web/lib/swr/use-program.ts
New useNetworkPartnersCount hook; keepPreviousData for partner profile; useProgram gains enabled option.
Partner Profile (Dashboard) Enhancements
apps/web/app/(ee)/partners.dub.co/(dashboard)/auth.tsx, .../profile/page-client.tsx, .../profile/profile-discovery-guide.tsx, .../profile/use-partner-discovery-requirements.ts, .../profile/about-you-form.tsx, .../profile/how-you-work-form.tsx, .../profile/profile-details-form.tsx, .../profile/settings-row.tsx
Adds discovery guide component and hook, discoverable toggle with optimistic update, SWR revalidation on profile edits, SettingsRow id prop, and auth loading shape change (partner instead of loading).
Program: Partner Network Pages & Hooks
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/layout.tsx, .../network/page.tsx, .../network/page-client.tsx, .../network/use-partner-network-filters.tsx, .../network/network-empty-state.tsx, .../network/network-upsell.tsx
New gated layout, main network page client with tabs/filters/pagination, partner sheet integration, filters hook, empty/upsell UIs.
Program: Directory Page Update
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/directory/page.tsx
Replaces Coming Soon with "Partner Discovery" client page wrapper.
UI Components: Network & Partners
apps/web/ui/partners/network-partner-sheet.tsx, apps/web/ui/partners/partner-info-cards.tsx, apps/web/ui/partners/partner-info-group.tsx, apps/web/ui/partners/partner-application-sheet.tsx, apps/web/ui/partners/partner-sheet-tabs.tsx, apps/web/ui/partners/conversion-score-icon.tsx, apps/web/ui/partners/partner-network/conversion-score-tooltip.tsx, apps/web/ui/partners/online-presence-summary.tsx, apps/web/ui/partners/partner-about.tsx
New NetworkPartnerSheet (invite/ignore flows), refactor PartnerInfoCards for network vs enrolled contexts, rename tabs export, add conversion score icon/tooltip, centralize online-presence data, and narrow several component prop types.
Online Presence & Discoverability Utilities
apps/web/lib/partners/online-presence.ts, apps/web/lib/partners/discoverability.ts
New ONLINE_PRESENCE_FIELDS descriptor and PartnerOnlinePresenceFields type; discovery requirements helper and minimum commissions constant.
Navigation & Middleware
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx, apps/web/lib/middleware/utils/app-redirect.ts
Adds Program nav entries (Groups, Partner Network); small redirect comment/path tweak.
Prisma Schema & Migrations
packages/prisma/schema/partner.prisma, packages/prisma/schema/program.prisma, packages/prisma/schema/network.prisma, apps/web/scripts/migrations/backfill-discoverableat.ts
Add discoverableAt to Partner, DiscoveredPartner model, Program.partnerNetworkEnabledAt, ProgramIndustryInterest model, IndustryInterest enum; backfill script to populate discoverableAt.
UI Library Additions
packages/ui/src/icons/nucleo/*, packages/ui/src/progress-circle.tsx, apps/web/ui/shared/animated-empty-state.tsx
New nucleo icons (chevron-up, star, star-fill, user-search) and exports; progress-circle uses CSS var for track; AnimatedEmptyState gains cardCount and cardClassName props.
Modals
apps/web/ui/modals/change-group-modal.tsx
Narrows partners prop type and conditionally shows email.

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
Loading
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
Loading
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")
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

Possibly related PRs

Poem

I nibble through the code tonight,
New network tunnels gleam with light.
Stars to mark and invites to fling,
Filters hop and dropdowns sing.
Discoverable carrots in sight —
Rabbit cheers: deploy it right! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title “Partner Network” succinctly captures the primary feature being introduced—adding comprehensive partner network support across API, UI, and schema—without extraneous detail or ambiguity.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch partner-network

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ 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 single PartnerNetworkPartnerProps, but line 700 accesses it as an array with fetchedPartners?.[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 for populateCache 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 to false 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4dd6224 and 85445e9.

📒 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:

  1. Program-level feature flag (partnerNetworkEnabledAt)
  2. 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).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 85445e9 and 66b0d1c.

📒 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 and UserPlus are correctly added and utilized within the component.

Also applies to: 37-37


490-492: Verify preloading behavior is as intended.

The program variable from useProgram 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 when currentArea is "program" and defaultProgramId 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.

Comment on lines 185 to 197
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)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 into URLSearchParams turns optional fields into "undefined" and flattens arrays incorrectly, so /count rejects the request. Iterate entries, skip undefined/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 types useSWR as NetworkPartnerProps. 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 when SUM(clicks) is 0. The COALESCE wrapper won't help because the division error occurs before COALESCE can act.

Apply the previously suggested fix:

-          COALESCE(SUM(conversions) / SUM(clicks), 0) as conversionRate
+          SUM(conversions) / NULLIF(SUM(clicks), 0) as conversionRate

This will return NULL when clicks are zero, which COALESCE can then handle safely.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66b0d1c and c88f5e6.

📒 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: Indexing discoverableAt 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 corresponding partner 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 to selectedGroupId and DEFAULT_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 plain div) to conditionally wrap elements with components like ConversionScoreTooltip 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%) to excellent (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 return NULL, the conversionRate value could be NULL/undefined.

Please verify that getConversionScore handles NULL/undefined inputs gracefully. From the relevant code snippets, the function signature is:

export function getConversionScore(
  conversionRate: number,
): PartnerConversionScore

The function expects a number, so passing NULL/undefined would cause a type error. After fixing Line 84 to use NULLIF, you'll need to handle the NULL case here:

conversionScore: partner.conversionRate != null 
  ? getConversionScore(partner.conversionRate) 
  : "unknown",

119-119: Verify NULL handling on conversionRate filter
The condition conversionRate < 1 excludes rows where conversionRate 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.

Comment on lines +121 to +133
const countries = await prisma.partner.groupBy({
by: ["country"],
_count: true,
where: { ...commonWhere, ...statusWheres[status || "discover"] },
orderBy: {
_count: {
country: "desc",
},
},
});

return NextResponse.json(countries);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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!)}`
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
? `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.

Comment on lines +13 to +14
@@unique([programId, partnerId])
@@index(partnerId)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
@@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).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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-null discoverableAt 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

📥 Commits

Reviewing files that changed from the base of the PR and between 20299cb and 2b3695a.

📒 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 uses useId 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.

Comment on lines +490 to +492
const { program } = useProgram({
enabled: Boolean(currentArea === "program" && defaultProgramId),
});
Copy link
Contributor

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 and usePartnerDiscoveryRequirements sit behind the partner.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 accesses fetchedPartners?.[0], indicating the API returns an array. Update the generic type to NetworkPartnerProps[] 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 when partners 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:

  1. When items change or isReady is false, reset state and set shownItems = items
  2. After render, check overflow and trim shownItems (lines 588-601)
  3. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2b3695a and b7c680b.

📒 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 checks text !== undefined. This inconsistency could cause rendering issues if text is null (passed through filter) but the check expects undefined.

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (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 be useSWR<NetworkPartnerProps[]> since line 712 accesses fetchedPartners?.[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: Remove as 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 when SUM(clicks) is 0. Use NULLIF 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

📥 Commits

Reviewing files that changed from the base of the PR and between b7c680b and dbc25d7.

📒 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (1)

703-713: Fix SWR typing for partner fetch.

The hook types fetchedPartners as NetworkPartnerProps 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 with waitUntil 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

📥 Commits

Reviewing files that changed from the base of the PR and between dbc25d7 and 5c86fb7.

📒 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 in update-discovered-partner.ts and invite-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 }), which populateCache merges with the existing partner data. The @ts-ignore on line 260 is warranted due to SWR's complex generic constraints around populateCache.


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.

@steven-tey steven-tey merged commit b69404d into main Oct 6, 2025
8 checks passed
@steven-tey steven-tey deleted the partner-network branch October 6, 2025 03:37
This was referenced Oct 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载