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

Skip to content

Conversation

@marcusljf
Copy link
Collaborator

@marcusljf marcusljf commented Dec 18, 2025

Introduces a shareable earnings chart modal and API route, allowing users to copy or download a chart image of their earnings. Updates UI components for improved styling consistency and integrates the share modal into the program dashboard.

CleanShot 2025-12-18 at 13 43 13@2x CleanShot 2025-12-18 at 13 43 22@2x
CleanShot.2025-12-18.at.13.32.18.mp4

Summary by CodeRabbit

  • New Features

    • Share Earnings modal: preview, copy, download, and switch light/dark backgrounds; chart share button added to earnings view.
    • Server-generated Open Graph images for partner earnings and rewind share previews with dynamic font loading.
  • Style

    • Refined card corners and border colors for a cleaner, more consistent dashboard look.
  • Chores

    • Rewind page now redirects to the 2025 rewind route and updated auth handling for rewind pages.

✏️ Tip: You can customize this high-level summary in your review settings.

Introduces a shareable earnings chart modal and API route, allowing users to copy or download a chart image of their earnings. Updates UI components for improved styling consistency and integrates the share modal into the program dashboard.
@vercel
Copy link
Contributor

vercel bot commented Dec 18, 2025

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

Project Deployment Review Updated (UTC)
dub Ready Ready Preview Dec 30, 2025 6:10am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

📝 Walkthrough

Walkthrough

Adds a share-earnings UI and modal, new OG image API for partner earnings with font loader, a centralized timeseries service, and small routing/middleware updates for rewind and partners paths. No public API breaking changes.

Changes

Cohort / File(s) Summary of edits / attention areas
Frontend — enrolled dashboard & UI
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx, apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx
Added ShareEarnings modal wiring and Tooltip/share button; layout, padding and card border-radius/color tweaks.
New UI component — share modal
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx
New exported React component that requests /api/og/partner-earnings, manages background presets, image blob loading, clipboard download/copy, AbortController cleanup and toast error handling.
API — OG images & font loader
apps/web/app/api/og/partner-earnings/route.tsx, apps/web/app/api/og/partner-rewind/route.tsx, apps/web/app/api/og/load-google-font.ts
New partner-earnings ImageResponse route (SVG chart), partner-rewind refactored to withPartnerProfile and dynamic Google font loading, and added loadGoogleFont helper. Check query validation, font fetching and ImageResponse fonts array.
Backend service — timeseries
apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts, apps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/timeseries/route.ts
Introduced getPartnerEarningsTimeseries with SQL aggregation, timezone-aware period bins, optional groupBy handling and zero-filled periods; API route delegates to it. Review SQL, grouping and edge-case filling.
Rewind share adjustments
apps/web/app/(ee)/partners.dub.co/(dashboard)/rewind/2025/rewind.tsx, apps/web/app/(ee)/partners.dub.co/(dashboard)/rewind/2025/share-rewind-modal.tsx
Removed partnerId propagation from rewind share modal and simplified inner prop signature; image URL/query no longer includes partnerId.
Routing & middleware
apps/web/app/(ee)/api/partners/[partnerId]/route.ts, apps/web/lib/middleware/partners.ts, apps/web/lib/middleware/utils/partners-redirect.ts
Adjusted import path for get-partner-for-program; added "/rewind" to authenticated paths and redirect mapping "/rewind" → "/rewind/2025"; minor redirect function simplification.

Sequence Diagram(s)

sequenceDiagram
  participant User as Browser (User)
  participant UI as Enrolled Page (React)
  participant API as /api/og/partner-earnings (Server)
  participant TS as getPartnerEarningsTimeseries (Service/DB)
  participant Font as loadGoogleFont (External)
  Note over UI,API: Share earnings -> generate OG image
  User->>UI: Click "Share" button
  UI->>API: GET /api/og/partner-earnings?programId&start&end&interval&background
  API->>TS: fetch timeseries(partnerId, programId, filters)
  TS-->>API: return timeseries data
  API->>Font: loadGoogleFont("Inter:700")
  Font-->>API: font ArrayBuffer (optional)
  API-->>UI: ImageResponse (PNG/SVG)
  UI->>UI: fetch blob, show preview
  UI->>User: actions: Download / Copy (ClipboardItem)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

"I hopped through code with eager paws,
I traced each pixel, fixed no flaws.
A chart, a blob, a font so neat—
I shared my carrots for you to eat.
🐇📈✨"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add shareable earnings chart feature' accurately reflects the main change in the PR: introducing a new shareable earnings chart modal and API route that enable users to share their earnings data.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca72772 and 506edf3.

📒 Files selected for processing (1)
  • apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts
⏰ 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

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

🧹 Nitpick comments (2)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (2)

115-130: Consider checking ClipboardItem support for better UX.

The ClipboardItem API for images has limited browser support (Safari added it in 13.1, Firefox has partial support). While the catch block handles failures gracefully, you could optionally check support upfront to provide a more informative message or hide the copy button.

🔎 Optional: Check for ClipboardItem support
const supportsImageClipboard = typeof ClipboardItem !== 'undefined' && 
  navigator.clipboard?.write !== undefined;

// Then in the UI, either disable/hide the Copy button or show a tooltip
// when supportsImageClipboard is false

152-158: Consider adding aria-label for accessibility.

The close button uses only an icon without visible text. Adding an aria-label would improve screen reader support.

🔎 Optional: Add aria-label
         <button
           type="button"
           onClick={() => setShowModal(false)}
+          aria-label="Close modal"
           className="group rounded-lg p-2 text-neutral-500 transition-all duration-75 hover:bg-neutral-100 focus:outline-none active:bg-neutral-200"
         >
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3669b6 and 83436ff.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/share/route.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx (9 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (11)
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 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/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-10-08T21:33:23.553Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2936
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/add-hostname-modal.tsx:28-34
Timestamp: 2025-10-08T21:33:23.553Z
Learning: In the dub/ui Button component, when the `disabledTooltip` prop is set to a non-undefined value (e.g., a string), the button is automatically disabled. Therefore, it's not necessary to also add the same condition to the `disabled` prop—setting `disabledTooltip={permissionsError || undefined}` is sufficient to disable the button when there's a permissions error.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically sets type="button" when an onClick prop is passed and defaults to type="submit" otherwise, using the logic: type={props.onClick ? "button" : "submit"}. This prevents accidental form submissions when buttons are used for modal triggers or other non-form actions.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 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.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-17T17:40:35.470Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/resources/program-help-and-support.tsx:95-121
Timestamp: 2025-09-17T17:40:35.470Z
Learning: In the Dub UI Switch component, providing a truthy `disabledTooltip` prop automatically disables the switch and prevents user interaction, so an explicit `disabled` prop is not needed when using `disabledTooltip`.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-17T17:40:35.470Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/resources/program-help-and-support.tsx:95-121
Timestamp: 2025-09-17T17:40:35.470Z
Learning: In the Dub UI Switch component, providing a truthy `disabledTooltip` prop automatically disables the switch and prevents user interaction, so an explicit `disabled` prop is not needed when using `disabledTooltip`. The component computes `switchDisabled = disabledTooltip ? true : disabled || loading` and passes this to the underlying Radix Switch primitive.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
🧬 Code graph analysis (3)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (2)
apps/web/lib/analytics/types.ts (1)
  • IntervalOptions (16-16)
packages/ui/src/button.tsx (1)
  • Button (158-158)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx (3)
apps/web/lib/swr/use-program-enrollment.ts (1)
  • useProgramEnrollment (11-49)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (1)
  • ShareEarningsModal (35-64)
packages/ui/src/tooltip.tsx (1)
  • Tooltip (68-119)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/share/route.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (7-22)
⏰ 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 (9)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx (1)

39-39: LGTM!

The styling update to rounded-xl border-neutral-200 maintains consistency with the other dashboard cards updated in this PR.

apps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/share/route.tsx (3)

15-36: LGTM!

The GET handler setup is well-structured: parallel font loading with Promise.all, safe JSON parsing with fallback, and proper type handling for query parameters.


55-141: LGTM!

The ImageResponse structure is clean with proper font registration. The currencyFormatter usage correctly expects values in cents, which aligns with how total and epc are calculated in the page client.


143-184: LGTM!

The Chart component handles edge cases well: empty data returns null, single data points render a centered circle (avoiding division by zero in scaleX), and maxEarnings has a minimum of 1 to prevent division by zero in scaleY.

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx (3)

332-342: LGTM!

The ShareEarningsModal integration correctly passes all required props. The fallback to empty string for programId is safe since the modal's effect guards against empty values before fetching.


346-359: LGTM!

The share button is well-implemented with a Tooltip for context and appropriate hover states. The type="button" prevents accidental form submissions.


238-240: LGTM!

Styling updates to rounded-xl border-neutral-200 are consistent across all card components in the dashboard.

Also applies to: 455-455, 522-522

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (2)

132-146: LGTM!

The download handler correctly manages memory by revoking the blob URL after use, following the standard pattern for programmatic downloads.


35-64: LGTM!

The component structure is clean with proper separation between the wrapper (handling modal visibility) and inner component (handling state and rendering). The props interface is well-typed.

Comment on lines +99 to +110
fetch(imageUrl, { signal: abortController.signal })
.then((res) =>
res.blob().then((blob) => {
setBlob(blob);
setIsLoading(false);
}),
)
.catch((err) => {
if (err.name === "AbortError") return;
toast.error("Failed to prepare chart image for sharing");
setIsLoading(false);
});
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

Check response status before processing the blob.

The fetch doesn't verify res.ok before calling res.blob(). If the server returns a 4xx/5xx error, this could set an invalid blob or fail silently.

🔎 Proposed fix
     fetch(imageUrl, { signal: abortController.signal })
-      .then((res) =>
-        res.blob().then((blob) => {
+      .then((res) => {
+        if (!res.ok) {
+          throw new Error(`Failed to fetch chart image: ${res.status}`);
+        }
+        return res.blob().then((blob) => {
           setBlob(blob);
           setIsLoading(false);
-        }),
-      )
+        });
+      })
       .catch((err) => {
🤖 Prompt for AI Agents
In
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx
around lines 99 to 110, the fetch response is passed straight to res.blob()
without checking res.ok; update the promise chain to first check if res.ok and,
if not, throw an error (include status/text) so the catch block handles HTTP
errors, then call res.blob() only on a successful response and
setBlob/setIsLoading accordingly; keep the existing AbortError early return and
ensure toast.error and setIsLoading(false) run for non-abort failures.

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

Caution

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

⚠️ Outside diff range comments (1)
apps/web/app/(ee)/partners.dub.co/(dashboard)/rewind/2025/share-rewind-modal.tsx (1)

30-42: Loading state not reset on fetch failure.

If the fetch fails, isLoading remains true indefinitely, keeping the Copy and Download buttons permanently disabled.

🔎 Proposed fix
   useEffect(() => {
     setIsLoading(true);
     fetch(imageUrl)
       .then((res) =>
         res.blob().then((blob) => {
           setBlob(blob);
           setIsLoading(false);
         }),
       )
       .catch(() => {
         toast.error("Failed to prepare rewind image for sharing");
+        setIsLoading(false);
       });
   }, [imageUrl]);
🧹 Nitpick comments (3)
apps/web/app/(ee)/partners.dub.co/(dashboard)/rewind/2025/share-rewind-modal.tsx (1)

25-25: Consider extracting the duplicated URL construction.

The image URL is constructed identically on lines 25 and 54. Extract it to a constant or use the existing imageUrl variable for the img src.

🔎 Proposed fix
         <img
-          src={`/api/og/partner-rewind?${new URLSearchParams({ step }).toString()}`}
+          src={imageUrl}
           alt="share rewind image"
           className="relative size-full"
         />

Also applies to: 54-54

apps/web/app/api/og/partner-rewind/route.tsx (1)

46-47: Font loading failure results in silent degradation.

If loadGoogleFont returns undefined, the image renders with an empty fonts array. This is acceptable fallback behavior, but consider logging when font loading fails for observability.

Also applies to: 137-146

apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts (1)

86-93: Consider adding explicit typing to the accumulator.

The reduce accumulator lacks type annotation, which can mask type errors.

🔎 Proposed fix
-  const commissionLookup = earnings.reduce((acc, item) => {
+  const commissionLookup = earnings.reduce<Record<string, { earnings: number; [key: string]: number }>>((acc, item) => {
     if (!(item.start in acc)) acc[item.start] = { earnings: 0 };
     acc[item.start].earnings += Number(item.earnings);
     if (groupBy) {
-      acc[item.start][item[groupBy]] = Number(item.earnings);
+      acc[item.start][item[groupBy] as string] = Number(item.earnings);
     }
     return acc;
   }, {});
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83436ff and df7f8b7.

📒 Files selected for processing (12)
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/timeseries/route.ts
  • apps/web/app/(ee)/api/partners/[partnerId]/route.ts
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/rewind/2025/rewind.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/rewind/2025/share-rewind-modal.tsx
  • apps/web/app/api/og/load-google-font.ts
  • apps/web/app/api/og/partner-earnings/route.tsx
  • apps/web/app/api/og/partner-rewind/route.tsx
  • apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts
  • apps/web/lib/api/partner-profile/get-partner-for-program.ts
  • apps/web/lib/middleware/partners.ts
  • apps/web/lib/middleware/utils/partners-redirect.ts
💤 Files with no reviewable changes (1)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/rewind/2025/rewind.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/timeseries/route.ts
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 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/(ee)/partners.dub.co/(dashboard)/rewind/2025/share-rewind-modal.tsx
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/rewind/2025/share-rewind-modal.tsx
🧬 Code graph analysis (4)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/timeseries/route.ts (2)
apps/web/lib/zod/schemas/partner-profile.ts (1)
  • getPartnerEarningsTimeseriesSchema (78-81)
apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts (1)
  • getPartnerEarningsTimeseries (10-127)
apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts (7)
apps/web/lib/zod/schemas/partner-profile.ts (1)
  • getPartnerEarningsTimeseriesSchema (78-81)
apps/web/lib/api/programs/get-program-enrollment-or-throw.ts (1)
  • getProgramEnrollmentOrThrow (6-67)
apps/web/lib/analytics/utils/get-start-end-dates.ts (1)
  • getStartEndDates (5-51)
apps/web/lib/planetscale/granularity.ts (1)
  • sqlGranularityMap (12-45)
packages/prisma/index.ts (1)
  • prisma (3-9)
packages/utils/src/functions/group-by.ts (1)
  • groupBy (1-13)
packages/prisma/client.ts (1)
  • Prisma (30-30)
apps/web/app/api/og/partner-rewind/route.tsx (5)
apps/web/lib/auth/partner.ts (1)
  • withPartnerProfile (47-175)
apps/web/ui/partners/rewind/constants.ts (1)
  • REWIND_STEPS (6-46)
apps/web/lib/api/partners/get-partner-rewind.ts (1)
  • getPartnerRewind (6-61)
apps/web/lib/api/errors.ts (1)
  • DubApiError (58-75)
apps/web/app/api/og/load-google-font.ts (1)
  • loadGoogleFont (1-14)
apps/web/app/api/og/partner-earnings/route.tsx (5)
apps/web/lib/auth/partner.ts (1)
  • withPartnerProfile (47-175)
apps/web/lib/zod/schemas/partner-profile.ts (1)
  • getPartnerEarningsTimeseriesSchema (78-81)
apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts (1)
  • getPartnerEarningsTimeseries (10-127)
apps/web/app/api/og/load-google-font.ts (1)
  • loadGoogleFont (1-14)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (7-22)
⏰ 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 (8)
apps/web/lib/middleware/utils/partners-redirect.ts (2)

8-8: LGTM! Clean redirect mapping for the rewind feature.

The addition of the "/rewind" → "/rewind/2025" redirect is consistent with the existing redirect patterns and properly supports the new rewind feature introduced in this PR.


12-12: Nice refactor! Cleaner and more idiomatic.

The simplified return statement maintains identical behavior while improving readability. Using the logical OR operator for the null fallback is more concise than the previous conditional approach.

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

21-21: LGTM! Properly integrates with the redirect mapping.

Adding "/rewind" to the authenticated paths ensures users must log in to access the rewind feature. This works cohesively with the redirect mapping in partners-redirect.ts, where authenticated users accessing "/rewind" will be redirected to "/rewind/2025".

apps/web/app/(ee)/api/partners/[partnerId]/route.ts (1)

1-6: LGTM!

The import path update aligns with the broader refactoring toward a centralized partner-profile namespace for partner-related data fetches.

apps/web/app/api/og/partner-rewind/route.tsx (1)

17-35: LGTM!

The refactor to use withPartnerProfile wrapper improves consistency with other partner routes. The zod validation ensures the step parameter is valid before use, making the non-null assertion on line 26 safe.

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

6-19: LGTM!

Clean refactor that properly separates concerns—the route handles HTTP layer while business logic is delegated to getPartnerEarningsTimeseries. The zod schema validation ensures type safety for the filters.

apps/web/app/api/og/partner-earnings/route.tsx (1)

142-183: LGTM on Chart component.

The Chart component handles edge cases well: empty data returns null, single data point renders centered, and the scaling logic correctly avoids division by zero with Math.max(..., 1).

apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts (1)

10-126: Well-structured utility function.

The function cleanly encapsulates the timeseries data retrieval logic with proper date range handling, granularity selection, and gap-filling for missing periods. The separation from the route handler improves testability and reusability.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (1)

87-98: Check response status before processing the blob.

The fetch doesn't verify res.ok before calling res.blob(). If the API returns a 4xx/5xx error, this could set an invalid blob or display a misleading error message to the user.

🔎 Proposed fix
     fetch(imageUrl, { signal: abortController.signal })
-      .then((res) =>
-        res.blob().then((blob) => {
+      .then((res) => {
+        if (!res.ok) {
+          throw new Error(`Failed to fetch chart image: ${res.status}`);
+        }
+        return res.blob().then((blob) => {
           setBlob(blob);
           setIsLoading(false);
-        }),
-      )
+        });
+      })
       .catch((err) => {
         if (err.name === "AbortError") return;
         toast.error("Failed to prepare chart image for sharing");
         setIsLoading(false);
       });
🧹 Nitpick comments (1)
apps/web/app/api/og/partner-earnings/route.tsx (1)

157-163: Consider moving scale functions inside the multi-point rendering branch.

The scaleX function on line 160 contains chartWidth / (data.length - 1), which would divide by zero if data.length === 1. While this is prevented by the special case handling on lines 165-176, the scale functions are defined before this check, creating a subtle risk if the code is refactored later.

🔎 Proposed refactor
   const maxEarnings = Math.max(...data.map((d) => d.earnings), 1);
 
-  const scaleX = (index: number) =>
-    padding.left + (chartWidth / (data.length - 1)) * index;
-
-  const scaleY = (value: number) =>
-    padding.top + chartHeight - (value / maxEarnings) * chartHeight;
-
   if (data.length === 1) {
+    const singleX = padding.left + chartWidth / 2;
+    const singleY = padding.top + chartHeight - (data[0].earnings / maxEarnings) * chartHeight;
-    const singleX = padding.left + chartWidth / 2;
-    const singleY = scaleY(data[0].earnings);
     return (
       <svg
         viewBox={`0 0 ${width} ${height}`}
         style={{ width: "100%", height: "100%" }}
       >
         <circle cx={singleX} cy={singleY} r={circleRadius} fill="#DA2778" />
       </svg>
     );
   }
 
+  const scaleX = (index: number) =>
+    padding.left + (chartWidth / (data.length - 1)) * index;
+
+  const scaleY = (value: number) =>
+    padding.top + chartHeight - (value / maxEarnings) * chartHeight;
+
   const linePath = data
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df7f8b7 and 7297301.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx
  • apps/web/app/api/og/partner-earnings/route.tsx
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 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/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-10-08T21:33:23.553Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2936
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/add-hostname-modal.tsx:28-34
Timestamp: 2025-10-08T21:33:23.553Z
Learning: In the dub/ui Button component, when the `disabledTooltip` prop is set to a non-undefined value (e.g., a string), the button is automatically disabled. Therefore, it's not necessary to also add the same condition to the `disabled` prop—setting `disabledTooltip={permissionsError || undefined}` is sufficient to disable the button when there's a permissions error.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically sets type="button" when an onClick prop is passed and defaults to type="submit" otherwise, using the logic: type={props.onClick ? "button" : "submit"}. This prevents accidental form submissions when buttons are used for modal triggers or other non-form actions.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 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.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-17T17:40:35.470Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/resources/program-help-and-support.tsx:95-121
Timestamp: 2025-09-17T17:40:35.470Z
Learning: In the Dub UI Switch component, providing a truthy `disabledTooltip` prop automatically disables the switch and prevents user interaction, so an explicit `disabled` prop is not needed when using `disabledTooltip`.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-09-17T17:40:35.470Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/resources/program-help-and-support.tsx:95-121
Timestamp: 2025-09-17T17:40:35.470Z
Learning: In the Dub UI Switch component, providing a truthy `disabledTooltip` prop automatically disables the switch and prevents user interaction, so an explicit `disabled` prop is not needed when using `disabledTooltip`. The component computes `switchDisabled = disabledTooltip ? true : disabled || loading` and passes this to the underlying Radix Switch primitive.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
🧬 Code graph analysis (3)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (2)
apps/web/lib/analytics/types.ts (1)
  • IntervalOptions (16-16)
packages/ui/src/button.tsx (1)
  • Button (158-158)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx (3)
apps/web/lib/swr/use-program-enrollment.ts (1)
  • useProgramEnrollment (11-49)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (1)
  • ShareEarningsModal (33-58)
packages/ui/src/tooltip.tsx (1)
  • Tooltip (68-119)
apps/web/app/api/og/partner-earnings/route.tsx (6)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/timeseries/route.ts (1)
  • GET (7-19)
apps/web/lib/auth/partner.ts (1)
  • withPartnerProfile (47-175)
apps/web/lib/zod/schemas/partner-profile.ts (1)
  • getPartnerEarningsTimeseriesSchema (78-81)
apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts (1)
  • getPartnerEarningsTimeseries (10-127)
apps/web/app/api/og/load-google-font.ts (1)
  • loadGoogleFont (1-14)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (7-22)
⏰ 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/app/api/og/partner-earnings/route.tsx (1)

17-24: Past review comment has been addressed.

The previous concern about total and epc parameters not being validated has been resolved. The current implementation correctly validates all query parameters through the schema, and total is now computed from the fetched timeseries data rather than being read directly from searchParams.

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx (2)

124-124: LGTM: Consistent styling improvements.

The border radius and color updates (rounded-lg → rounded-xl, border-neutral-300 → border-neutral-200) improve visual consistency across the dashboard cards.

Also applies to: 238-238, 455-455, 522-522


346-359: LGTM: Share button implementation is clean and accessible.

The Tooltip-wrapped share button follows established patterns in the codebase and includes proper accessibility attributes.

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (3)

120-134: LGTM: Download implementation includes proper cleanup.

The download handler correctly manages the blob URL lifecycle by revoking the object URL after use.


156-165: LGTM: Modal UI with proper loading and disabled states.

The conditional rendering and button disabled states ensure users can't interact with incomplete data.

Also applies to: 193-206


103-118: No action needed. The ClipboardItem API is fully supported in Firefox 127+ (released June 2024) and widely available across modern browsers as of December 2025. The code correctly implements the API with proper error handling that gracefully catches and reports failures to the user. Feature detection is optional but not required.

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

♻️ Duplicate comments (1)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (1)

91-97: Check response status before processing the blob.

The fetch doesn't verify res.ok before calling res.blob(). If the server returns a 4xx/5xx error, this could set an invalid blob or fail silently.

🔎 Proposed fix
     fetch(imageUrl, { signal: abortController.signal })
-      .then((res) =>
-        res.blob().then((blob) => {
+      .then((res) => {
+        if (!res.ok) {
+          throw new Error(`Failed to fetch chart image: ${res.status}`);
+        }
+        return res.blob().then((blob) => {
           setBlob(blob);
           setIsLoading(false);
-        }),
-      )
+        });
+      })
       .catch((err) => {
🧹 Nitpick comments (3)
apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts (2)

31-38: Consider using object property shorthand.

The parameters partnerId and programId can use shorthand syntax for cleaner code.

🔎 Proposed refactor
   const { program, links } = await getProgramEnrollmentOrThrow({
-    partnerId: partnerId,
-    programId: programId,
+    partnerId,
+    programId,
     include: {
       program: true,
       links: true,
     },
   });

146-175: Consider extracting the data initialization logic for better readability.

The nested ternary operators and filters on lines 156-168 work correctly but are complex. Extracting this into a helper function would improve maintainability.

🔎 Example refactor
+  const initializeGroupData = (
+    groupBy: string | undefined,
+    type: string | undefined,
+    linkId: string | undefined,
+    links: { id: string }[],
+  ): Record<string, number> => {
+    if (!groupBy) return {};
+    
+    if (groupBy === "type") {
+      const types = ["sale", "lead", "click"];
+      return Object.fromEntries(
+        types
+          .filter((t) => !type || type === t)
+          .map((t) => [t, 0])
+      );
+    }
+    
+    return Object.fromEntries(
+      links
+        .filter((link) => !linkId || link.id === linkId)
+        .map((link) => [link.id, 0])
+    );
+  };

   while (currentDate < endDate) {
     const periodKey = format(currentDate, formatString);
     const { earnings, ...rest } = commissionLookup[periodKey] || {};

     timeseries.push({
       start: currentDate.toISOString(),
       earnings: earnings || 0,
       groupBy: groupBy || undefined,
-      data: groupBy
-        ? {
-            ...(groupBy === "type"
-              ? Object.fromEntries(
-                  ["sale", "lead", "click"]
-                    // only show filtered type if type filter is provided
-                    .filter((t) => (type ? type === t : true))
-                    .map((t) => [t, 0]),
-                )
-              : Object.fromEntries(
-                  links
-                    // only show filtered link if linkId filter is provided
-                    .filter((link) => (linkId ? link.id === linkId : true))
-                    .map((link) => [link.id, 0]),
-                )),
-            ...rest,
-          }
-        : undefined,
+      data: groupBy 
+        ? { ...initializeGroupData(groupBy, type, linkId, links), ...rest }
+        : undefined,
     });

     currentDate = dateIncrement(currentDate);
   }
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (1)

107-122: Consider adding ClipboardItem browser support detection.

The ClipboardItem API is not supported in all browsers (e.g., Firefox doesn't support image copying). Consider adding feature detection to provide a better user experience.

🔎 Proposed enhancement
   const handleCopy = async () => {
     if (!blob) return;

     try {
+      if (!navigator.clipboard || typeof ClipboardItem === 'undefined') {
+        toast.error("Clipboard copying is not supported in this browser");
+        return;
+      }
+
       const clipboardItem = new ClipboardItem({
         [blob.type]: blob,
       });

       await navigator.clipboard.write([clipboardItem]);

       toast.success("Copied to clipboard");
     } catch (err) {
       console.error("Failed to copy image: ", err);
       toast.error("Failed to copy image to clipboard");
     }
   };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7297301 and ca72772.

📒 Files selected for processing (2)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx
  • apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts (5)
apps/web/lib/zod/schemas/partner-profile.ts (1)
  • getPartnerEarningsTimeseriesSchema (78-81)
apps/web/lib/api/programs/get-program-enrollment-or-throw.ts (1)
  • getProgramEnrollmentOrThrow (6-67)
apps/web/lib/analytics/utils/get-start-end-dates.ts (1)
  • getStartEndDates (5-51)
apps/web/lib/planetscale/granularity.ts (1)
  • sqlGranularityMap (12-45)
packages/utils/src/functions/group-by.ts (1)
  • groupBy (1-13)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (1)
apps/web/lib/analytics/types.ts (1)
  • IntervalOptions (16-16)
⏰ 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/lib/api/partner-profile/get-partner-earnings-timeseries.ts (3)

1-17: LGTM!

The function signature is well-typed using Zod schema inference, and imports are properly organized.


63-103: LGTM!

The WHERE clause construction properly uses parameterized queries with placeholders, preventing SQL injection vulnerabilities.


119-127: Past type issue has been addressed.

The QueryResult interface now properly includes type and linkId as optional fields, resolving the previous review concern about type definition not including grouped columns.

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx (3)

1-58: LGTM!

The component structure, imports, and type definitions are well-organized.


124-138: LGTM!

The download handler properly creates, uses, and cleans up the blob URL to prevent memory leaks.


196-210: LGTM!

The action buttons properly handle disabled states based on loading and blob availability.

@steven-tey steven-tey merged commit f255512 into main Dec 30, 2025
7 of 10 checks passed
@steven-tey steven-tey deleted the shareable-partner-stats branch December 30, 2025 06:37
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.

3 participants