Skip to content

Commit 8d32d97

Browse files
authored
Merge pull request dubinc#2371 from dubinc/communication-tab
Add "Help and support" step to the program onboarding
2 parents d3ad3f0 + e38d98b commit 8d32d97

15 files changed

Lines changed: 358 additions & 38 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"use client";
2+
3+
import { parseActionError } from "@/lib/actions/parse-action-errors";
4+
import { updateProgramAction } from "@/lib/actions/partners/update-program";
5+
import useProgram from "@/lib/swr/use-program";
6+
import useWorkspace from "@/lib/swr/use-workspace";
7+
import { ProgramProps } from "@/lib/types";
8+
import { Button } from "@dub/ui";
9+
import { useAction } from "next-safe-action/hooks";
10+
import { useForm } from "react-hook-form";
11+
import { toast } from "sonner";
12+
import { mutate } from "swr";
13+
import { SettingsRow } from "../settings-row";
14+
15+
type FormData = Pick<ProgramProps, "supportEmail" | "helpUrl" | "termsUrl">;
16+
17+
export function ProgramCommunication() {
18+
const { program } = useProgram();
19+
const { id: workspaceId } = useWorkspace();
20+
21+
const {
22+
register,
23+
handleSubmit,
24+
formState: { isDirty, isValid, isSubmitting },
25+
} = useForm<FormData>({
26+
mode: "onBlur",
27+
defaultValues: {
28+
supportEmail: program?.supportEmail,
29+
helpUrl: program?.helpUrl,
30+
termsUrl: program?.termsUrl,
31+
},
32+
});
33+
34+
const { executeAsync } = useAction(updateProgramAction, {
35+
onSuccess: async () => {
36+
toast.success("Communication settings updated successfully.");
37+
await mutate(`/api/programs/${program?.id}?workspaceId=${workspaceId}`);
38+
},
39+
onError: ({ error }) => {
40+
toast.error(
41+
parseActionError(error, "Failed to update communication settings."),
42+
);
43+
},
44+
});
45+
46+
const onSubmit = async (data: FormData) => {
47+
if (!workspaceId || !program?.id) {
48+
return;
49+
}
50+
51+
await executeAsync({
52+
...data,
53+
workspaceId,
54+
programId: program.id,
55+
helpUrl: data.helpUrl || null,
56+
termsUrl: data.termsUrl || null,
57+
});
58+
};
59+
60+
return (
61+
<form onSubmit={handleSubmit(onSubmit)}>
62+
<div className="rounded-lg border border-neutral-200 bg-white">
63+
<div className="p-6">
64+
<h2 className="inline-flex items-center gap-2 text-lg font-semibold text-neutral-900">
65+
Help and Support
66+
</h2>
67+
<p className="mt-1 text-sm text-neutral-600">
68+
Configure the support email, help center, and terms of service for
69+
your program.
70+
</p>
71+
</div>
72+
73+
<div className="divide-y divide-neutral-200 border-t border-neutral-200 px-6">
74+
<SettingsRow
75+
heading="Support Email"
76+
description="For partner support requests"
77+
required
78+
>
79+
<div className="flex items-center justify-end">
80+
<div className="w-full max-w-md">
81+
<input
82+
type="email"
83+
className="block w-full rounded-md border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
84+
placeholder="support@dub.co"
85+
{...register("supportEmail", {
86+
required: true,
87+
})}
88+
/>
89+
</div>
90+
</div>
91+
</SettingsRow>
92+
93+
<SettingsRow
94+
heading="Help Center"
95+
description="Program help articles and documentation"
96+
>
97+
<div className="flex items-center justify-end">
98+
<div className="w-full max-w-md">
99+
<input
100+
type="url"
101+
className="block w-full rounded-md border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
102+
{...register("helpUrl")}
103+
placeholder="https://dub.co/help"
104+
/>
105+
</div>
106+
</div>
107+
</SettingsRow>
108+
109+
<SettingsRow
110+
heading="Terms of Service"
111+
description="Program terms of service and legal information"
112+
>
113+
<div className="flex items-center justify-end">
114+
<div className="w-full max-w-md">
115+
<input
116+
type="url"
117+
className="block w-full rounded-md border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
118+
{...register("termsUrl")}
119+
placeholder="https://dub.co/legal/terms"
120+
/>
121+
</div>
122+
</div>
123+
</SettingsRow>
124+
</div>
125+
<div className="flex items-center justify-end gap-2 border-t border-neutral-200 px-6 py-4">
126+
<Button
127+
text="Save changes"
128+
variant="primary"
129+
className="h-8 w-fit"
130+
loading={isSubmitting}
131+
disabled={!isDirty || !isValid}
132+
/>
133+
</div>
134+
</div>
135+
</form>
136+
);
137+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ProgramCommunication } from "./page-client";
2+
3+
export default function ProgramSettingsCommunicationPage() {
4+
return <ProgramCommunication />;
5+
}

apps/web/app/app.dub.co/(dashboard)/[slug]/programs/[programId]/settings/links/links-settings.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ function LinksSettingsForm({ program }: { program: ProgramProps }) {
6767
mutate(`/api/programs/${program.id}?workspaceId=${workspaceId}`);
6868
},
6969
onError({ error }) {
70-
console.error(error);
7170
toast.error("Failed to update program.");
7271
},
7372
});
@@ -249,7 +248,7 @@ function LinksSettingsForm({ program }: { program: ProgramProps }) {
249248
loadingFolders && "opacity-50",
250249
)}
251250
{...register("defaultFolderId", {
252-
required: true,
251+
// required: true,
253252
})}
254253
disabled={loadingFolders}
255254
>

apps/web/app/app.dub.co/(dashboard)/[slug]/programs/[programId]/settings/program-settings-header.tsx

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,39 @@ export function ProgramSettingsHeader({
1717

1818
return (
1919
<div className="border-b border-gray-200">
20-
<TabSelect
21-
variant="accent"
22-
options={[
23-
{
24-
id: "rewards",
25-
label: "Rewards",
26-
href: `/${slug}/programs/${programId}/settings/rewards`,
27-
},
28-
{
29-
id: "discounts",
30-
label: "Discounts",
31-
href: `/${slug}/programs/${programId}/settings/discounts`,
32-
},
33-
{
34-
id: "links",
35-
label: "Links",
36-
href: `/${slug}/programs/${programId}/settings/links`,
37-
},
38-
{
39-
id: "branding",
40-
label: "Branding",
41-
href: `/${slug}/programs/${programId}/settings/branding`,
42-
},
43-
]}
44-
selected={page}
45-
/>
20+
<div className="scrollbar-hide overflow-x-auto">
21+
<TabSelect
22+
variant="accent"
23+
options={[
24+
{
25+
id: "rewards",
26+
label: "Rewards",
27+
href: `/${slug}/programs/${programId}/settings/rewards`,
28+
},
29+
{
30+
id: "discounts",
31+
label: "Discounts",
32+
href: `/${slug}/programs/${programId}/settings/discounts`,
33+
},
34+
{
35+
id: "links",
36+
label: "Links",
37+
href: `/${slug}/programs/${programId}/settings/links`,
38+
},
39+
{
40+
id: "branding",
41+
label: "Branding",
42+
href: `/${slug}/programs/${programId}/settings/branding`,
43+
},
44+
{
45+
id: "communication",
46+
label: "Communication",
47+
href: `/${slug}/programs/${programId}/settings/communication`,
48+
},
49+
]}
50+
selected={page}
51+
/>
52+
</div>
4653
</div>
4754
);
4855
}

apps/web/app/app.dub.co/(dashboard)/[slug]/programs/[programId]/settings/settings-row.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ import { PropsWithChildren } from "react";
33
export function SettingsRow({
44
heading,
55
description,
6+
required,
67
children,
7-
}: PropsWithChildren<{ heading: string; description?: string }>) {
8+
}: PropsWithChildren<{
9+
heading: string;
10+
description?: string;
11+
required?: boolean;
12+
}>) {
813
return (
914
<div className="grid grid-cols-1 gap-4 py-8 sm:grid-cols-2">
1015
<div className="flex flex-col gap-1">
11-
<h3 className="font-medium leading-none text-neutral-900">{heading}</h3>
16+
<h3 className="text-[15px] font-medium leading-none text-neutral-900">
17+
{heading} {required && <span className="text-red-700">*</span>}
18+
</h3>
1219
{description && (
1320
<p className="text-sm text-neutral-600">{description}</p>
1421
)}

apps/web/app/app.dub.co/(new-program)/[slug]/programs/new/form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function Form() {
137137
{...register("name", { required: true })}
138138
placeholder="Acme"
139139
autoFocus={!isMobile}
140-
className={"mt-2 max-w-full"}
140+
className="mt-2 max-w-full"
141141
/>
142142
</div>
143143

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"use client";
2+
3+
import { onboardProgramAction } from "@/lib/actions/partners/onboard-program";
4+
import useWorkspace from "@/lib/swr/use-workspace";
5+
import { ProgramData } from "@/lib/types";
6+
import { Button, Input, useMediaQuery } from "@dub/ui";
7+
import { useAction } from "next-safe-action/hooks";
8+
import { useRouter } from "next/navigation";
9+
import { useState } from "react";
10+
import { useFormContext } from "react-hook-form";
11+
import { toast } from "sonner";
12+
13+
export function Form() {
14+
const router = useRouter();
15+
const { isMobile } = useMediaQuery();
16+
const [hasSubmitted, setHasSubmitted] = useState(false);
17+
const { id: workspaceId, slug: workspaceSlug, mutate } = useWorkspace();
18+
19+
const {
20+
register,
21+
handleSubmit,
22+
watch,
23+
formState: { isSubmitting },
24+
} = useFormContext<ProgramData>();
25+
26+
const { executeAsync, isPending } = useAction(onboardProgramAction, {
27+
onSuccess: () => {
28+
router.push(`/${workspaceSlug}/programs/new/connect`);
29+
mutate();
30+
},
31+
onError: ({ error }) => {
32+
toast.error(error.serverError);
33+
},
34+
});
35+
36+
const onSubmit = async (data: ProgramData) => {
37+
if (!workspaceId) {
38+
return;
39+
}
40+
41+
setHasSubmitted(true);
42+
43+
await executeAsync({
44+
...data,
45+
termsUrl: data.termsUrl || null,
46+
helpUrl: data.helpUrl || null,
47+
workspaceId,
48+
step: "help-and-support",
49+
});
50+
};
51+
52+
const [supportEmail] = watch(["supportEmail"]);
53+
54+
return (
55+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
56+
<p className="text-sm text-neutral-600">
57+
These will be displayed to the partners on their dashboard. They can be
58+
added later, but it is recommended to have these setup before your first
59+
partner joins.
60+
</p>
61+
62+
<div className="space-y-6">
63+
<div>
64+
<label className="block text-sm font-medium text-neutral-800">
65+
Support email <span className="text-red-800">*</span>
66+
</label>
67+
<Input
68+
type="email"
69+
{...register("supportEmail", { required: true })}
70+
placeholder="support@dub.com"
71+
autoFocus={!isMobile}
72+
className="mt-2 w-full max-w-none"
73+
/>
74+
</div>
75+
76+
<div>
77+
<label className="block text-sm font-medium text-neutral-800">
78+
Help center URL
79+
</label>
80+
<Input
81+
type="url"
82+
{...register("helpUrl")}
83+
placeholder="https://dub.co/help"
84+
autoFocus={!isMobile}
85+
className="mt-2 w-full max-w-none"
86+
/>
87+
</div>
88+
89+
<div>
90+
<label className="block text-sm font-medium text-neutral-800">
91+
Terms of Service URL
92+
</label>
93+
<Input
94+
type="url"
95+
{...register("termsUrl")}
96+
placeholder="https://dub.co/legal/terms"
97+
autoFocus={!isMobile}
98+
className="mt-2 w-full max-w-none"
99+
/>
100+
</div>
101+
</div>
102+
103+
<Button
104+
text="Continue"
105+
className="w-full"
106+
loading={isSubmitting || isPending || hasSubmitted}
107+
disabled={!supportEmail}
108+
type="submit"
109+
/>
110+
</form>
111+
);
112+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { StepPage } from "../step-page";
2+
import { Form } from "./form";
3+
4+
export default function Page() {
5+
return (
6+
<StepPage title="Help and Support">
7+
<Form />
8+
</StepPage>
9+
);
10+
}

apps/web/app/app.dub.co/(new-program)/form-wrapper.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export function FormWrapper({ children }: { children: React.ReactNode }) {
2929
partners: programOnboarding.partners?.length
3030
? programOnboarding.partners
3131
: [{ email: "", key: "" }],
32+
supportEmail: programOnboarding.supportEmail || null,
33+
helpUrl: programOnboarding.helpUrl || null,
34+
termsUrl: programOnboarding.termsUrl || null,
3235
}
3336
: undefined,
3437
});

0 commit comments

Comments
 (0)