Skip to content
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

Partner improvements #1772

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions apps/web/app/api/programs/[programId]/partners/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const GET = withWorkspace(
include: {
partner: true,
link: true,
application: true,
},
skip: (page - 1) * pageSize,
take: pageSize,
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/zod/schemas/partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { z } from "zod";
import { CustomerSchema } from "./customers";
import { getPaginationQuerySchema } from "./misc";
import { ProgramEnrollmentSchema } from "./programs";
import { ProgramApplicationSchema, ProgramEnrollmentSchema } from "./programs";
import { parseDateSchema } from "./utils";

export const PARTNERS_MAX_PAGE_SIZE = 100;
Expand Down Expand Up @@ -52,6 +52,7 @@ export const EnrolledPartnerSchema = PartnerSchema.omit({
})
.extend({
earnings: z.number(),
application: ProgramApplicationSchema.nullable(),
});

export const PAYOUTS_MAX_PAGE_SIZE = 100;
Expand Down
13 changes: 13 additions & 0 deletions apps/web/lib/zod/schemas/programs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ export const ProgramEnrollmentSchema = z.object({
createdAt: z.date(),
});

export const ProgramApplicationSchema = z.object({
id: z.string(),
programId: z.string(),
name: z.string(),
email: z.string(),
website: z.string().nullable(),
comments: z.string().nullable(),
createdAt: z.date(),
updatedAt: z.date(),
partnerId: z.string().nullable(),
proposal: z.string().nullable(),
});

export const getProgramMetricsQuerySchema = z.object({
interval: z.enum(intervals).default("30d"),
start: parseDateSchema.optional(),
Expand Down
193 changes: 127 additions & 66 deletions apps/web/ui/partners/partner-details-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
currencyFormatter,
DICEBEAR_AVATAR_URL,
formatDate,
formatDateTime,
getPrettyUrl,
nFormatter,
} from "@dub/utils";
Expand All @@ -50,6 +51,7 @@ function PartnerDetailsSheetContent({
setIsOpen,
}: PartnerDetailsSheetProps) {
const { slug } = useWorkspace();
const { program } = useProgram();

const badge = PartnerStatusBadges[partner.status];

Expand All @@ -60,6 +62,8 @@ function PartnerDetailsSheetContent({
"overview",
);

const isApplication = partner.status === "pending" && partner.application;

return (
<>
<div>
Expand Down Expand Up @@ -94,6 +98,18 @@ function PartnerDetailsSheetContent({
</StatusBadge>
)}
</div>
{isApplication && partner.application && (
<span className="mt-1 text-sm font-medium text-neutral-600">
{formatDateTime(partner.application.createdAt, {
month: "short",
year:
new Date(partner.application.createdAt).getFullYear() ===
new Date().getFullYear()
? undefined
: "numeric",
})}
</span>
)}
</div>
<div className="flex min-w-[40%] shrink grow basis-1/2 flex-wrap items-center justify-end gap-2">
{partner.link && (
Expand Down Expand Up @@ -121,82 +137,127 @@ function PartnerDetailsSheetContent({
</div>
</div>

{/* Stats */}
<div className="mt-6 flex divide-x divide-neutral-200">
{[
[
"Clicks",
!partner.link
? "-"
: nFormatter(partner.link?.clicks, { full: true }),
],
[
"Leads",
!partner.link
? "-"
: nFormatter(partner.link?.leads, { full: true }),
],
[
"Sales",
!partner.link
? "-"
: nFormatter(partner.link?.sales, { full: true }),
],
[
"Revenue",
!partner.link
? "-"
: currencyFormatter(saleAmount, {
minimumFractionDigits: saleAmount % 1 === 0 ? 0 : 2,
{isApplication ? (
<hr className="mt-6 border-neutral-200" />
) : (
<>
{/* Stats */}
<div className="mt-6 flex divide-x divide-neutral-200">
{[
[
"Clicks",
!partner.link
? "-"
: nFormatter(partner.link?.clicks, { full: true }),
],
[
"Leads",
!partner.link
? "-"
: nFormatter(partner.link?.leads, { full: true }),
],
[
"Sales",
!partner.link
? "-"
: nFormatter(partner.link?.sales, { full: true }),
],
[
"Revenue",
!partner.link
? "-"
: currencyFormatter(saleAmount, {
minimumFractionDigits: saleAmount % 1 === 0 ? 0 : 2,
maximumFractionDigits: 2,
}),
],
[
"Earnings",
currencyFormatter(earnings, {
minimumFractionDigits: earnings % 1 === 0 ? 0 : 2,
maximumFractionDigits: 2,
}),
],
[
"Earnings",
currencyFormatter(earnings, {
minimumFractionDigits: earnings % 1 === 0 ? 0 : 2,
maximumFractionDigits: 2,
}),
],
].map(([label, value]) => (
<div key={label} className="flex flex-col px-5 first:pl-0">
<span className="text-xs text-neutral-500">{label}</span>
<span className="text-base text-neutral-900">{value}</span>
],
].map(([label, value]) => (
<div key={label} className="flex flex-col px-5 first:pl-0">
<span className="text-xs text-neutral-500">{label}</span>
<span className="text-base text-neutral-900">{value}</span>
</div>
))}
</div>
))}
</div>

<div className="mt-6">
<ToggleGroup
className="grid w-full grid-cols-2 rounded-lg border-transparent bg-neutral-100 p-0.5"
optionClassName="justify-center text-neutral-600 hover:text-neutral-700"
indicatorClassName="rounded-md bg-white"
options={[
{ value: "overview", label: "Overview" },
{ value: "payouts", label: "Payouts" },
]}
selected={selectedTab}
selectAction={(value) => setSelectedTab(value as any)}
/>
</div>
<div className="mt-6">
<ToggleGroup
className="grid w-full grid-cols-2 rounded-lg border-transparent bg-neutral-100 p-0.5"
optionClassName="justify-center text-neutral-600 hover:text-neutral-700"
indicatorClassName="rounded-md bg-white"
options={[
{ value: "overview", label: "Overview" },
{ value: "payouts", label: "Payouts" },
]}
selected={selectedTab}
selectAction={(value) => setSelectedTab(value as any)}
/>
</div>
</>
)}
<div className="mt-6">
{selectedTab === "overview" && (
<div className="flex flex-col gap-6 text-sm text-neutral-500">
<h3 className="text-base font-semibold text-neutral-900">
About this partner
{isApplication ? "Application" : "About this partner"}
</h3>

<div>
<h4 className="font-semibold text-neutral-900">
Description
</h4>
<p className="mt-1.5">
{partner.bio || (
<span className="italic text-neutral-400">
No description provided
</span>
)}
</p>
<div className="flex flex-col gap-6">
<div>
<h4 className="font-semibold text-neutral-900">
Description
</h4>
<p className="mt-1.5">
{partner.bio || (
<span className="italic text-neutral-400">
No description provided
</span>
)}
</p>
</div>
{isApplication &&
[
{
key: "website",
name: "Website/Social media channel",
optional: true,
// TODO: Make URL clickable
},
{
key: "proposal",
name: `How do you plan to promote ${program?.name || "this program"}?`,
},
{
key: "comments",
name: "Any additional questions or comments?",
optional: true,
},
].map(({ key, name, optional }) => (
<div key={key}>
<h4 className="font-semibold text-neutral-900">
{name}
{optional && (
<span className="font-normal text-neutral-500">
{" "}
(optional)
</span>
)}
</h4>
<p className="mt-1.5">
{partner.application?.[key] || (
<span className="italic text-neutral-400">
No answer provided
</span>
)}
</p>
</div>
))}
</div>
</div>
)}
Expand Down
Loading