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

    • Added earnings sharing functionality allowing partners to share earnings charts with dynamic image generation.
    • New share modal with options to toggle background theme (light/dark), copy chart to clipboard, and download as PNG.
  • Style

    • Updated card styling with refined border radius and color adjustments for visual consistency.

✏️ 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 Error Error Dec 18, 2025 9:45pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

Walkthrough

Introduces earnings sharing functionality with a dynamic Open Graph image generation endpoint, a new ShareEarningsModal component for previewing and downloading earnings charts, and UI enhancements to the program enrollment dashboard. Updates styling to use rounded-xl borders and neutral-200 colors.

Changes

Cohort / File(s) Change Summary
Earnings Share OG Image Generation
apps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/share/route.tsx
New edge-route handler generating dynamic Open Graph images for earnings sharing. Loads fonts, composes an image with header, total earnings, EPC value, and time-series chart visualization. Parses query parameters (background, total, epc, timeseries JSON) with robust handling for invalid data. Includes Chart subcomponent rendering SVG line/area charts with gradient fills and data point markers.
Share Earnings Modal
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/share-earnings-modal.tsx
New ShareEarningsModal component for previewing and sharing earnings charts. Fetches image blob from OG endpoint, manages loading state with AbortController cleanup, and provides UI to toggle light/dark backgrounds, copy to clipboard, and download as PNG with error handling via toast notifications.
Program Enrollment Page Enhancements
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/page-client.tsx
Integrates share earnings modal workflow with new state (showShareModal) and ShareEarningsModal component usage. Adds Tooltip-wrapped share button next to Earnings label. Wires modal props with programEnrollment data (programId, total, epc, start, end, interval, timeseries). Adjusts layout gaps and container styling.
Styling Updates
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx
Updates container styling from rounded-lg border-neutral-300 to rounded-xl border-neutral-200. Removes inline comments without altering rendering logic or data flow.

Sequence Diagram

sequenceDiagram
    actor User
    participant PageClient as Page Client
    participant Modal as ShareEarningsModal
    participant API as OG Image Endpoint
    participant Clipboard as Clipboard API
    
    User->>PageClient: Clicks Share Button
    PageClient->>Modal: Opens with earnings data
    Modal->>Modal: User selects background (light/dark)
    Modal->>API: Fetch OG image with params<br/>(programId, total, epc, timeseries)
    API->>API: Render React chart image<br/>(loads fonts, composes SVG)
    API-->>Modal: ImageResponse (blob)
    Modal->>Modal: Display image preview
    alt User Copies to Clipboard
        User->>Modal: Click Copy
        Modal->>Clipboard: Copy image blob
        Clipboard-->>Modal: Success/Error
        Modal->>Modal: Show toast notification
    else User Downloads
        User->>Modal: Click Download
        Modal->>Modal: Trigger PNG download
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Edge route implementation: Requires understanding of edge runtime, image generation with Vercel's @vercel/og, font loading, and SVG chart rendering logic. Pay special attention to timeseries data parsing and chart boundary handling for single-point vs. multi-point cases.
  • Modal component integration: Verify fetch logic with AbortController cleanup, clipboard API error handling, and image blob lifecycle management.
  • State wiring: Ensure programEnrollment data flows correctly through page-client to modal props and that modal state (showShareModal) is properly managed.

Suggested reviewers

  • steven-tey
  • TWilson023
  • devkiran

Poem

🐰 A chart to share, with colors bright,
Light and dark, a sharing delight,
Copy, download, the earnings shine,
Through modals modal, earnings align! ✨

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' clearly and concisely summarizes the main objective of the pull request, which is to introduce a shareable earnings chart feature. It accurately reflects the primary changes across all modified files.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch shareable-partner-stats

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants