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

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a3d66e5
WIP customer page updates
TWilson023 Dec 16, 2025
3a81c38
Update partner-nav.tsx
TWilson023 Dec 16, 2025
01a7ba5
More WIP customer page
TWilson023 Dec 16, 2025
70c5c14
Merge branch 'main' into customers-updates
TWilson023 Dec 17, 2025
612be20
WIP
TWilson023 Dec 17, 2025
5cbd56a
Merge branch 'main' into customers-updates
TWilson023 Dec 17, 2025
a0d579d
Sales table tweaks
TWilson023 Dec 17, 2025
b182979
Update customer-details-column.tsx
TWilson023 Dec 17, 2025
4199328
Referral partner info
TWilson023 Dec 17, 2025
9ce98d0
Responsive grid rework
TWilson023 Dec 17, 2025
69b3a0e
Add stats
TWilson023 Dec 17, 2025
88ef57a
Update timestamp-tooltip.tsx
TWilson023 Dec 17, 2025
5a30c9c
Update customer-details-column.tsx
TWilson023 Dec 17, 2025
8a0896d
WIP program customers
TWilson023 Dec 17, 2025
67707c1
Merge branch 'main' into customers-updates
TWilson023 Dec 18, 2025
58a0225
Add partner column to program customers table
TWilson023 Dec 18, 2025
5f7f2e5
Update customer links
TWilson023 Dec 18, 2025
d64358c
Update more links
TWilson023 Dec 18, 2025
ce129a6
Improve layout/page code duplication
TWilson023 Dec 18, 2025
0e44eff
Update links
TWilson023 Dec 18, 2025
fe2e63b
Merge branch 'main' into customers-updates
TWilson023 Dec 18, 2025
8e96c81
Update layout.tsx
TWilson023 Dec 18, 2025
9911e1b
Update app-redirect.ts
TWilson023 Dec 18, 2025
f007c16
Update customer-table.tsx
TWilson023 Dec 18, 2025
fd058df
Update customer-table.tsx
TWilson023 Dec 18, 2025
efb8fab
Merge branch 'customers-updates' of github.com:dubinc/dub into custom…
TWilson023 Dec 18, 2025
9606a45
Quick fixes
TWilson023 Dec 18, 2025
20fe549
Misc. fixes
TWilson023 Dec 18, 2025
63a4d2a
Update partner-side customer page
TWilson023 Dec 18, 2025
72d6ee1
Merge branch 'main' into customers-updates
TWilson023 Dec 18, 2025
b2b56bc
Update app-redirect.ts
TWilson023 Dec 18, 2025
6eaeae3
Merge branch 'customers-updates' of github.com:dubinc/dub into custom…
TWilson023 Dec 18, 2025
cbf5fe2
Name tweaks
TWilson023 Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/web/app/(ee)/api/customers/count/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { NextResponse } from "next/server";

// GET /api/customers/count
export const GET = withWorkspace(async ({ workspace, searchParams }) => {
const { email, externalId, search, country, linkId, groupBy } =
const { email, externalId, search, country, linkId, programId, groupBy } =
getCustomersCountQuerySchema.parse(searchParams);

const commonWhere: Prisma.CustomerWhereInput = {
Expand All @@ -33,6 +33,11 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => {
groupBy !== "linkId" && {
linkId,
}),
...(programId && {
link: {
programId,
},
}),
};

// Get customer count by country
Expand Down
6 changes: 6 additions & 0 deletions apps/web/app/(ee)/api/customers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const GET = withWorkspace(
search,
country,
linkId,
programId,
includeExpandedFields,
page,
pageSize,
Expand Down Expand Up @@ -79,6 +80,11 @@ export const GET = withWorkspace(
...(linkId && {
linkId,
}),
...(programId && {
link: {
programId,
},
}),
},
orderBy: {
[sortBy]: sortOrder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { PartnerEarningsResponse } from "@/lib/types";
import { CustomerActivityList } from "@/ui/customers/customer-activity-list";
import { CustomerDetailsColumn } from "@/ui/customers/customer-details-column";
import { CustomerSalesTable } from "@/ui/customers/customer-sales-table";
import { PageContent } from "@/ui/layout/page-content";
import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper";
import { ProgramRewardList } from "@/ui/partners/program-reward-list";
import { BackLink } from "@/ui/shared/back-link";
import { Button, MoneyBill2, Tooltip } from "@dub/ui";
import { fetcher, formatDate, OG_AVATAR_URL } from "@dub/utils";
import { ChevronRight, MoneyBill2, Tooltip, UserCheck } from "@dub/ui";
import { cn, fetcher, formatDate } from "@dub/utils";
import { addMonths, isBefore } from "date-fns";
import { AlertCircle } from "lucide-react";
import Link from "next/link";
import { redirect, useParams } from "next/navigation";
import { memo, useMemo } from "react";
import useSWR from "swr";
Expand Down Expand Up @@ -50,109 +50,97 @@ export function ProgramCustomerPageClient() {
}

return (
<div className="mb-10 mt-2">
<BackLink href={`/programs/${programSlug}/earnings`}>Earnings</BackLink>
<div className="mt-5 flex items-center gap-4">
{customer ? (
<img
src={`${OG_AVATAR_URL}${customer.id}`}
alt={customer.email ?? customer.id}
className="size-8 rounded-full"
/>
) : (
<div className="size-8 animate-pulse rounded-full bg-neutral-200" />
)}

<div className="flex flex-col gap-1">
{customer ? (
<>
{customer["name"] && (
<h1 className="text-base font-semibold leading-tight text-neutral-900">
{customer["name"]}
</h1>
)}
{customer.email && (
<span className="text-sm font-medium text-neutral-500">
{customer.email}
</span>
)}
</>
) : (
<div className="h-5 w-24 animate-pulse rounded-md bg-neutral-200" />
)}
<PageContent
title={
<div className="flex items-center gap-1.5">
<div
// href={`/programs/${programSlug}/customers`}
// aria-label="Back to customers"
// title="Back to customers"
className={cn(
"bg-bg-subtle flex size-8 shrink-0 items-center justify-center rounded-lg",
// "hover:bg-bg-emphasis transition-[transform,background-color] duration-150 active:scale-95",
)}
>
<UserCheck className="size-4" />
</div>
<ChevronRight className="text-content-muted size-2.5 shrink-0 [&_*]:stroke-2" />
<div>
{customer ? (
customer.name || customer.email
) : (
<div className="h-6 w-32 animate-pulse rounded-md bg-neutral-200" />
)}
</div>
</div>
</div>
<div className="mt-8 grid grid-cols-1 items-start gap-x-16 gap-y-10 lg:grid-cols-[minmax(0,1fr)_240px]">
{/* Main content */}
<div className="flex flex-col gap-10">
<section className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-neutral-900">
Earnings
</h2>
{programEnrollment?.rewards && (
<Tooltip
content={
<ProgramRewardList
rewards={programEnrollment?.rewards}
className="gap-2 border-none p-3"
showModifiersTooltip={false}
/>
}
>
<div className="border-border-subtle flex cursor-default items-center justify-center gap-1.5 rounded-md border px-2 py-1 transition-all hover:bg-neutral-50">
<MoneyBill2 className="size-4" />
<span className="text-sm">Eligible rewards</span>
</div>
</Tooltip>
)}
</div>
{rewardPeriodEndDate &&
isBefore(rewardPeriodEndDate, new Date()) && (
<div className="flex items-center gap-2 rounded-lg border border-amber-100 bg-amber-50 px-3 py-2">
<AlertCircle className="size-4 shrink-0 text-amber-600" />
<p className="text-sm text-amber-900">
The earning period for this customer has ended as of{" "}
{formatDate(rewardPeriodEndDate)}. No future conversions
will be rewarded.
</p>
}
>
<PageWidthWrapper className="flex flex-col gap-6 pb-10">
<div className="@3xl/page:grid-cols-[minmax(440px,1fr)_minmax(0,360px)] grid grid-cols-1 gap-6">
<div className="@3xl/page:order-2">
<CustomerDetailsColumn
customer={customer}
customerActivity={customer?.activity}
isCustomerActivityLoading={!customer}
/>
</div>
<div className="@3xl/page:order-1">
<div className="border-border-subtle overflow-hidden rounded-xl border p-4">
<section className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-neutral-900">
Earnings
</h2>
{programEnrollment?.rewards && (
<Tooltip
content={
<ProgramRewardList
rewards={programEnrollment?.rewards}
className="gap-2 border-none p-3"
showModifiersTooltip={false}
/>
}
>
<div className="border-border-subtle flex cursor-default items-center justify-center gap-1.5 rounded-md border px-2 py-1 transition-all hover:bg-neutral-50">
<MoneyBill2 className="size-4" />
<span className="text-sm">Eligible rewards</span>
</div>
</Tooltip>
)}
</div>
)}
<EarningsTable customerId={customerId} />
</section>
{rewardPeriodEndDate &&
isBefore(rewardPeriodEndDate, new Date()) && (
<div className="flex items-center gap-2 rounded-lg border border-amber-100 bg-amber-50 px-3 py-2">
<AlertCircle className="size-4 shrink-0 text-amber-600" />
<p className="text-sm text-amber-900">
The earning period for this customer has ended as of{" "}
{formatDate(rewardPeriodEndDate)}. No future conversions
will be rewarded.
</p>
</div>
)}

<section className="flex flex-col">
<div className="flex items-center justify-between">
<h2 className="py-3 text-lg font-semibold text-neutral-900">
Activity
</h2>
<Link
href={`/programs/${programSlug}/events?interval=all&customerId=${customerId}`}
>
<Button
variant="secondary"
text="View all"
className="h-7 px-2"
/>
</Link>
<div className="border-border-subtle overflow-hidden rounded-lg border">
<EarningsTable customerId={customerId} />
</div>
</section>
</div>
<CustomerActivityList
activity={customer?.activity}
isLoading={!customer}
/>
</section>
</div>

{/* Right side details */}
<div className="-order-1 lg:order-1">
<CustomerDetailsColumn
customer={customer}
customerActivity={customer?.activity}
isCustomerActivityLoading={!customer}
/>
<section className="mt-3 flex flex-col px-4">
<div className="flex items-center justify-between">
<h2 className="py-3 text-lg font-semibold text-neutral-900">
Activity
</h2>
</div>
<CustomerActivityList
activity={customer?.activity}
isLoading={!customer}
/>
</section>
</div>
</div>
</div>
</div>
</PageWidthWrapper>
</PageContent>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import { PageContent } from "@/ui/layout/page-content";
import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper";
import { ProgramCustomerPageClient } from "./page-client";

export default function ProgramCustomer() {
return (
<PageContent>
<PageWidthWrapper className="flex flex-col gap-6 pb-10">
<ProgramCustomerPageClient />
</PageWidthWrapper>
</PageContent>
);
return <ProgramCustomerPageClient />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { CUSTOMER_PAGE_EVENTS_LIMIT } from "@/lib/constants/misc";
import useCustomer from "@/lib/swr/use-customer";
import useWorkspace from "@/lib/swr/use-workspace";
import { CommissionResponse, CustomerEnriched } from "@/lib/types";
import { CustomerPartnerEarningsTable } from "@/ui/customers/customer-partner-earnings-table";
import { ArrowUpRight } from "@dub/ui";
import { OG_AVATAR_URL, fetcher } from "@dub/utils";
import Link from "next/link";
import { redirect, useParams } from "next/navigation";
import { memo } from "react";
import useSWR from "swr";

export function CustomerEarningsPageClient() {
const { customerId } = useParams<{ customerId: string }>();

const { slug } = useWorkspace();
const { data: customer, isLoading } = useCustomer<CustomerEnriched>({
customerId,
query: { includeExpandedFields: true },
});

if (!customer && !isLoading) redirect(`/${slug}/customers`);

return !customer || (customer.partner && customer.programId) ? (
<section className="flex flex-col gap-4">
<h2 className="text-lg font-semibold text-neutral-900">
Partner earnings
</h2>
<div className="border-border-subtle flex flex-col overflow-hidden rounded-lg border">
<Link
href={`/${slug}/program/partners/${customer?.partner?.id}`}
target="_blank"
className="group flex items-center justify-between overflow-hidden bg-neutral-100 px-3 py-2.5"
>
<div className="flex min-w-0 items-center gap-3">
{customer?.partner ? (
<>
<img
src={
customer?.partner?.image ||
`${OG_AVATAR_URL}${customer?.partner?.name}`
}
alt={customer.partner.name}
className="size-5 rounded-full"
/>
<span className="block min-w-0 truncate text-sm font-medium text-neutral-900">
{customer.partner.name}
</span>
</>
) : (
<>
<div className="size-5 animate-pulse rounded-full bg-neutral-200" />
<div className="h-5 w-24 animate-pulse rounded bg-neutral-200" />
</>
)}
</div>
<ArrowUpRight className="size-3 shrink-0 -translate-x-0.5 translate-y-0.5 opacity-0 transition-[transform,opacity] group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />
</Link>

<div className="border-border-subtle border-t">
<PartnerEarningsTable customerId={customerId} />
</div>
</div>
</section>
) : null;
}

const PartnerEarningsTable = memo(({ customerId }: { customerId: string }) => {
const { id: workspaceId, slug } = useWorkspace();

const { data: commissions, isLoading: isComissionsLoading } = useSWR<
CommissionResponse[]
>(
`/api/commissions?${new URLSearchParams({
customerId,
workspaceId: workspaceId!,
pageSize: CUSTOMER_PAGE_EVENTS_LIMIT.toString(),
})}`,
fetcher,
);

const { data: totalCommissions, isLoading: isTotalCommissionsLoading } =
useSWR<{ all: { count: number } }>(
// Only fetch total earnings count if the earnings data is equal to the limit
commissions?.length === CUSTOMER_PAGE_EVENTS_LIMIT &&
`/api/commissions/count?${new URLSearchParams({
customerId,
workspaceId: workspaceId!,
})}`,
fetcher,
);

return (
<CustomerPartnerEarningsTable
commissions={commissions}
totalCommissions={
isTotalCommissionsLoading
? undefined
: totalCommissions?.all?.count ?? commissions?.length
}
viewAllHref={`/${slug}/program/commissions?customerId=${customerId}`}
isLoading={isComissionsLoading}
/>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CustomerEarningsPageClient } from "./page-client";

export default function CustomerEarningsPage() {
return <CustomerEarningsPageClient />;
}
Loading