-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add shareable earnings chart feature #3246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughIntroduces 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
Sequence DiagramsequenceDiagram
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
Estimated code review effortπ― 3 (Moderate) | β±οΈ ~25 minutes
Suggested reviewers
Poem
Pre-merge checks and finishing touchesβ Failed checks (1 warning)
β Passed checks (2 passed)
β¨ Finishing touches
π§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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
ClipboardItemAPI 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 addingaria-labelfor accessibility.The close button uses only an icon without visible text. Adding an
aria-labelwould 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
π 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.tsxapps/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.tsxapps/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.tsxapps/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-200maintains 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
currencyFormatterusage correctly expects values in cents, which aligns with howtotalandepcare 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), andmaxEarningshas a minimum of 1 to prevent division by zero inscaleY.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
programIdis 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-200are 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.
| 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); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.32.18.mp4
Summary by CodeRabbit
New Features
Style
βοΈ Tip: You can customize this high-level summary in your review settings.