Skip to content

Conversation

@TWilson023
Copy link
Collaborator

@TWilson023 TWilson023 commented Oct 20, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a program marketplace where partners can browse, filter, and apply to available programs
    • Introduced featured programs carousel and program discovery with filtering by category, reward type, and status
    • Added product category selection during program creation and configuration
    • New application settings management for programs
  • UI Improvements

    • Enhanced sidebar navigation with marketplace access
    • New program detail pages with enrollment status tracking
    • Improved program card displays with rewards and category information
@vercel
Copy link
Contributor

vercel bot commented Oct 20, 2025

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

Project Deployment Preview Updated (UTC)
dub Error Error Oct 29, 2025 8:50pm
@TWilson023 TWilson023 changed the base branch from main to network-v2 October 20, 2025 16:48
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 20, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The PR introduces a Partner Network feature enabling partners to discover and apply for network programs via a marketplace. It adds API endpoints for querying network programs with filtering and pagination, implements partner eligibility checks based on minimum payouts and enrollments, creates a marketplace UI with featured programs and dynamic filtering, and extends the Program schema with marketplace-related fields.

Changes

Cohort / File(s) Summary
Network Program APIs
apps/web/app/(ee)/api/network/programs/route.ts, apps/web/app/(ee)/api/network/programs/[programSlug]/route.ts, apps/web/app/(ee)/api/network/programs/count/route.ts
Three new GET endpoints for retrieving network programs with filtering, pagination, sorting, and optional grouping by category/rewardType/status; includes partner eligibility validation via checkProgramNetworkRequirements.
Partner Eligibility
apps/web/app/(ee)/api/network/programs/check-program-network-requirements.ts
New utility function validating partner meets minimum thresholds (PROGRAM_NETWORK_PARTNER_MIN_PROGRAMS and PROGRAM_NETWORK_PARTNER_MIN_PAYOUTS).
Program Data Endpoints
apps/web/app/(ee)/api/programs/[programId]/route.ts, apps/web/app/(ee)/api/partner-profile/programs/[programId]/groups/[groupIdOrSlug]/route.ts
Enhanced GET endpoint for program retrieval with optional categories; new endpoint for fetching partner-profile program groups.
Marketplace Pages & Layout
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/{layout,page,page-client}.tsx
New marketplace layout with eligibility gating and client-side page component handling featured programs, filtering, sorting, pagination, and empty states.
Program Detail & Featured Pages
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/[programSlug]/{page,page-client}.tsx, apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/featured-programs.tsx
Program detail view fetching from network programs API with action controls (accept/apply); featured programs carousel with autoplay.
Marketplace Filtering & UI
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/{program-card,program-sort,marketplace-empty-state,use-program-network-filters}.tsx
Program card component for display with rewards/categories; sort selector for marketplace; empty state with filter-clearing option; custom hook composing filter state from counts.
Category Support
apps/web/lib/partners/categories.ts, apps/web/ui/partners/program-category-select.tsx, apps/web/ui/partners/program-network/program-category.tsx, apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/form.tsx
New categories data with icon mappings; ProgramCategorySelect combobox component; ProgramCategory badge renderer; category field wired into program onboarding form.
Program Schemas & Types
apps/web/lib/zod/schemas/program-network.ts, apps/web/lib/zod/schemas/programs.ts, apps/web/lib/zod/schemas/program-onboarding.ts, apps/web/lib/zod/schemas/groups.ts, apps/web/lib/types.ts
New NetworkProgramSchema and NetworkProgramExtendedSchema; getNetworkProgramsQuerySchema and getNetworkProgramsCountQuerySchema for validation; extended ProgramSchema with marketplaceEnabledAt, marketplaceFeaturedAt, and categories; new NetworkProgramProps and NetworkProgramExtendedProps types.
Program Data Access
apps/web/lib/api/programs/get-program-or-throw.ts
Enhanced to optionally include categories in query result.
Program Actions
apps/web/lib/actions/partners/create-program.ts, apps/web/lib/actions/partners/update-auto-approve-partners.ts
Program creation now handles optional category field upserting; updateAutoApprovePartnersAction renamed to updateApplicationSettingsAction with marketplaceEnabled and category fields for granular updates.
SWR Hooks
apps/web/lib/swr/use-network-programs-count.ts, apps/web/lib/swr/use-partner-payouts-count.ts, apps/web/lib/swr/use-program-enrollment.ts
New hook for fetching network program counts with grouping; refactored payouts count hook with generic type and configurable include params; program enrollment hook now supports enabled flag.
UI Components
apps/web/ui/modals/application-settings-modal.tsx, apps/web/ui/partners/program-application-sheet.tsx, apps/web/ui/partners/lander/lander-hero.tsx, apps/web/ui/partners/program-network/program-reward-icon.tsx
New modal for application settings (auto-approve, marketplace, category); program application sheet refactored with fetched group data and backDestination routing; LanderHero extended with heading component control and optional label; reward icon hover card component.
Status & Navigation
apps/web/ui/partners/partner-status-badges.ts, apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx, apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx
New ProgramNetworkStatusBadges mapping (approved→Enrolled, pending→Applied); sidebar nav wires enrolled programs and payouts counts for conditional Marketplace rendering; payout table fallback for undefined count.
Program Card Updates
apps/web/ui/partners/program-card.tsx
Removed exported ProgramEnrollmentStatusBadges constant.
Database Schema
packages/prisma/schema/program.prisma
Added marketplaceEnabledAt, marketplaceFeaturedAt, and marketplaceHeaderImage optional fields to Program model.
UI Library
packages/ui/src/combobox/index.tsx, packages/ui/src/icons/nucleo/{index,shop,sort-alpha-ascending,sort-alpha-descending,suitcase}.tsx
Combobox single-select path now uses optional chaining for setSelected; new icon exports for shop, sort directions, suitcase, and grid-layout.

Sequence Diagram

sequenceDiagram
    participant Partner as Partner
    participant UI as Marketplace UI
    participant API as API Routes
    participant DB as Database
    participant Auth as Auth Check

    Partner->>UI: Navigate to Marketplace
    UI->>Auth: Check eligibility via checkProgramNetworkRequirements
    Auth->>DB: Fetch partner payouts & enrollments count
    DB-->>Auth: Return counts
    alt Eligible
        Auth-->>UI: Pass
        UI->>API: GET /api/network/programs?filters...
        API->>DB: Query programs where marketplaceEnabledAt IS NOT NULL
        DB-->>API: Return filtered programs with rewards/categories
        API-->>UI: Return NetworkProgramSchema validated data
        UI->>UI: Display featured programs & filtered list
    else Not Eligible
        Auth-->>UI: Redirect to /programs
    end

    Partner->>UI: Click on Program Card
    UI->>API: GET /api/network/programs/{programSlug}
    API->>DB: Fetch program details with groups & categories
    DB-->>API: Return program data
    API-->>UI: Return NetworkProgramExtendedSchema
    UI->>UI: Display program detail with Apply/Accept buttons

    Partner->>UI: Click Apply/Accept
    UI->>API: POST application or accept action
    API->>DB: Update enrollment status
    DB-->>API: Confirm
    API-->>UI: Success response
    UI->>UI: Navigate to program dashboard
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱�� ~75 minutes

Key areas requiring careful attention:

  • Raw SQL fragments in apps/web/app/(ee)/api/network/programs/count/route.ts and route.ts for correct Prisma query composition and bigint handling
  • Complex filtering logic with dynamic SQL assembly across category, rewardType, status, and featured conditions
  • Partner eligibility checks and access control flow in network program endpoints
  • Marketplace state management spanning pagination, filtering, sorting, and real-time count updates across multiple components
  • updateApplicationSettingsAction refactoring with conditional field updates and altered audit logging behavior
  • Program application sheet refactoring to fetch group data dynamically and handle missing form fields gracefully

Possibly related PRs

Suggested reviewers

  • steven-tey

Poem

🐰 A marketplace hops into view,

Where partners discover programs new,

With filters and sorts, a carousel spins,

Featured and filtered, the browsing begins!

Categories badge each program with care,

Apply or enroll—the network's now fair! 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Program marketplace" is directly related to the primary changes in the changeset. The pull request implements a comprehensive marketplace feature for partners, including new API routes for discovering and filtering network programs, UI components for browsing and applying to programs, database schema updates to support marketplace configuration, and supporting utilities and hooks. The title clearly summarizes the main feature being added without being vague or misleading, though it could be slightly more specific (e.g., mentioning partners or the network context).

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.

@vercel
Copy link
Contributor

vercel bot commented Oct 20, 2025

Deployment failed with the following error:

Failed to create deployment for team_y0zuoaL8RHti4hQoucTdqRSG in project prj_bLzJgTPWhAyLJ6KKGaYemCx1vZVU: FetchError: request to https://76.76.21.112/v13/now/deployments?ownerId=team_y0zuoaL8RHti4hQoucTdqRSG&projectId=prj_bLzJgTPWhAyLJ6KKGaYemCx1vZVU&skipAutoDetectionConfirmation=1&teamId=team_y0zuoaL8RHti4hQoucTdqRSG&traceCarrier=%7B%22ot-baggage-webhookAt%22%3A%221760995188499%22%2C%22ot-baggage-senderUsername%22%3A%22gh.TWilson023%22%2C%22baggage%22%3A%22webhookAt%3D1760995188499%2CsenderUsername%3Dgh.TWilson023%22%2C%22x-datadog-trace-id%22%3A%222100967458407601301%22%2C%22x-datadog-parent-id%22%3A%22144857218407747344%22%2C%22x-datadog-sampling-priority%22%3A%222%22%2C%22x-datadog-tags%22%3A%22_dd.p.tid%3D68f6a77400000000%2C_dd.p.dm%3D-3%22%2C%22traceparent%22%3A%2200-68f6a774000000001d2822c5a30a5495-0202a2df66584f10-01%22%2C%22tracestate%22%3A%22dd%3Dt.tid%3A68f6a77400000000%3Bt.dm%3A-3%3Bs%3A2%3Bp%3A0202a2df66584f10%22%7D failed, reason: socket hang up
@TWilson023 TWilson023 marked this pull request as ready for review October 28, 2025 18:24
@TWilson023
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/ui/partners/program-application-sheet.tsx (1)

75-79: Fix success gating and submission flow; remove ts-ignore and avoid sending client-only fields

  • Success UI is tied to isSubmitSuccessful, which can turn true on early returns or after setError; this may show a false “Application submitted.”
  • Early return on missing partner/group data provides no feedback.
  • The ts-ignore + placeholder formData is unnecessary; default it in form and let RHF provide real values.
  • Don’t send termsAgreement to the action if the server schema doesn’t expect it.

Apply this consolidated patch:

@@
   const form = useForm<FormData>({
     defaultValues: {
-      termsAgreement: false,
+      termsAgreement: false,
+      // ensure formData exists even when no extra fields are rendered
+      formData: { fields: [] },
     },
   });
 
   const {
     register,
     handleSubmit,
     setError,
-    formState: { errors, isSubmitting, isSubmitSuccessful },
+    watch,
+    formState: { errors, isSubmitting },
   } = form;
+
+  const [isSuccess, setIsSuccess] = useState(false);
@@
   const { executeAsync } = useAction(createProgramApplicationAction, {
     onSuccess: () => {
       mutate(`/api/partner-profile/programs/${program!.slug}`);
       onSuccess?.();
+      setIsSuccess(true);
     },
   });
 
   const onSubmit = async (data: FormData) => {
-    if (!group || !program || !partner?.email || !partner.country) return;
+    if (!group || !program) {
+      setError("root.serverError", { message: "Program data not loaded." });
+      toast.error("Unable to load program details. Please refresh and try again.");
+      return;
+    }
+    if (!partner?.email || !partner?.country) {
+      setError("root.serverError", { message: "Partner profile incomplete." });
+      toast.error("Complete your profile (email & country) to apply.");
+      return;
+    }
 
-    const result = await executeAsync({
-      // @ts-ignore
-      formData: { fields: [] },
-      ...data,
+    // strip client-only fields from payload
+    const { termsAgreement: _omit, ...payload } = data;
+    const result = await executeAsync({
+      ...payload,
       email: partner.email,
-      name: partner.name,
+      name: partner.name ?? partner.email,
       country: partner.country,
       programId: program.id,
       groupId: group.id,
     });
 
     if (result?.serverError || result?.validationErrors) {
       setError("root.serverError", {
         message: "Failed to submit application",
       });
       toast.error(parseActionError(result, "Failed to submit application"));
+      return;
     }
   };
@@
-          className={cn(
+          className={cn(
             "flex h-full flex-col transition-opacity duration-200",
-            isSubmitSuccessful && "pointer-events-none opacity-0",
+            isSuccess && "pointer-events-none opacity-0",
           )}
           {...{
-            inert: isSubmitSuccessful,
+            inert: isSuccess,
           }}
         >
@@
-            isSubmitSuccessful
+            isSuccess
               ? "translate-y-0 opacity-100"
               : "pointer-events-none translate-y-4 opacity-0",
           )}
-          inert={!isSubmitSuccessful}
+          inert={!isSuccess}
         >

Also applies to: 81-87, 88-93, 95-116, 121-131, 233-240

apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/form.tsx (1)

106-107: Add category to button disabled check.

The submit button's disabled state doesn't check for the category field, even though the field has required: true validation (line 165). This creates an inconsistent UX where users can click submit without selecting a category, only to see a validation error.

Apply this diff:

  const buttonDisabled =
-   isSubmitting || isPending || !name || !url || !domain || !logo;
+   isSubmitting || isPending || !name || !url || !domain || !logo || !category;
🧹 Nitpick comments (33)
apps/web/ui/partners/partner-status-badges.ts (1)

62-72: LGTM! Implementation is correct.

The spread syntax correctly creates a variant of PartnerStatusBadges with contextually appropriate labels for program networks ("Enrolled" and "Applied"). The nested spreads preserve all other properties (variant, className, icon).

Consider these optional improvements for maintainability:

  1. Add TypeScript type definitions to ensure consistency across both badge configurations:
type StatusBadgeConfig = {
  label: string;
  variant: string;
  className: string;
  icon: React.ComponentType;
};

type StatusBadges = {
  pending: StatusBadgeConfig;
  approved: StatusBadgeConfig;
  rejected: StatusBadgeConfig;
  invited: StatusBadgeConfig;
  declined: StatusBadgeConfig;
  deactivated: StatusBadgeConfig;
  banned: StatusBadgeConfig;
  archived: StatusBadgeConfig;
};

export const PartnerStatusBadges: StatusBadges = {
  // ... existing implementation
};

export const ProgramNetworkStatusBadges: StatusBadges = {
  // ... existing implementation
};
  1. Add JSDoc comments to clarify usage context:
/**
 * Status badges for partner applications in workspace context.
 */
export const PartnerStatusBadges = { ... };

/**
 * Status badges for program enrollment in network marketplace context.
 * Inherits all statuses from PartnerStatusBadges with adjusted labels:
 * - "Approved" → "Enrolled"
 * - "Pending" → "Applied"
 */
export const ProgramNetworkStatusBadges = { ... };
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx (1)

173-173: Good defensive programming.

The fallback to 0 ensures rowCount always receives a defined numeric value, preventing potential issues when payoutsCount is undefined during loading or error states.

Optionally, consider using the nullish coalescing operator for more precise handling:

-    rowCount: payoutsCount || 0,
+    rowCount: payoutsCount ?? 0,

This would only default to 0 for null or undefined, though in practice || 0 works equally well here since legitimate count values are positive numbers.

apps/web/ui/partners/program-application-sheet.tsx (2)

223-229: Disable submit until terms are accepted (when termsUrl exists)

Prevents avoidable round-trips and aligns UX with validation.

-            <Button
+            <Button
               type="submit"
               variant="primary"
               text="Submit application"
-              disabled={isGroupLoading}
+              disabled={
+                isGroupLoading || (program.termsUrl ? !watch("termsAgreement") : false)
+              }
               loading={isSubmitting}
             />

26-27: Avoid duplicate DOM ids for the terms checkbox

Multiple open sheets will reuse id="termsAgreement". Use useId for uniqueness.

-import { Dispatch, SetStateAction, useState } from "react";
+import { Dispatch, SetStateAction, useId, useState } from "react";
@@
 function ProgramApplicationSheetContent({
@@
 }: ProgramApplicationSheetProps) {
@@
   const { partner } = usePartnerProfile();
+  const termsId = useId();
@@
-                  <input
+                  <input
                     type="checkbox"
-                    id="termsAgreement"
+                    id={termsId}
@@
-                  <label
-                    htmlFor="termsAgreement"
+                  <label
+                    htmlFor={termsId}
                     className="text-sm text-neutral-800"
                   >

Also applies to: 188-217

packages/prisma/schema/program.prisma (1)

67-70: Add marketplace indexes and consider Text for header image URL

  • Likely queries: WHERE marketplaceEnabledAt IS NOT NULL and ORDER BY marketplaceFeaturedAt DESC with pagination. Add indexes to avoid seq scans. Example:
@@index([marketplaceEnabledAt])
@@index([marketplaceFeaturedAt])
  • Header image URLs can exceed varchar defaults on some providers. For consistency with termsUrl/helpUrl, consider:
marketplaceHeaderImage String? @db.Text
apps/web/lib/swr/use-program-enrollment.ts (1)

8-12: Good SWR gating with enabled flag

Key is properly guarded; avoids unnecessary requests. Consider passing revalidateOnFocus: false if this endpoint is expensive.

Also applies to: 24-27

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/program-sort.tsx (1)

96-97: Chevron rotation may not trigger reliably

group-data-[state=open] assumes data-state on the button. Use openPopover state instead.

-        <ChevronDown className="size-4 flex-shrink-0 text-neutral-400 transition-transform duration-75 group-data-[state=open]:rotate-180" />
+        <ChevronDown
+          className={cn(
+            "size-4 flex-shrink-0 text-neutral-400 transition-transform duration-75",
+            openPopover && "rotate-180",
+          )}
+        />
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/applications-menu.tsx (2)

31-36: Align disabled state with program load

The standalone “Application settings” button should match the popover trigger behavior when program is unavailable.

-      <Button
+      <Button
         text="Application settings"
         onClick={() => setShowApplicationSettingsModal(true)}
         variant="secondary"
         className="hidden sm:flex"
+        disabled={!program}
       />

89-95: Add accessible name for icon-only trigger

Icon-only buttons need an accessible label.

-        <Button
+        <Button
           type="button"
           className="whitespace-nowrap px-2"
           variant="secondary"
           disabled={!program}
           icon={<ThreeDots className="size-4 shrink-0" />}
+          aria-label="Open applications menu"
+          title="Menu"
         />
apps/web/lib/swr/use-partner-payouts-count.ts (2)

19-23: Scope SWR key by partnerId to prevent cross-user cache bleed

If the session’s defaultPartnerId changes, the URL key stays identical. Include partnerId in the key (and query) so data revalidates correctly.

-  const { data: payoutsCount, error } = useSWR<T>(
-    partnerId &&
-      `/api/partner-profile/payouts/count${getQueryString(query, {
-        include: includeParams,
-      })}`,
+  const { data: payoutsCount, error } = useSWR<T>(
+    partnerId &&
+      `/api/partner-profile/payouts/count${getQueryString(
+        { ...(query || {}), partnerId },
+        { include: [...includeParams, "partnerId"] },
+      )}`,
     fetcher,
     {
       keepPreviousData: true,
     },
   );

If the API doesn’t accept partnerId, consider switching to a tuple key (e.g., [url, partnerId]) and updating fetcher accordingly.


30-34: Optional: expose mutate/isValidating for parity with other hooks

Helpful for manual refresh and loading states.

-  return {
-    payoutsCount,
-    error,
-    loading: !payoutsCount && !error,
-  };
+  const { isValidating, mutate } = ({} as any); // from SWR's return if you destructure above
+  return { payoutsCount, error, loading: !payoutsCount && !error, isValidating, mutate };
apps/web/lib/zod/schemas/program-onboarding.ts (1)

20-25: Single category vs. array elsewhere — confirm mapping

On onboarding you accept a single category (Category | null). Programs schema elsewhere uses categories: Category[]. Ensure create/update actions correctly map this to the join table and UI that expects arrays.

If you plan multi-select soon, consider aligning to z.array(z.nativeEnum(Category)).min(1) here to avoid future migrations.

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/page.tsx (1)

1-13: LGTM: page wrapper is clean

Optional: add route-level metadata for title/OG if desired.

apps/web/app/(ee)/api/network/programs/check-program-network-requirements.ts (1)

8-12: Add explicit return type for clarity and safer contracts.

Annotate the function to return Promise.

-export async function checkProgramNetworkRequirements({
+export async function checkProgramNetworkRequirements({
   partner,
 }: {
   partner: Pick<PartnerProps, "id">;
-}) {
+}): Promise<boolean> {
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/marketplace-empty-state.tsx (1)

15-23: Use semantic and ensure Esc actually clears filters.

Replace the styled span with a . Also verify a keydown Escape handler is wired where this component is used; otherwise the hint misleads.

-            <span className="text-content-default bg-bg-emphasis rounded-md px-1 py-0.5 text-xs font-semibold">
-              Esc
-            </span>{" "}
+            <kbd className="text-content-default bg-bg-emphasis rounded-md px-1 py-0.5 text-xs font-semibold">
+              Esc
+            </kbd>{" "}
             to clear all filters.

Also applies to: 41-47

apps/web/lib/zod/schemas/program-network.ts (1)

41-51: Deduplicate and normalize rewardType parsing.

Avoid duplicates and normalize to a string[] of allowed values.

 export const getNetworkProgramsQuerySchema = z
   .object({
     category: z.nativeEnum(Category).optional(),
-    rewardType: z
-      .union([z.string(), z.array(rewardTypeSchema)])
-      .transform((v) =>
-        Array.isArray(v)
-          ? v
-          : v.split(",").filter((v) => rewardTypes.includes(v as any)),
-      )
-      .optional(),
+    rewardType: z
+      .union([z.string(), z.array(rewardTypeSchema)])
+      .transform((v) =>
+        Array.isArray(v) ? v : v.split(",").filter(Boolean),
+      )
+      .transform((arr) =>
+        Array.from(new Set(arr.filter((v) => rewardTypes.includes(v as any)))),
+      )
+      .optional(),
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/use-program-network-filters.tsx (4)

81-83: Format labels with all underscores replaced.

-        getOptionLabel: (value) =>
-          categoriesMap[value]?.label || value.replace("_", " "),
+        getOptionLabel: (value) =>
+          categoriesMap[value]?.label || value.replaceAll("_", " "),

138-144: Normalize multi-select from URL and deduplicate values.

-  const multiFilters = useMemo(
-    () => ({
-      rewardType: searchParamsObj.rewardType?.split(",").filter(Boolean) ?? [],
-    }),
-    [searchParamsObj],
-  ) as Record<string, string[]>;
+  const multiFilters = useMemo(
+    () => ({
+      rewardType: Array.from(
+        new Set(searchParamsObj.rewardType?.split(",").filter(Boolean) ?? []),
+      ),
+    }),
+    [searchParamsObj],
+  ) as Record<string, string[]>;

158-171: Prevent duplicate values when selecting multi-filters.

   const onSelect = useCallback(
     (key: string, value: any) =>
       queryParams({
         set: Object.keys(multiFilters).includes(key)
           ? {
-              [key]: multiFilters[key].concat(value).join(","),
+              [key]: Array.from(new Set(multiFilters[key].concat(value))).join(
+                ",",
+              ),
             }
           : {
               [key]: value,
             },
         del: "page",
       }),

111-133: Return empty array for status options instead of null.

Keeps consumer components simpler and avoids null checks.

-        options:
-          statusCount?.map(({ status, _count }) => {
+        options:
+          statusCount?.map(({ status, _count }) => {
             // ...
-          }) ?? null,
+          }) ?? [],
apps/web/ui/partners/program-network/program-category.tsx (1)

16-19: Polish fallback label + minor a11y UX

  • Replace only-first-underscore with replaceAll for multi-word categories.
  • Add a title so truncated labels are discoverable.
-  const { icon: Icon, label } = categoryData ?? {
-    icon: CircleInfo,
-    label: category.replace("_", " "),
-  };
+  const { icon: Icon, label } = categoryData ?? {
+    icon: CircleInfo,
+    label: category.replaceAll("_", " "),
+  };
-      <span className="min-w-0 truncate text-sm font-medium">{label}</span>
+      <span title={label} className="min-w-0 truncate text-sm font-medium">
+        {label}
+      </span>

Also applies to: 21-21, 33-34

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/featured-programs.tsx (1)

28-29: Consider honoring reduced‑motion for autoplay

Optionally disable autoplay when the user prefers reduced motion. If @dub/ui’s Carousel doesn’t handle this internally, pass autoplay={false} when matchMedia('(prefers-reduced-motion: reduce)') is true.

apps/web/ui/partners/program-category-select.tsx (1)

2-3: Tighten Combobox types and avoid any casts; memoize options

Use a typed Option and useMemo for options to prevent reallocation and drops as Category | null.

-import { Combobox, ComboboxProps } from "@dub/ui";
+import { Combobox, ComboboxProps } from "@dub/ui";
+import { useMemo } from "react";
@@
-} & Omit<ComboboxProps<false, any>, "selected" | "options" | "onSelect">) {
+} & Omit<ComboboxProps<false, { value: Category; label: string; icon?: any }>, "selected" | "options" | "onSelect">) {
-  const selectedCategory = selected ? categoriesMap?.[selected] : null;
+  const selectedCategory = selected ? categoriesMap?.[selected] : null;
+  const options = useMemo(
+    () => categories.map(({ id, label, icon }) => ({ value: id, label, icon })),
+    [],
+  );
@@
-    <Combobox
-      options={categories.map(({ id, label, icon }) => ({
-        value: id,
-        label,
-        icon,
-      }))}
+    <Combobox
+      options={options}
@@
-      onSelect={(option) => onChange(option?.value as Category | null)}
+      onSelect={(option) => onChange(option ? option.value : null)}

Also applies to: 12-13, 16-21, 31-35

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/page-client.tsx (2)

87-91: Error state exists; consider making it screen‑reader friendly

Add role="status" and aria-live="polite" so the error is announced.

-        <div className="text-content-subtle py-12 text-sm">
+        <div className="text-content-subtle py-12 text-sm" role="status" aria-live="polite">
           Failed to load programs
         </div>

100-105: Match skeleton count to page size

Use PROGRAM_NETWORK_MAX_PAGE_SIZE for placeholder count to reduce layout shift when page size changes.

-                : [...Array(5)].map((_, idx) => <ProgramCard key={idx} />)}
+                : [...Array(PROGRAM_NETWORK_MAX_PAGE_SIZE)].map((_, idx) => (
+                    <ProgramCard key={idx} />
+                  ))}
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/program-card.tsx (2)

54-58: Lazy‑load images to improve LCP and bandwidth

Add loading="lazy" and decoding="async" to non-critical images.

-          <img
+          <img
             src={program.logo || `${OG_AVATAR_URL}${program.name}`}
             alt={program.name}
-            className="size-12 rounded-full"
+            className="size-12 rounded-full"
+            loading="lazy"
+            decoding="async"
           />

Apply similarly to the featured card avatar. The header image is background-like; leaving it eager is fine due to above-the-fold placement.

Also applies to: 238-242, 226-233


97-139: Deduplicate Rewards/Industry blocks across both cards

The two sections are nearly identical. Extract a small <ProgramMeta> subcomponent to reduce duplication and keep interactions consistent.

I can draft a ProgramMeta component that takes { program, inverted?: boolean } and consolidates the reward/category UI.

Also applies to: 140-189, 283-327, 328-377

apps/web/app/(ee)/api/network/programs/route.ts (2)

44-49: Make search case-insensitive.

Current contains is case-sensitive in Prisma. Add mode: "insensitive" to improve UX.

-      ...(search && {
-        OR: [
-          { name: { contains: search } },
-          { slug: { contains: search } },
-          { domain: { contains: search } },
-        ],
-      }),
+      ...(search && {
+        OR: [
+          { name: { contains: search, mode: "insensitive" } },
+          { slug: { contains: search, mode: "insensitive" } },
+          { domain: { contains: search, mode: "insensitive" } },
+        ],
+      }),

107-116: sortOrder is ignored for popularity.

If clients pass sortOrder=asc with sortBy=popularity, it’s dropped. Either document “popularity is always desc” or honor sortOrder.

-    orderBy:
-      sortBy === "popularity"
-        ? {
-            partners: {
-              _count: "desc",
-            },
-          }
-        : { [sortBy]: sortOrder },
+    orderBy:
+      sortBy === "popularity"
+        ? { partners: { _count: sortOrder } }
+        : { [sortBy]: sortOrder },
apps/web/lib/actions/partners/update-auto-approve-partners.ts (1)

50-67: Consider logging marketplace/category changes too.

Add audit logs when marketplaceEnabled toggles and when category changes for traceability.

   waitUntil(
     Promise.allSettled([
       ...(autoApprovePartners !== undefined
         ? [
             recordAuditLog({ /* as above */ }),
           ]
         : []),
+      ...(marketplaceEnabled !== undefined
+        ? [
+            recordAuditLog({
+              workspaceId: workspace.id,
+              programId,
+              action: marketplaceEnabled
+                ? "marketplace.enabled"
+                : "marketplace.disabled",
+              description: marketplaceEnabled
+                ? "Marketplace enabled"
+                : "Marketplace disabled",
+              actor: user,
+            }),
+          ]
+        : []),
+      ...(category !== undefined
+        ? [
+            recordAuditLog({
+              workspaceId: workspace.id,
+              programId,
+              action: "program.category.set",
+              description: `Program category ${category ?? "cleared"}`,
+              actor: user,
+            }),
+          ]
+        : []),
     ]),
   );
apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx (1)

112-123: Avoid truthy gating for counts; use explicit numeric checks.

Truthiness hides Marketplace when thresholds are 0 or counts are 0 but valid. Compare as numbers.

-          ...(enrolledProgramsCount &&
-          enrolledProgramsCount >= PROGRAM_NETWORK_PARTNER_MIN_PROGRAMS &&
-          payoutsCount &&
-          payoutsCount >= PROGRAM_NETWORK_PARTNER_MIN_PAYOUTS
+          ...(typeof enrolledProgramsCount === "number" &&
+          enrolledProgramsCount >= PROGRAM_NETWORK_PARTNER_MIN_PROGRAMS &&
+          typeof payoutsCount === "number" &&
+          payoutsCount >= PROGRAM_NETWORK_PARTNER_MIN_PAYOUTS
             ? [
               {
                 name: "Marketplace",
                 icon: Shop,
                 href: "/programs/marketplace" as `/${string}`,
               },
             ]
             : []),
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/[programSlug]/page-client.tsx (2)

276-281: Also mutate network programs cache after accepting an invite.

Keeps marketplace views in sync.

-    onSuccess: async () => {
-      await mutatePrefix("/api/partner-profile/programs");
+    onSuccess: async () => {
+      await Promise.all([
+        mutatePrefix("/api/partner-profile/programs"),
+        mutatePrefix("/api/network/programs"),
+      ]);
       toast.success("Program invite accepted!");
       router.push(`/programs/${program.slug}`);
     },

39-45: Handle load errors to avoid a blank state.

Render an error UI when error is set and !program.

-  const { data: program, error } = useSWR<NetworkProgramExtendedProps>(
+  const { data: program, error } = useSWR<NetworkProgramExtendedProps>(
     programSlug ? `/api/network/programs/${programSlug}` : null,
     fetcher,
     { keepPreviousData: true },
   );
+  if (error && !program) {
+    return (
+      <PageContent title="Program details">
+        <PageWidthWrapper>
+          <div className="mx-auto mt-10 max-w-screen-sm text-sm text-red-600">
+            Failed to load program. Please retry.
+          </div>
+        </PageWidthWrapper>
+      </PageContent>
+    );
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0415ea9 and 4cee583.

📒 Files selected for processing (47)
  • apps/web/app/(ee)/api/network/programs/[programSlug]/route.ts (1 hunks)
  • apps/web/app/(ee)/api/network/programs/check-program-network-requirements.ts (1 hunks)
  • apps/web/app/(ee)/api/network/programs/count/route.ts (1 hunks)
  • apps/web/app/(ee)/api/network/programs/route.ts (1 hunks)
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/groups/[groupIdOrSlug]/route.ts (1 hunks)
  • apps/web/app/(ee)/api/programs/[programId]/route.ts (1 hunks)
  • apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/form.tsx (4 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/[programSlug]/page-client.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/[programSlug]/page.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/featured-programs.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/layout.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/marketplace-empty-state.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/page-client.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/page.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/program-card.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/program-sort.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/use-program-network-filters.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/applications-menu.tsx (2 hunks)
  • apps/web/lib/actions/partners/create-program.ts (2 hunks)
  • apps/web/lib/actions/partners/update-auto-approve-partners.ts (2 hunks)
  • apps/web/lib/api/programs/get-program-or-throw.ts (2 hunks)
  • apps/web/lib/partners/categories.ts (1 hunks)
  • apps/web/lib/swr/use-network-programs-count.ts (1 hunks)
  • apps/web/lib/swr/use-partner-payouts-count.ts (2 hunks)
  • apps/web/lib/swr/use-program-enrollment.ts (2 hunks)
  • apps/web/lib/types.ts (2 hunks)
  • apps/web/lib/zod/schemas/groups.ts (1 hunks)
  • apps/web/lib/zod/schemas/program-network.ts (1 hunks)
  • apps/web/lib/zod/schemas/program-onboarding.ts (2 hunks)
  • apps/web/lib/zod/schemas/programs.ts (2 hunks)
  • apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx (7 hunks)
  • apps/web/ui/modals/application-settings-modal.tsx (1 hunks)
  • apps/web/ui/partners/lander/lander-hero.tsx (1 hunks)
  • apps/web/ui/partners/partner-status-badges.ts (1 hunks)
  • apps/web/ui/partners/program-application-sheet.tsx (5 hunks)
  • apps/web/ui/partners/program-card.tsx (0 hunks)
  • apps/web/ui/partners/program-category-select.tsx (1 hunks)
  • apps/web/ui/partners/program-network/program-category.tsx (1 hunks)
  • apps/web/ui/partners/program-network/program-reward-icon.tsx (1 hunks)
  • packages/prisma/schema/program.prisma (1 hunks)
  • packages/ui/src/combobox/index.tsx (2 hunks)
  • packages/ui/src/icons/nucleo/index.ts (2 hunks)
  • packages/ui/src/icons/nucleo/shop.tsx (1 hunks)
  • packages/ui/src/icons/nucleo/sort-alpha-ascending.tsx (1 hunks)
  • packages/ui/src/icons/nucleo/sort-alpha-descending.tsx (1 hunks)
  • packages/ui/src/icons/nucleo/suitcase.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/ui/partners/program-card.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
PR: dubinc/dub#2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/applications-menu.tsx
🔇 Additional comments (30)
packages/ui/src/combobox/index.tsx (2)

143-144: LGTM! Proper null safety for optional setter.

The early return correctly guards against calling setSelected when it's undefined, which is valid given the optional type definition. This prevents potential runtime crashes in multi-select mode.


154-157: LGTM! Appropriate use of optional chaining.

The optional chaining correctly handles the case where setSelected is undefined while ensuring setIsOpen(false) on line 157 still executes. This approach is more suitable than an early return here since the popover should close regardless of whether a setter is provided.

packages/ui/src/icons/nucleo/shop.tsx (1)

1-65: LGTM! Clean icon implementation.

The Shop icon component follows the established Nucleo icon pattern correctly. It's a pure presentational component with proper TypeScript types, spread props for customization, and consistent use of currentColor for theming.

packages/ui/src/icons/nucleo/suitcase.tsx (1)

1-37: LGTM! Consistent implementation.

The Suitcase icon component is correctly implemented following the same pattern as other Nucleo icons. Clean, simple, and ready to use.

packages/ui/src/icons/nucleo/sort-alpha-descending.tsx (1)

1-62: LGTM! Well-structured sorting icon.

The SortAlphaDescending icon component is correctly implemented with consistent structure and styling. Pairs appropriately with the ascending variant.

packages/ui/src/icons/nucleo/sort-alpha-ascending.tsx (1)

1-62: LGTM! Matching ascending variant.

The SortAlphaAscending icon component is correctly implemented and appropriately mirrors the descending variant. Clean and consistent.

packages/ui/src/icons/nucleo/index.ts (1)

206-206: LGTM! Exports properly organized.

The new icon exports are correctly added in alphabetical order and follow the established barrel export pattern. All referenced icon modules are present and properly implemented.

Also applies to: 210-211, 221-221

apps/web/ui/partners/program-application-sheet.tsx (1)

35-41: Nice: narrowed program prop and flexible back destination

The Pick<> typing reduces prop surface, and the backDestination-driven href/text simplifies re-use between programs and marketplace. LGTM.

Also applies to: 269-280

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/program-sort.tsx (1)

15-22: Backend already supports sortBy=popularity—no action needed

The API schema (apps/web/lib/zod/schemas/program-network.ts:58) validates "popularity" as a valid sortBy value, and the route handler (apps/web/app/(ee)/api/network/programs/route.ts:108) explicitly implements the corresponding orderBy logic. The UI change aligns correctly with existing backend support.

apps/web/app/(ee)/api/programs/[programId]/route.ts (1)

7-13: LGTM; no breaking changes confirmed

The categories field is already optional in ProgramSchema (defined as .nullish() at apps/web/lib/zod/schemas/programs.ts:42), so enriching this endpoint's payload is non-breaking. All other callers of getProgramOrThrow omit the includeCategories parameter, preserving their current behavior. Downstream consumers will safely ignore the additional field or utilize it as needed.

apps/web/lib/api/programs/get-program-or-throw.ts (1)

33-41: Refactor suggestion is correct—remove ts-ignore with explicit type branching

The verification confirms the refactor is sound:

  • ProgramCategory model contains { programId: String, category: Category } fields
  • When includeCategories=true, Prisma returns ProgramCategory[] objects
  • The mapping { category } correctly extracts Category[] values matching ProgramSchema
  • The suggested refactor properly types this conditional transformation and eliminates the @ts-ignore

The branching approach with type assertion safely handles both code paths and maintains correctness with ProgramSchema validation.

apps/web/lib/types.ts (1)

100-104: LGTM: network program types wired to zod schemas

Type exports look correct; no issues.

Also applies to: 464-468

apps/web/lib/swr/use-partner-payouts-count.ts (1)

7-14: All call sites verified as compatible—no issues found.

All 7 usages across the codebase (partners-sidebar-nav.tsx, payout-stats.tsx, marketplace/layout.tsx, payouts-card.tsx, payouts/payout-stats.tsx, use-payout-filters.tsx, and payout-table.tsx) are already correctly aligned with the current signature (query?: Record<string, string>, { includeParams? }? = {}). Whether passing two explicit parameters, a single query object, or no arguments, all call sites work correctly with the existing signature.

apps/web/lib/zod/schemas/programs.ts (1)

7-7: LGTM! Clean schema extension.

The addition of marketplaceEnabledAt and categories fields to ProgramSchema follows the established pattern of using nullish() for optional fields, consistent with other similar fields like landerPublishedAt and autoApprovePartnersEnabledAt.

Also applies to: 37-37, 42-42

apps/web/lib/zod/schemas/groups.ts (1)

84-88: LGTM! Appropriate schema subset.

The PartnerProgramGroupSchema correctly exposes a minimal public subset of group data (id, slug, applicationFormData) for partner-facing endpoints, which follows the principle of least privilege for data exposure.

apps/web/lib/actions/partners/create-program.ts (2)

42-42: LGTM! Category field properly destructured.

The category field is correctly extracted from the onboarding data and used in the conditional upsert logic below.


188-202: Consider future category update logic.

The conditional upsert creates a category association when present, but there's no corresponding deletion logic if a category is later removed or changed. While this is acceptable for program creation, ensure that any future program update flows handle category modifications appropriately to avoid orphaned associations.

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/[programSlug]/page.tsx (1)

1-5: LGTM! Standard Next.js pattern.

This server component wrapper follows the conventional Next.js pattern for delegating rendering to a client component.

apps/web/app/(ee)/api/partner-profile/programs/[programId]/groups/[groupIdOrSlug]/route.ts (1)

1-16: LGTM! Clean API endpoint implementation.

The route handler follows best practices: uses authentication wrapper (withPartnerProfile), delegates validation to utility function (getGroupOrThrow), and ensures type safety with schema validation (PartnerProgramGroupSchema).

apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/form.tsx (2)

33-39: LGTM! Category field properly tracked.

The category field is correctly added to the watch array for reactive form state management.


157-174: LGTM! Category field properly implemented.

The Product industry field is correctly implemented using Controller with required validation, ensuring the category is captured during program creation.

apps/web/lib/partners/categories.ts (1)

17-81: All Category enum values are properly mapped—verification complete.

Confirmed that all 11 values from the Category enum in packages/prisma/schema/program.prisma (Artificial_Intelligence, Development, Design, Productivity, Finance, Marketing, Ecommerce, Security, Education, Health, Consumer) have corresponding entries in the categories array. The implementation is complete and type-safe.

apps/web/ui/partners/lander/lander-hero.tsx (1)

21-25: LGTM — flexible heading + label gating look solid

Nice polymorphic Heading selection and optional label block; className merge is clean.

Also applies to: 25-34

apps/web/ui/partners/program-network/program-reward-icon.tsx (1)

1-1: Fix HoverCard trigger semantics and a11y; avoid Icon shadowing

  • Use asChild on Trigger to prevent nested interactive elements.
  • Rename icon var to avoid shadowing the imported type.
  • Add aria-label (and tabIndex when not clickable) for keyboard users.
-import { Icon } from "@dub/ui";
+import type { Icon as IconType } from "@dub/ui";
@@
-export const ProgramRewardIcon = ({
-  icon: Icon,
+export const ProgramRewardIcon = ({
+  icon: IconComponent,
   description,
   onClick,
   className,
 }: {
-  icon: Icon;
+  icon: IconType;
   description: string;
   onClick?: () => void;
   className?: string;
 }) => {
   const As = onClick ? "button" : "div";
@@
-        >
-          <Icon className="text-content-default size-4" />
+        >
+          <IconComponent className="text-content-default size-4" />
           <span>{description}</span>
         </HoverCard.Content>
       </HoverCard.Portal>
-      <HoverCard.Trigger>
+      <HoverCard.Trigger asChild>
         <As
-          {...(onClick && { type: "button", onClick })}
+          {...(onClick && { type: "button", onClick })}
+          {...(!onClick && { tabIndex: 0 })}
+          aria-label={description}
           className={cn(
             "text-content-default flex size-6 items-center justify-center rounded-md",
             onClick && "hover:bg-bg-subtle active:bg-bg-emphasis",
             className,
           )}
         >
-          <Icon className="size-4" />
+          <IconComponent className="size-4" />
         </As>
       </HoverCard.Trigger>

Also applies to: 5-15, 26-26, 30-41, 39-39

⛔ Skipped due to learnings
Learnt from: TWilson023
PR: dubinc/dub#2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/featured-programs.tsx (1)

55-61: Typing of CarouselCard is fine; keep it local

The wrapper using ComponentProps<typeof FeaturedProgramCard> is correct and keeps props in sync. No changes needed.

apps/web/ui/partners/program-category-select.tsx (1)

25-28: Graceful label fallback is good

selected.replaceAll("_", " ") is a sensible fallback when no mapping exists.

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/page-client.tsx (1)

33-37: SWR usage looks good

Stable key, keepPreviousData, and revalidateOnFocus: false are appropriate here.

apps/web/lib/swr/use-network-programs-count.ts (1)

21-23: Workspace dependency is fine

Assuming useWorkspace() always provides id in Partner Dashboard, this is OK. With the above filter, transient undefined won’t leak.

Do you ever call this hook outside workspace context? If so, we should guard for missing workspaceId.

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/program-card.tsx (1)

17-33: Aux/middle‑click handler is fine

Custom onAuxClick and interactive-child guard are good for preserving expected behaviors.

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

115-116: Pagination constraints are properly enforced by the schema.

The schema uses getPaginationQuerySchema with validators that ensure page is positive (defaults to 1 if omitted) and pageSize does not exceed 100. These constraints prevent negative skip values and invalid take inputs at the point of parsing, before the code at lines 115–116 executes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants