Skip to content

Commit

Permalink
feat(dashboard): Sign up Questionnaire (#7114)
Browse files Browse the repository at this point in the history
  • Loading branch information
scopsy authored Nov 27, 2024
1 parent e5d7b0f commit ab76a48
Show file tree
Hide file tree
Showing 27 changed files with 709 additions and 40 deletions.
2 changes: 1 addition & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@
"xyflow",
"zulip",
"zwnj",
"lstrip",
"SOLOPRENEUR",
"rstrip",
"truncatewords",
"xmlschema",
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@segment/analytics-next": "^1.73.0",
"@sentry/react": "^8.35.0",
"@tanstack/react-query": "^5.59.6",
"@types/js-cookie": "^3.0.6",
"@uiw/codemirror-extensions-langs": "^4.23.6",
"@uiw/codemirror-theme-white": "^4.23.6",
"@uiw/codemirror-themes": "^4.23.6",
Expand All @@ -64,6 +65,7 @@
"date-fns": "^4.1.0",
"flat": "^6.0.1",
"framer-motion": "^11.3.19",
"js-cookie": "^3.0.5",
"lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.2",
"lucide-react": "^0.439.0",
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
6 changes: 6 additions & 0 deletions apps/dashboard/src/api/organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { UpdateExternalOrganizationDto } from '@novu/shared';
import { post } from './api.client';

export function updateClerkOrgMetadata(data: UpdateExternalOrganizationDto) {
return post('/clerk/organization', data);
}
20 changes: 20 additions & 0 deletions apps/dashboard/src/api/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import { OrganizationTypeEnum, CompanySizeEnum, JobTitleEnum } from '@novu/shared';
import { post } from './api.client';
import * as Sentry from '@sentry/react';

export const sendTelemetry = async (event: string, data?: Record<string, unknown>): Promise<void> => {
await post('/telemetry/measure', {
event,
data,
});
};

interface IdentifyUserProps {
hubspotContext: string;
pageUri: string;
pageName: string;
jobTitle: JobTitleEnum;
organizationType: OrganizationTypeEnum;
companySize?: CompanySizeEnum;
}

export const identifyUser = async (userData: IdentifyUserProps) => {
try {
await post('/telemetry/identify', userData);
} catch (error) {
console.error('Error identifying user:', error);
Sentry.captureException(error);
}
};
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/auth-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ReactNode } from 'react';

export const AuthLayout = ({ children }: { children: ReactNode }) => {
return (
<div className="flex h-screen items-center justify-center gap-8 bg-[url('/images/auth/background.svg')]">
<div className="flex h-screen items-center justify-center gap-8 bg-[url('/images/auth/background.svg')] bg-cover bg-no-repeat">
<div className="flex max-w-[1100px] flex-1 flex-row">{children}</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/auth/auth-card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Card } from '../primitives/card';

export function AuthCard({ children }: { children: React.ReactNode }) {
return <Card className="flex h-[692px] w-full overflow-hidden">{children}</Card>;
return <Card className="flex min-h-[692px] w-full overflow-hidden">{children}</Card>;
}
18 changes: 5 additions & 13 deletions apps/dashboard/src/components/auth/create-organization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { OrganizationList as OrganizationListForm } from '@clerk/clerk-react';
import { ROUTES } from '../../utils/routes';
import { clerkSignupAppearance } from '../../utils/clerk-appearance';
import { AuthCard } from './auth-card';
import { RiArrowLeftSLine } from 'react-icons/ri';
import { StepIndicator } from './shared';

export default function OrganizationCreate() {
return (
<div className="mx-auto flex w-full max-w-[1130px] flex-col gap-3">
<AuthCard>
<div className="flex min-w-[564px] max-w-[564px] items-center p-[60px]">
<div className="flex flex-col gap-[4px]">
<StepIndicator />
<StepIndicator hideBackButton className="pl-[20px]" step={1} />

<OrganizationListForm
appearance={{
elements: {
Expand All @@ -22,24 +23,15 @@ export default function OrganizationCreate() {
hidePersonal
skipInvitationScreen
afterSelectOrganizationUrl={ROUTES.ENV}
afterCreateOrganizationUrl={ROUTES.ENV}
afterCreateOrganizationUrl={ROUTES.SIGNUP_QUESTIONNAIRE}
/>
</div>
</div>

<div className="w-full max-w-[564px] flex-1">
<img src="/images/auth/ui-org.svg" alt="create-org-illustration" className="opacity-70" />
<img src="/images/auth/ui-org.svg" alt="Novu dashboard overview" className="opacity-70" />
</div>
</AuthCard>
</div>
);
}

function StepIndicator(): JSX.Element {
return (
<div className="text-foreground-600 inline-flex items-center gap-[2px] pl-[20px]">
<RiArrowLeftSLine className="h-4 w-4" />
<span className="font-label-x-small text-xs">1/3</span>
</div>
);
}
217 changes: 217 additions & 0 deletions apps/dashboard/src/components/auth/questionnaire-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { Button } from '@/components/primitives/button';
import { CardDescription, CardTitle } from '@/components/primitives/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/primitives/select';
import React from 'react';
import { StepIndicator } from './shared';
import { JobTitleEnum, jobTitleToLabelMapper, OrganizationTypeEnum, CompanySizeEnum } from '@novu/shared';
import { useForm, Controller } from 'react-hook-form';
import { updateClerkOrgMetadata } from '../../api/organization';
import { hubspotCookie } from '../../utils/cookies';
import { identifyUser } from '../../api/telemetry';
import { useTelemetry } from '../../hooks';
import { TelemetryEvent } from '../../utils/telemetry';
import { useNavigate } from 'react-router-dom';
import { ROUTES } from '../../utils/routes';
import { useMutation } from '@tanstack/react-query';

interface QuestionnaireFormData {
jobTitle: JobTitleEnum;
organizationType: OrganizationTypeEnum;
companySize?: CompanySizeEnum;
}

interface SubmitQuestionnaireData {
jobTitle: JobTitleEnum;
organizationType: OrganizationTypeEnum;
companySize?: CompanySizeEnum;
pageUri: string;
pageName: string;
hubspotContext: string;
}

export function QuestionnaireForm() {
const { control, watch, handleSubmit } = useForm<QuestionnaireFormData>();
const submitQuestionnaireMutation = useSubmitQuestionnaire();

const selectedJobTitle = watch('jobTitle');
const selectedOrgType = watch('organizationType');
const companySize = watch('companySize');

const shouldShowCompanySize =
(selectedOrgType === OrganizationTypeEnum.COMPANY || selectedOrgType === OrganizationTypeEnum.AGENCY) &&
!!selectedJobTitle;

const isFormValid = React.useMemo(() => {
if (!selectedJobTitle || !selectedOrgType) return false;
if (shouldShowCompanySize && !companySize) return false;

return true;
}, [selectedJobTitle, selectedOrgType, shouldShowCompanySize, companySize]);

const onSubmit = async (data: QuestionnaireFormData) => {
const hubspotContext = hubspotCookie.get();

submitQuestionnaireMutation.mutate({
...data,
pageUri: window.location.href,
pageName: 'Create Organization Form',
hubspotContext: hubspotContext || '',
});
};

return (
<>
<div className="w-full max-w-[564px] px-0 pt-[80px]">
<div className="flex flex-col items-center gap-8">
<div className="flex w-[350px] flex-col gap-1">
<div className="flex w-full items-center gap-1.5">
<div className="flex flex-1 flex-col gap-1">
<StepIndicator step={2} />
<CardTitle className="text-foreground-900 text-lg font-medium">
Help us personalize your experience
</CardTitle>
</div>
</div>
<CardDescription className="text-foreground-400 text-xs">
This helps us set up Novu to match your goals and plan features and improvements.
</CardDescription>
</div>

<form onSubmit={handleSubmit(onSubmit)} className="flex w-[350px] flex-col gap-8">
<div className="flex flex-col gap-7">
<div className="flex flex-col gap-[4px]">
<label className="text-foreground-600 text-xs font-medium">Job title</label>
<Controller
name="jobTitle"
control={control}
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger
className={`shadow-regular-shadow-x-small h-[32px] w-full border border-[#E1E4EA] ${field.value ? 'text-[#0E121B]' : 'text-[#99A0AE]'}`}
>
<SelectValue placeholder="What's your nature of work" />
</SelectTrigger>
<SelectContent>
{Object.entries(jobTitleToLabelMapper).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</div>

{selectedJobTitle && (
<div className="flex flex-col gap-[4px]">
<label className="text-xs font-medium text-[#525866]">Organization type</label>
<div className="flex flex-wrap gap-[8px]">
<Controller
name="organizationType"
control={control}
render={({ field }) => (
<>
{Object.values(OrganizationTypeEnum).map((type) => (
<Button
key={type}
variant="outline"
size="xs"
type="button"
className={`h-[28px] rounded-full px-3 py-1 text-sm ${
field.value === type ? 'border-[#E1E4EA] bg-[#F2F5F8]' : 'border-[#E1E4EA]'
}`}
onClick={() => field.onChange(type)}
>
{type}
</Button>
))}
</>
)}
/>
</div>
</div>
)}

{shouldShowCompanySize && (
<div className="flex flex-col gap-[4px]">
<label className="text-xs font-medium text-[#525866]">Company size</label>
<div className="flex flex-wrap gap-[8px]">
<Controller
name="companySize"
control={control}
render={({ field }) => (
<>
{Object.values(CompanySizeEnum).map((size) => (
<Button
key={size}
variant="outline"
size="xs"
type="button"
className={`h-[28px] rounded-full px-3 py-1 text-sm ${
field.value === size ? 'border-[#E1E4EA] bg-[#F2F5F8]' : 'border-[#E1E4EA]'
}`}
onClick={() => field.onChange(size)}
>
{size}
</Button>
))}
</>
)}
/>
</div>
</div>
)}
</div>

{isFormValid && (
<div className="flex flex-col gap-3">
<Button className="bg-black" type="submit" disabled={submitQuestionnaireMutation.isPending}>
{submitQuestionnaireMutation.isPending ? 'Creating...' : 'Get started'}
</Button>
</div>
)}
</form>
</div>
</div>

<div className="w-full max-w-[564px] flex-1">
<img src="/images/auth/ui-org.svg" alt="create-org-illustration" />
</div>
</>
);
}

function useSubmitQuestionnaire() {
const track = useTelemetry();
const navigate = useNavigate();

return useMutation({
mutationFn: async (data: SubmitQuestionnaireData) => {
await updateClerkOrgMetadata({
companySize: data.companySize,
jobTitle: data.jobTitle,
organizationType: data.organizationType,
});

await identifyUser({
jobTitle: data.jobTitle,
pageUri: data.pageUri,
pageName: data.pageName,
hubspotContext: data.hubspotContext,
companySize: data.companySize,
organizationType: data.organizationType,
});

track(TelemetryEvent.CREATE_ORGANIZATION_FORM_SUBMITTED, {
location: 'web',
jobTitle: data.jobTitle,
companySize: data.companySize,
organizationType: data.organizationType,
});
},
onSuccess: () => {
navigate(ROUTES.USECASE_SELECT);
},
});
}
33 changes: 33 additions & 0 deletions apps/dashboard/src/components/auth/shared.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { RiArrowLeftSLine } from 'react-icons/ri';
import { cn } from '../../utils/ui';
import { useNavigate } from 'react-router-dom';

interface StepIndicatorProps {
step: number;
className?: string;
hideBackButton?: boolean;
}

export function StepIndicator({ step, className, hideBackButton }: StepIndicatorProps) {
const navigate = useNavigate();

function handleGoBack() {
navigate(-1);
}

return (
<div className={cn('text-foreground-600 inline-flex items-center gap-0.5', className)}>
{!hideBackButton && (
<button
onClick={handleGoBack}
className="transition-colors hover:text-gray-700"
type="button"
aria-label="Go back to previous step"
>
<RiArrowLeftSLine className="h-4 w-4" />
</button>
)}
<span className="font-label-x-small text-xs">{step}/3</span>
</div>
);
}
Loading

0 comments on commit ab76a48

Please sign in to comment.