forked from formbricks/formbricks
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Product onboarding with XM approach (formbricks#2770)
Co-authored-by: Johannes <[email protected]> Co-authored-by: Johannes <[email protected]> Co-authored-by: Matthias Nannt <[email protected]>
- Loading branch information
1 parent
3ab8092
commit f358254
Showing
94 changed files
with
1,300 additions
and
1,706 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
...p)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
"use client"; | ||
|
||
import Dance from "@/images/onboarding-dance.gif"; | ||
import Lost from "@/images/onboarding-lost.gif"; | ||
import { ArrowRight } from "lucide-react"; | ||
import Image from "next/image"; | ||
import { useRouter } from "next/navigation"; | ||
import { useEffect } from "react"; | ||
import { cn } from "@formbricks/lib/cn"; | ||
import { TEnvironment } from "@formbricks/types/environment"; | ||
import { TProductConfigChannel } from "@formbricks/types/product"; | ||
import { Button } from "@formbricks/ui/Button"; | ||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions"; | ||
|
||
interface ConnectWithFormbricksProps { | ||
environment: TEnvironment; | ||
webAppUrl: string; | ||
widgetSetupCompleted: boolean; | ||
channel: TProductConfigChannel; | ||
} | ||
|
||
export const ConnectWithFormbricks = ({ | ||
environment, | ||
webAppUrl, | ||
widgetSetupCompleted, | ||
channel, | ||
}: ConnectWithFormbricksProps) => { | ||
const router = useRouter(); | ||
|
||
const handleFinishOnboarding = async () => { | ||
if (!widgetSetupCompleted) { | ||
router.push(`/environments/${environment.id}/connect/invite`); | ||
return; | ||
} | ||
router.push(`/environments/${environment.id}/surveys`); | ||
}; | ||
|
||
useEffect(() => { | ||
const handleVisibilityChange = async () => { | ||
if (document.visibilityState === "visible") { | ||
router.refresh(); | ||
} | ||
}; | ||
|
||
document.addEventListener("visibilitychange", handleVisibilityChange); | ||
return () => { | ||
document.removeEventListener("visibilitychange", handleVisibilityChange); | ||
}; | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, []); | ||
|
||
return ( | ||
<div className="mt-6 flex w-5/6 flex-col items-center space-y-10 lg:w-2/3 2xl:w-1/2"> | ||
<div className="flex w-full space-x-10"> | ||
<div className="flex w-1/2 flex-col space-y-4"> | ||
<OnboardingSetupInstructions | ||
environmentId={environment.id} | ||
webAppUrl={webAppUrl} | ||
channel={channel} | ||
widgetSetupCompleted={widgetSetupCompleted} | ||
/> | ||
</div> | ||
<div | ||
className={cn( | ||
"flex h-[30rem] w-1/2 flex-col items-center justify-center rounded-lg border bg-slate-200 text-center shadow", | ||
widgetSetupCompleted ? "border-green-500 bg-green-100" : "" | ||
)}> | ||
{widgetSetupCompleted ? ( | ||
<div> | ||
<Image src={Dance} alt="lost" height={250} /> | ||
<p className="mt-6 text-xl font-bold">Connection successful ✅</p> | ||
</div> | ||
) : ( | ||
<div className="space-y-4"> | ||
<Image src={Lost} alt="lost" height={250} /> | ||
<p className="pt-4 text-slate-400">Waiting for your signal...</p> | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
<Button | ||
id="finishOnboarding" | ||
variant={widgetSetupCompleted ? "darkCTA" : "minimal"} | ||
onClick={handleFinishOnboarding} | ||
EndIcon={ArrowRight}> | ||
{widgetSetupCompleted ? "Finish Onboarding" : "Skip"} | ||
</Button> | ||
</div> | ||
); | ||
}; |
121 changes: 121 additions & 0 deletions
121
...(onboarding)/environments/[environmentId]/connect/components/InviteOrganizationMember.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
"use client"; | ||
|
||
import { inviteOrganizationMemberAction } from "@/app/(app)/(onboarding)/organizations/actions"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { useRouter } from "next/navigation"; | ||
import { FormProvider, useForm } from "react-hook-form"; | ||
import { toast } from "react-hot-toast"; | ||
import { z } from "zod"; | ||
import { TOrganization } from "@formbricks/types/organizations"; | ||
import { Button } from "@formbricks/ui/Button"; | ||
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@formbricks/ui/Form"; | ||
import { Input } from "@formbricks/ui/Input"; | ||
|
||
interface InviteOrganizationMemberProps { | ||
organization: TOrganization; | ||
environmentId: string; | ||
} | ||
|
||
const ZInviteOrganizationMemberDetails = z.object({ | ||
email: z.string().email(), | ||
inviteMessage: z.string().trim().min(1), | ||
}); | ||
type TInviteOrganizationMemberDetails = z.infer<typeof ZInviteOrganizationMemberDetails>; | ||
|
||
export const InviteOrganizationMember = ({ organization, environmentId }: InviteOrganizationMemberProps) => { | ||
const router = useRouter(); | ||
|
||
const form = useForm<TInviteOrganizationMemberDetails>({ | ||
defaultValues: { | ||
email: "", | ||
inviteMessage: "I'm looking into Formbricks to run targeted surveys. Can you help me set it up? 🙏", | ||
}, | ||
resolver: zodResolver(ZInviteOrganizationMemberDetails), | ||
}); | ||
const { isSubmitting } = form.formState; | ||
|
||
const handleInvite = async (data: TInviteOrganizationMemberDetails) => { | ||
try { | ||
await inviteOrganizationMemberAction(organization.id, data.email, "developer", data.inviteMessage); | ||
toast.success("Invite sent successful"); | ||
await finishOnboarding(); | ||
} catch (error) { | ||
toast.error("An unexpected error occurred"); | ||
} | ||
}; | ||
|
||
const finishOnboarding = async () => { | ||
router.push(`/environments/${environmentId}/surveys`); | ||
}; | ||
|
||
return ( | ||
<div className="mb-8 w-full max-w-xl space-y-8"> | ||
<FormProvider {...form}> | ||
<form onSubmit={form.handleSubmit(handleInvite)} className="w-full space-y-4"> | ||
<div className="space-y-4"> | ||
<FormField | ||
control={form.control} | ||
name="email" | ||
render={({ field, fieldState: { error } }) => ( | ||
<FormItem className="w-full space-y-4"> | ||
<FormLabel>Email</FormLabel> | ||
<FormControl> | ||
<div> | ||
<Input | ||
value={field.value} | ||
onChange={(email) => field.onChange(email)} | ||
placeholder="[email protected]" | ||
className=" bg-white" | ||
/> | ||
{error?.message && <FormError className="text-left">{error.message}</FormError>} | ||
</div> | ||
</FormControl> | ||
</FormItem> | ||
)} | ||
/> | ||
<FormField | ||
control={form.control} | ||
name="inviteMessage" | ||
render={({ field, fieldState: { error } }) => ( | ||
<FormItem className="w-full space-y-4"> | ||
<FormLabel>Invite Message</FormLabel> | ||
<FormControl> | ||
<div> | ||
<textarea | ||
rows={5} | ||
className="focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-transparent bg-white px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300" | ||
value={field.value} | ||
onChange={(inviteMessage) => field.onChange(inviteMessage)} | ||
/> | ||
{error?.message && <FormError className="text-left">{error.message}</FormError>} | ||
</div> | ||
</FormControl> | ||
</FormItem> | ||
)} | ||
/> | ||
|
||
<div className="flex w-full justify-end space-x-2"> | ||
<Button | ||
id="onboarding-inapp-invite-have-a-look-first" | ||
className="font-normal text-slate-400" | ||
variant="minimal" | ||
onClick={(e) => { | ||
e.preventDefault(); | ||
finishOnboarding(); | ||
}}> | ||
Skip | ||
</Button> | ||
<Button | ||
id="onboarding-inapp-invite-send-invite" | ||
variant="darkCTA" | ||
type={"submit"} | ||
loading={isSubmitting}> | ||
Invite | ||
</Button> | ||
</div> | ||
</div> | ||
</form> | ||
</FormProvider> | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.