Skip to content

Commit e2a2b41

Browse files
authored
Fix declined status revoke invite + misc improvements (dubinc#3881)
1 parent 3d450c5 commit e2a2b41

13 files changed

Lines changed: 163 additions & 71 deletions

File tree

apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/network-partner-application-sheet.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
StatusBadge,
1919
Trophy,
2020
useKeyboardShortcut,
21+
useRouterStuff,
2122
User,
2223
Users,
2324
} from "@dub/ui";
@@ -49,6 +50,7 @@ export function NetworkPartnerApplicationSheet({
4950
}) {
5051
const [currentTabId, setCurrentTabId] =
5152
useState<NetworkPartnerSheetTabId>("about");
53+
const { queryParams } = useRouterStuff();
5254

5355
const PartnerDetails = (
5456
<div className="rounded-lg border border-neutral-200 bg-neutral-100 p-3">
@@ -127,6 +129,7 @@ export function NetworkPartnerApplicationSheet({
127129
<Sheet
128130
open={isOpen}
129131
onOpenChange={setIsOpen}
132+
onClose={() => queryParams({ del: "partnerId", scroll: false })}
130133
contentProps={{
131134
className: "md:w-[max(min(calc(100vw-334px),1170px),540px)]",
132135
}}

apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/page.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export default function NetworkApplicationsPage() {
3939
useRouterStuff();
4040

4141
const [detailsSheetState, setDetailsSheetState] = useState<
42-
{ open: false; partnerId: null } | { open: true; partnerId: string }
42+
| { open: false; partnerId: string | null }
43+
| { open: true; partnerId: string }
4344
>({ open: false, partnerId: null });
4445

4546
const {
@@ -399,15 +400,7 @@ export default function NetworkApplicationsPage() {
399400
: undefined
400401
}
401402
setIsOpen={(open) => {
402-
if (!open) {
403-
setDetailsSheetState({ open: false, partnerId: null });
404-
queryParams({ del: "partnerId", scroll: false });
405-
} else if (detailsSheetState.partnerId) {
406-
setDetailsSheetState({
407-
open: true,
408-
partnerId: detailsSheetState.partnerId,
409-
});
410-
}
403+
setDetailsSheetState((state) => ({ ...state, open }) as any);
411404
}}
412405
onReview={handleReviewPartner}
413406
/>

apps/web/app/(ee)/api/cron/cleanup/declined-invites/route.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ export async function POST(req: Request) {
2828
updatedAt: {
2929
lt: subDays(new Date(), 90),
3030
},
31-
// only delete if there are no messages (e.g. prior network messages)
32-
messages: {
33-
none: {},
34-
},
3531
},
3632
take: 250,
3733
});

apps/web/app/(ee)/api/cron/cleanup/rejected-applications/route.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ export async function POST(req: Request) {
3232
commissions: {
3333
none: {},
3434
},
35-
messages: {
36-
none: {},
37-
},
3835
},
3936
take: 250,
4037
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function UnapprovedProgramPage({
5353
const badge = PartnerStatusBadges[programEnrollment.status];
5454

5555
const { setShowConfirmModal, confirmModal } = useConfirmModal({
56-
title: "Withdraw Application",
56+
title: "Withdraw application",
5757
description: `Are you sure you want to withdraw your application for ${programEnrollment.program.name}? This will delete your application completely and you'll have to re-apply if you want to join again.`,
5858
confirmText: "Withdraw application",
5959
onConfirm: async () => {
@@ -105,7 +105,7 @@ export function UnapprovedProgramPage({
105105
<div className="mt-6">
106106
<Button
107107
variant="secondary"
108-
text="Withdraw Application"
108+
text="Withdraw application"
109109
onClick={() => setShowConfirmModal(true)}
110110
className="h-8 px-2.5"
111111
/>

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/apply/program-sidebar.tsx

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use client";
22

3+
import { submitNetworkProfileAction } from "@/lib/actions/partners/submit-network-profile";
4+
import { getNetworkProfileChecklistProgress } from "@/lib/network/get-network-profile-checklist-progress";
35
import { evaluateApplicationRequirements } from "@/lib/partners/evaluate-application-requirements";
46
import usePartnerProfile from "@/lib/swr/use-partner-profile";
57
import useProgramEnrollment from "@/lib/swr/use-program-enrollment";
@@ -10,14 +12,26 @@ import {
1012
RewardProps,
1113
} from "@/lib/types";
1214
import { applicationRequirementsSchema } from "@/lib/zod/schemas/programs";
15+
import { useConfirmModal } from "@/ui/modals/confirm-modal";
1316
import { LanderRewards } from "@/ui/partners/lander/lander-rewards";
17+
import { NetworkStatusBadges } from "@/ui/partners/partner-network/network-status-badges";
1418
import { PartnerStatusBadges } from "@/ui/partners/partner-status-badges";
1519
import { useProgramApplicationSheet } from "@/ui/partners/program-application-sheet";
1620
import { ProgramEligibilityCard } from "@/ui/partners/program-eligibility-card";
17-
import { BlurImage, Button, CircleCheck, Link4, StatusBadge } from "@dub/ui";
21+
import {
22+
BlurImage,
23+
Button,
24+
CircleCheck,
25+
Link4,
26+
ProgressCircle,
27+
StatusBadge,
28+
} from "@dub/ui";
1829
import { capitalize, cn, OG_AVATAR_URL } from "@dub/utils";
30+
import { useAction } from "next-safe-action/hooks";
31+
import Link from "next/link";
1932
import { redirect } from "next/navigation";
20-
import { useMemo, useState } from "react";
33+
import { ReactNode, useMemo, useState } from "react";
34+
import { toast } from "sonner";
2135

2236
export function ProgramSidebar({
2337
program,
@@ -33,7 +47,35 @@ export function ProgramSidebar({
3347
applicationRewards: RewardProps[];
3448
applicationDiscount: DiscountProps | null;
3549
}) {
36-
const { partner } = usePartnerProfile();
50+
const { partner, mutate } = usePartnerProfile();
51+
52+
const { completedCount, totalCount, isComplete } =
53+
getNetworkProfileChecklistProgress({
54+
partner,
55+
});
56+
57+
const { executeAsync: submitNetworkProfile } = useAction(
58+
submitNetworkProfileAction,
59+
{
60+
onSuccess: () => {
61+
toast.success("Application submitted successfully");
62+
},
63+
onError: ({ error }) => {
64+
toast.error(error.serverError);
65+
},
66+
},
67+
);
68+
69+
const { setShowConfirmModal, confirmModal } = useConfirmModal({
70+
title: "Submit application",
71+
description:
72+
"Are you sure you want to submit your Dub Network application for review? You won't be able to make changes to your application after submitting it.",
73+
confirmText: "Confirm submission",
74+
onConfirm: async () => {
75+
await submitNetworkProfile();
76+
await mutate();
77+
},
78+
});
3779
const { programEnrollment } = useProgramEnrollment({
3880
swrOpts: {
3981
keepPreviousData: true,
@@ -59,6 +101,84 @@ export function ProgramSidebar({
59101
? "You do not meet the eligibility requirements for this program"
60102
: undefined;
61103

104+
const [justApplied, setJustApplied] = useState(false);
105+
106+
const applyDisabledTooltip: ReactNode = justApplied
107+
? undefined
108+
: programEnrollment?.status === "pending"
109+
? "Your application is under review"
110+
: programEnrollment?.status &&
111+
["banned", "rejected", "deactivated"].includes(
112+
programEnrollment.status,
113+
)
114+
? `You were ${programEnrollment.status} from this program`
115+
: programEnrollment
116+
? undefined
117+
: !isComplete
118+
? (
119+
<div className="max-w-xs p-3 text-center">
120+
<div className="text-content-default text-pretty text-sm leading-5">
121+
Complete your profile to join the Dub Partner Network. Once
122+
approved, you can then apply to this program.
123+
</div>
124+
<Link
125+
href="/profile"
126+
className="bg-bg-subtle mt-3 flex items-center justify-center gap-2 rounded-lg px-2.5 py-1.5"
127+
>
128+
<ProgressCircle
129+
progress={completedCount / totalCount}
130+
className="text-green-500"
131+
/>
132+
<span className="text-content-default text-sm font-medium">
133+
{completedCount} of {totalCount} tasks completed
134+
</span>
135+
</Link>
136+
</div>
137+
)
138+
: partner && !["approved", "trusted"].includes(partner.networkStatus)
139+
? (() => {
140+
const networkStatusBadge =
141+
NetworkStatusBadges[partner.networkStatus];
142+
if (!("partnerTooltip" in networkStatusBadge)) {
143+
return null;
144+
}
145+
const {
146+
partnerTooltip,
147+
icon: Icon,
148+
className,
149+
} = networkStatusBadge;
150+
const { content, cta } = partnerTooltip;
151+
152+
return (
153+
<div className="max-w-xs space-y-2 p-4 text-center">
154+
<div className="text-content-default text-pretty text-sm leading-5">
155+
{content}
156+
</div>
157+
{partner.networkStatus === "draft" ? (
158+
<Button
159+
className="p-2"
160+
text={cta}
161+
onClick={() => setShowConfirmModal(true)}
162+
/>
163+
) : (
164+
<Link
165+
href="/profile"
166+
className={cn(
167+
"flex items-center justify-center gap-2 rounded-lg p-2",
168+
"ctaClassName" in partnerTooltip
169+
? partnerTooltip.ctaClassName
170+
: className,
171+
)}
172+
>
173+
<Icon className="size-4 shrink-0" />
174+
<span className="text-sm font-medium">{cta}</span>
175+
</Link>
176+
)}
177+
</div>
178+
);
179+
})()
180+
: requirementsNotMet;
181+
62182
const statusBadge = programEnrollment
63183
? {
64184
...PartnerStatusBadges,
@@ -69,8 +189,6 @@ export function ProgramSidebar({
69189
}[programEnrollment.status]
70190
: null;
71191

72-
const [justApplied, setJustApplied] = useState(false);
73-
74192
const buttonText = useMemo(() => {
75193
if (justApplied) return "Applied";
76194
if (!programEnrollment) return "Apply";
@@ -98,6 +216,7 @@ export function ProgramSidebar({
98216

99217
return (
100218
<div>
219+
{confirmModal}
101220
{programApplicationSheet}
102221
<div className="flex items-start justify-between gap-2">
103222
<BlurImage
@@ -159,11 +278,12 @@ export function ProgramSidebar({
159278
text={buttonText}
160279
icon={justApplied ? <CircleCheck className="size-4" /> : undefined}
161280
disabled={
162-
!!programEnrollment || justApplied || !!requirementsNotMet
281+
!applyDisabledTooltip &&
282+
(!!programEnrollment || justApplied)
163283
? true
164284
: undefined
165285
}
166-
disabledTooltip={requirementsNotMet}
286+
disabledTooltip={applyDisabledTooltip}
167287
onClick={() => setIsApplicationSheetOpen(true)}
168288
/>
169289
</div>

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/[programSlug]/header-controls.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ function ApplyButton({ program }: { program: NetworkProgramProps }) {
152152
href="/profile"
153153
className={cn(
154154
"flex items-center justify-center gap-2 rounded-lg p-2",
155-
className,
155+
"ctaClassName" in partnerTooltip
156+
? partnerTooltip.ctaClassName
157+
: className,
156158
)}
157159
>
158160
<Icon className="size-4 shrink-0" />

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx

Lines changed: 17 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -761,51 +761,32 @@ function RowMenuButton({
761761
content={
762762
<Command tabIndex={0} loop className="focus:outline-none">
763763
<Command.List className="w-screen text-sm focus-visible:outline-none sm:w-auto sm:min-w-[200px]">
764-
{row.original.status === "invited" ? (
764+
{["invited", "declined"].includes(row.original.status) ? (
765765
<Command.Group className="grid gap-px p-1.5">
766-
<MenuItem
767-
icon={Users6}
768-
label="Change group"
769-
onSelect={() => {
770-
setShowChangeGroupModal(true);
771-
setIsOpen(false);
772-
}}
773-
/>
774-
775-
<MenuItem
776-
icon={Tag}
777-
label="Update tags"
778-
onSelect={() => {
779-
setShowUpdatePartnerTagsModal(true);
780-
setIsOpen(false);
781-
}}
782-
/>
783-
784-
<MenuItem
785-
icon={
786-
isResendingInvite ? LoadingSpinner : EnvelopeArrowRight
787-
}
788-
label="Resend invite"
789-
onSelect={async () => {
790-
if (row.original.status !== "invited") {
791-
return;
766+
{row.original.status === "invited" && (
767+
<MenuItem
768+
icon={
769+
isResendingInvite ? LoadingSpinner : EnvelopeArrowRight
792770
}
771+
label="Resend invite"
772+
onSelect={async () => {
773+
if (row.original.status !== "invited") {
774+
return;
775+
}
793776

794-
await resendInvite({
795-
workspaceId,
796-
partnerId: row.original.id,
797-
});
798-
}}
799-
/>
777+
await resendInvite({
778+
workspaceId,
779+
partnerId: row.original.id,
780+
});
781+
}}
782+
/>
783+
)}
800784

801785
<MenuItem
802786
icon={isDeletingInvite ? LoadingSpinner : Trash}
803787
label="Delete invite"
804788
variant="danger"
805789
onSelect={async () => {
806-
if (row.original.status !== "invited") {
807-
return;
808-
}
809790
if (
810791
!window.confirm(
811792
"Are you sure you want to delete this invite? This action cannot be undone.",

apps/web/lib/actions/partners/delete-program-invite.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export const deleteProgramInviteAction = authActionClient
3434
partnerId,
3535
programId,
3636
},
37-
status: "invited", // can only delete invited enrollments
37+
status: {
38+
in: ["invited", "declined"],
39+
},
3840
},
3941
include: {
4042
program: true,

apps/web/lib/api/partners/bulk-delete-partners.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,9 @@ export async function bulkDeletePartners({
147147
// Delete the messages
148148
const deletedMessages = await prisma.message.deleteMany({
149149
where: {
150-
programEnrollment: {
151-
id: {
152-
in: programEnrollmentIds,
153-
},
150+
programId: ACME_PROGRAM_ID,
151+
partnerId: {
152+
in: partnerIds,
154153
},
155154
},
156155
});

0 commit comments

Comments
 (0)