style: onboarding screens (#1412)

* style: new onboarding screens

* chore: onboarding tour screens

* fix: build error

* fix: build errors

* style: default layout background

* chor: update user auth hook logic, style: new onboarding screens

* fix: component structure

* chore: tab responsiveness added

* fix: redirection logic

* style: welcome screens responsiveness

* chore: update workspace url input field

* style: mobile responsiveness added

* chore: complete onboarding workflow

* style: create workspace page design update

* style: workspace invitations page design update

* chore: update steps logic

* fix: step change logic

* style: tour steps
This commit is contained in:
Aaryan Khandelwal 2023-07-12 19:55:08 +05:30 committed by GitHub
parent 26f0e9da00
commit a1b09fcbc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1542 additions and 1080 deletions

View file

@ -348,12 +348,12 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
</div>
)}
{properties.created_on && (
<div className="flex items-center text-xs cursor-default text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-100">
{renderLongDetailDateFormat(issue.created_at)}
</div>
)}
{properties.updated_on && (
<div className="flex items-center text-xs cursor-default text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-100">
{renderLongDetailDateFormat(issue.updated_at)}
</div>
)}

View file

@ -1,5 +1,5 @@
export * from "./tour";
export * from "./invite-members";
export * from "./onboarding-card";
export * from "./join-workspaces";
export * from "./user-details";
export * from "./workspace";
export * from "./onboarding-logo";

View file

@ -1,87 +1,215 @@
// types
import { useForm } from "react-hook-form";
import useToast from "hooks/use-toast";
import React, { useEffect } from "react";
import useSWR, { mutate } from "swr";
// react-hook-form
import { Controller, useFieldArray, useForm } from "react-hook-form";
// services
import workspaceService from "services/workspace.service";
import { ICurrentUserResponse, IUser } from "types";
// ui components
import { MultiInput, PrimaryButton, SecondaryButton } from "components/ui";
import userService from "services/user.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import { ICurrentUserResponse, IWorkspace, OnboardingSteps } from "types";
// fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
type Props = {
setStep: React.Dispatch<React.SetStateAction<number | null>>;
workspace: any;
workspace: IWorkspace | undefined;
user: ICurrentUserResponse | undefined;
stepChange: (steps: Partial<OnboardingSteps>) => Promise<void>;
};
export const InviteMembers: React.FC<Props> = ({ setStep, workspace, user }) => {
type EmailRole = {
email: string;
role: 5 | 10 | 15 | 20;
};
type FormValues = {
emails: EmailRole[];
};
export const InviteMembers: React.FC<Props> = ({ workspace, user, stepChange }) => {
const { setToastAlert } = useToast();
const {
setValue,
watch,
control,
handleSubmit,
formState: { isSubmitting },
} = useForm<IUser>();
formState: { isSubmitting, errors },
} = useForm<FormValues>();
const { fields, append, remove } = useFieldArray({
control,
name: "emails",
});
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
const nextStep = async () => {
if (!user || !invitations) return;
const payload: Partial<OnboardingSteps> = {
workspace_invite: true,
};
// update onboarding status from this step if no invitations are present
if (invitations.length === 0) {
payload.workspace_join = true;
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
is_onboarded: true,
};
},
false
);
await userService.updateUserOnBoard({ userRole: user.role }, user);
}
await stepChange(payload);
};
const onSubmit = async (formData: FormValues) => {
if (!workspace) return;
const payload = { ...formData };
const onSubmit = async (formData: IUser) => {
await workspaceService
.inviteWorkspace(workspace.slug, formData, user)
.then(() => {
.inviteWorkspace(workspace.slug, payload, user)
.then(async () => {
setToastAlert({
type: "success",
title: "Invitations sent!",
title: "Success!",
message: "Invitations sent successfully.",
});
setStep(4);
await nextStep();
})
.catch((err) => console.log(err));
};
const checkEmail = watch("emails") && watch("emails").length > 0;
const appendField = () => {
append({ email: "", role: 15 });
};
useEffect(() => {
if (fields.length === 0) {
append([
{ email: "", role: 15 },
{ email: "", role: 15 },
{ email: "", role: 15 },
]);
}
}, [fields, append]);
return (
<form
className="flex w-full items-center justify-center"
className="w-full space-y-7 sm:space-y-10 overflow-hidden flex flex-col"
onSubmit={handleSubmit(onSubmit)}
onKeyDown={(e) => {
if (e.code === "Enter") e.preventDefault();
}}
>
<div className="flex w-full max-w-xl flex-col gap-12">
<div className="flex flex-col gap-6 rounded-[10px] bg-custom-background-100 p-7 shadow-md">
<h2 className="text-xl font-medium">Invite co-workers to your team</h2>
<div className="flex flex-col items-start justify-center gap-2.5">
<span>Email</span>
<div className="w-full">
<MultiInput
name="emails"
placeholder="Enter co-workers Email IDs"
watch={watch}
setValue={setValue}
className="w-full"
/>
<h2 className="text-xl sm:text-2xl font-semibold">Invite people to collaborate</h2>
<div className="md:w-3/5 text-sm h-full max-h-[40vh] flex flex-col overflow-hidden">
<div className="grid grid-cols-11 gap-x-4 mb-1 text-sm">
<h6 className="col-span-7">Co-workers Email</h6>
<h6 className="col-span-4">Role</h6>
</div>
<div className="space-y-3 sm:space-y-4 mb-3 h-full overflow-y-auto">
{fields.map((field, index) => (
<div key={field.id} className="group relative grid grid-cols-11 gap-4">
<div className="col-span-7">
<Controller
control={control}
name={`emails.${index}.email`}
rules={{
required: "Email ID is required",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid Email ID",
},
}}
render={({ field }) => (
<>
<Input
{...field}
className="text-xs sm:text-sm"
placeholder="Enter their email..."
/>
{errors.emails?.[index]?.email && (
<span className="text-red-500 text-xs">
{errors.emails?.[index]?.email?.message}
</span>
)}
</>
)}
/>
</div>
<div className="col-span-3">
<Controller
control={control}
name={`emails.${index}.role`}
rules={{ required: true }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
label={<span className="text-xs sm:text-sm">{ROLE[value]}</span>}
onChange={onChange}
width="w-full"
input
>
{Object.entries(ROLE).map(([key, value]) => (
<CustomSelect.Option key={key} value={parseInt(key)}>
{value}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
{fields.length > 1 && (
<button
type="button"
className="hidden group-hover:grid self-center place-items-center rounded -ml-3"
onClick={() => remove(index)}
>
<XMarkIcon className="h-3.5 w-3.5 text-custom-text-200" />
</button>
)}
</div>
</div>
</div>
<div className="flex w-full flex-col items-center justify-center gap-3">
<PrimaryButton
type="submit"
className="flex w-1/2 items-center justify-center text-center"
disabled={!checkEmail}
loading={isSubmitting}
size="md"
>
{isSubmitting ? "Inviting..." : "Continue"}
</PrimaryButton>
<SecondaryButton
type="button"
className="w-1/2 rounded-lg border-none bg-transparent"
size="md"
outline
onClick={() => setStep(4)}
>
Skip
</SecondaryButton>
))}
</div>
<button
type="button"
className="flex items-center gap-2 outline-custom-primary-100 bg-transparent text-custom-primary-100 text-xs font-medium py-2 pr-3"
onClick={appendField}
>
<PlusIcon className="h-3 w-3" />
Add more
</button>
</div>
<div className="flex items-center gap-4">
<PrimaryButton type="submit" loading={isSubmitting} size="md">
{isSubmitting ? "Sending..." : "Send Invite"}
</PrimaryButton>
<SecondaryButton size="md" onClick={nextStep} outline>
Skip this step
</SecondaryButton>
</div>
</form>
);

View file

@ -0,0 +1,155 @@
import React, { useState } from "react";
import useSWR, { mutate } from "swr";
// services
import workspaceService from "services/workspace.service";
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { CheckCircleIcon } from "@heroicons/react/24/outline";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IUser, IWorkspaceMemberInvitation, OnboardingSteps } from "types";
// fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
type Props = {
stepChange: (steps: Partial<OnboardingSteps>) => Promise<void>;
};
export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
const { user } = useUser();
const { data: invitations, mutate: mutateInvitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
const handleInvitation = (
workspace_invitation: IWorkspaceMemberInvitation,
action: "accepted" | "withdraw"
) => {
if (action === "accepted") {
setInvitationsRespond((prevData) => [...prevData, workspace_invitation.id]);
} else if (action === "withdraw") {
setInvitationsRespond((prevData) =>
prevData.filter((item: string) => item !== workspace_invitation.id)
);
}
};
// complete onboarding
const finishOnboarding = async () => {
if (!user) return;
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
is_onboarded: true,
};
},
false
);
await userService.updateUserOnBoard({ userRole: user.role }, user);
await stepChange({ workspace_join: true });
};
const submitInvitations = async () => {
if (invitationsRespond.length <= 0) return;
setIsJoiningWorkspaces(true);
await workspaceService
.joinWorkspaces({ invitations: invitationsRespond })
.then(async () => {
await mutateInvitations();
await finishOnboarding();
setIsJoiningWorkspaces(false);
})
.catch(() => setIsJoiningWorkspaces(false));
};
return (
<div className="w-full space-y-7 sm:space-y-10">
<h5 className="sm:text-lg">We see that someone has invited you to</h5>
<h4 className="text-xl sm:text-2xl font-semibold">Join a workspace</h4>
<div className="md:w-3/5 space-y-4">
{invitations &&
invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
return (
<div
key={invitation.id}
className={`flex cursor-pointer items-center gap-2 border py-5 px-3.5 rounded ${
isSelected
? "border-custom-primary-100"
: "border-custom-border-100 hover:bg-custom-background-80"
}`}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
>
<div className="flex-shrink-0">
<div className="grid place-items-center h-9 w-9 rounded">
{invitation.workspace.logo && invitation.workspace.logo !== "" ? (
<img
src={invitation.workspace.logo}
height="100%"
width="100%"
className="rounded"
alt={invitation.workspace.name}
/>
) : (
<span className="grid place-items-center h-9 w-9 py-1.5 px-3 rounded bg-gray-700 uppercase text-white">
{invitation.workspace.name[0]}
</span>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">
{truncateText(invitation.workspace.name, 30)}
</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div>
<span
className={`flex-shrink-0 ${
isSelected ? "text-custom-primary-100" : "text-custom-text-200"
}`}
>
<CheckCircleIcon className="h-5 w-5" />
</span>
</div>
);
})}
</div>
<div className="flex items-center gap-3">
<PrimaryButton
type="submit"
size="md"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
>
Accept & Join
</PrimaryButton>
<SecondaryButton size="md" onClick={finishOnboarding} outline>
Skip for now
</SecondaryButton>
</div>
</div>
);
};

View file

@ -1,29 +0,0 @@
import React from "react";
import Image from "next/image";
interface IOnboardingCard {
step: string;
title: string;
description: React.ReactNode | string;
imgURL: string;
}
type Props = {
data: IOnboardingCard;
gradient?: boolean;
};
export const OnboardingCard: React.FC<Props> = ({ data, gradient = false }) => (
<div
className={`flex flex-col items-center justify-center gap-7 rounded-[10px] px-14 pt-10 text-center ${
gradient ? "bg-gradient-to-b from-[#C1DDFF] via-brand-base to-transparent" : ""
}`}
>
<div className="h-44 w-full">
<Image src={data.imgURL} height="180" width="450" alt={data.title} />
</div>
<h3 className="text-2xl font-medium">{data.title}</h3>
<p className="text-base text-custom-text-200">{data.description}</p>
<span className="text-base text-custom-text-200">{data.step}</span>
</div>
);

View file

@ -1,29 +0,0 @@
import React from "react";
type Props = {
className?: string;
width?: string | number;
height?: string | number;
color?: string;
};
export const OnboardingLogo: React.FC<Props> = ({
width = "378",
height = "117",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 378 117"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M101.928 74V15.0505H128.464C134.757 15.0505 139.714 16.7721 143.335 20.2152C146.956 23.599 148.767 28.1998 148.767 34.0176C148.767 39.8354 146.956 44.4659 143.335 47.909C139.714 51.2929 134.757 52.9848 128.464 52.9848H108.606V74H101.928ZM108.606 46.7514H128.019C132.649 46.7514 136.152 45.6235 138.526 43.3676C140.901 41.1117 142.088 37.9951 142.088 34.0176C142.088 30.0995 140.901 27.0125 138.526 24.7567C136.152 22.5008 132.649 21.3729 128.019 21.3729H108.606V46.7514ZM152.411 74V11.6667H159.09V74H152.411ZM185.455 74.7124C181.121 74.7124 177.322 73.7032 174.057 71.6848C170.851 69.607 168.358 66.8168 166.577 63.3143C164.796 59.8117 163.906 55.953 163.906 51.7381C163.906 47.4638 164.796 43.6051 166.577 40.1619C168.358 36.6594 170.851 33.8989 174.057 31.8805C177.322 29.8027 181.121 28.7638 185.455 28.7638C189.136 28.7638 192.282 29.4762 194.894 30.9009C197.566 32.3257 199.732 34.2551 201.395 36.689V29.4762H208.073V74H201.395V66.8762C199.732 69.2508 197.566 71.1505 194.894 72.5752C192.282 74 189.136 74.7124 185.455 74.7124ZM186.346 68.6571C189.67 68.6571 192.46 67.8854 194.716 66.3419C197.031 64.7984 198.783 62.7503 199.97 60.1976C201.157 57.5856 201.751 54.7657 201.751 51.7381C201.751 48.6511 201.157 45.8313 199.97 43.2786C198.783 40.7259 197.031 38.6778 194.716 37.1343C192.46 35.5908 189.67 34.819 186.346 34.819C183.08 34.819 180.261 35.5908 177.886 37.1343C175.511 38.6778 173.701 40.7259 172.454 43.2786C171.207 45.8313 170.584 48.6511 170.584 51.7381C170.584 54.7657 171.207 57.5856 172.454 60.1976C173.701 62.7503 175.511 64.7984 177.886 66.3419C180.261 67.8854 183.08 68.6571 186.346 68.6571ZM215.618 74V29.4762H222.296V36.4219C223.899 34.2848 225.858 32.4741 228.174 30.99C230.489 29.5059 233.457 28.7638 237.078 28.7638C240.165 28.7638 243.045 29.5059 245.716 30.99C248.447 32.4148 250.643 34.5816 252.305 37.4905C254.027 40.34 254.888 43.8722 254.888 48.0871V74H248.209V48.2652C248.209 44.2284 247.052 40.993 244.736 38.559C242.421 36.0657 239.423 34.819 235.743 34.819C233.249 34.819 230.993 35.383 228.975 36.5109C226.957 37.6389 225.324 39.2417 224.077 41.3195C222.89 43.3379 222.296 45.6829 222.296 48.3543V74H215.618ZM281.816 74.7124C277.305 74.7124 273.357 73.7032 269.973 71.6848C266.589 69.607 263.948 66.8168 262.048 63.3143C260.208 59.8117 259.287 55.953 259.287 51.7381C259.287 47.4638 260.178 43.6051 261.959 40.1619C263.74 36.6594 266.292 33.8989 269.617 31.8805C272.941 29.8027 276.859 28.7638 281.371 28.7638C285.942 28.7638 289.86 29.8027 293.125 31.8805C296.45 33.8989 299.003 36.6594 300.784 40.1619C302.565 43.6051 303.455 47.4638 303.455 51.7381V54.4095H266.144C266.5 57.0216 267.331 59.4259 268.637 61.6224C270.003 63.7595 271.813 65.4811 274.069 66.7871C276.325 68.0338 278.937 68.6571 281.905 68.6571C285.052 68.6571 287.694 67.9744 289.831 66.609C291.968 65.1843 293.63 63.3736 294.817 61.1771H302.119C300.576 65.1546 298.112 68.4197 294.728 70.9724C291.404 73.4657 287.1 74.7124 281.816 74.7124ZM266.233 48.1762H296.509C295.916 44.3768 294.313 41.2008 291.701 38.6481C289.089 36.0954 285.645 34.819 281.371 34.819C277.097 34.819 273.654 36.0954 271.042 38.6481C268.489 41.2008 266.886 44.3768 266.233 48.1762Z" />
<path d="M81 8H27V36H54V63H81V8Z" fill="#3F76FF" />
<rect y="36" width="27" height="27" fill="#3F76FF" />
<rect x="27" y="63" width="27" height="27" fill="#3F76FF" />
</svg>
);

View file

@ -0,0 +1,2 @@
export * from "./root";
export * from "./sidebar";

View file

@ -0,0 +1,157 @@
import { useState } from "react";
import Image from "next/image";
// hooks
import useUser from "hooks/use-user";
// components
import { TourSidebar } from "components/onboarding";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
// images
import PlaneWhiteLogo from "public/plane-logos/white-horizontal.svg";
import IssuesTour from "public/onboarding/issues.svg";
import CyclesTour from "public/onboarding/cycles.svg";
import ModulesTour from "public/onboarding/modules.svg";
import ViewsTour from "public/onboarding/views.svg";
import PagesTour from "public/onboarding/pages.svg";
type Props = {
onComplete: () => void;
};
export type TTourSteps = "welcome" | "issues" | "cycles" | "modules" | "views" | "pages";
const TOUR_STEPS: {
key: TTourSteps;
title: string;
description: string;
image: any;
prevStep?: TTourSteps;
nextStep?: TTourSteps;
}[] = [
{
key: "issues",
title: "Plan with issues",
description:
"The issue is the building block of the Plane. Most concepts in Plane are either associated with issues and their properties.",
image: IssuesTour,
nextStep: "cycles",
},
{
key: "cycles",
title: "Move with cycles",
description:
"Cycles help you and your team to progress faster, similar to the sprints commonly used in agile development.",
image: CyclesTour,
prevStep: "issues",
nextStep: "modules",
},
{
key: "modules",
title: "Break into modules",
description:
"Modules break your big think into Projects or Features, to help you organize better.",
image: ModulesTour,
prevStep: "cycles",
nextStep: "views",
},
{
key: "views",
title: "Views",
description:
"Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.",
image: ViewsTour,
prevStep: "modules",
nextStep: "pages",
},
{
key: "pages",
title: "Document with pages",
description:
"Modules break your big think into Projects or Features, to help you organize better.",
image: PagesTour,
prevStep: "views",
},
];
export const TourRoot: React.FC<Props> = ({ onComplete }) => {
const [step, setStep] = useState<TTourSteps>("welcome");
const { user } = useUser();
const currentStep = TOUR_STEPS.find((tourStep) => tourStep.key === step);
return (
<>
{step === "welcome" ? (
<div className="w-4/5 md:w-1/2 lg:w-2/5 h-3/4 bg-custom-background-100 rounded-[10px] overflow-hidden">
<div className="h-full overflow-hidden">
<div className="h-3/5 bg-custom-primary-100 grid place-items-center">
<Image src={PlaneWhiteLogo} alt="Plane White Logo" />
</div>
<div className="h-2/5 overflow-y-auto p-6">
<h3 className="font-medium text-lg">
Welcome to Plane, {user?.first_name} {user?.last_name}
</h3>
<p className="text-custom-text-200 text-sm mt-3">
We{"'"}re glad that you decided to try out Plane. You can now manage your projects
with ease. Get started by creating a project.
</p>
<div className="flex items-center gap-6 mt-8">
<PrimaryButton onClick={() => setStep("issues")}>Take a Product Tour</PrimaryButton>
<button
type="button"
className="outline-custom-text-100 bg-transparent text-custom-primary-100 text-xs font-medium"
onClick={onComplete}
>
No thanks, I will explore it myself
</button>
</div>
</div>
</div>
</div>
) : (
<div className="relative w-4/5 md:w-1/2 lg:w-3/5 h-3/5 sm:h-3/4 bg-custom-background-100 rounded-[10px] grid grid-cols-10 overflow-hidden">
<button
type="button"
className="fixed top-[19%] sm:top-[11.5%] right-[9%] md:right-[24%] lg:right-[19%] border border-custom-text-100 rounded-full p-1 translate-x-1/2 -translate-y-1/2 z-10 cursor-pointer"
onClick={onComplete}
>
<XMarkIcon className="h-3 w-3 text-custom-text-100" />
</button>
<TourSidebar step={step} setStep={setStep} />
<div className="col-span-10 lg:col-span-7 h-full overflow-hidden">
<div className="flex justify-end items-end h-1/2 sm:h-3/5 overflow-hidden bg-custom-primary-100">
<Image src={currentStep?.image} alt={currentStep?.title} />
</div>
<div className="flex flex-col h-1/2 sm:h-2/5 p-4 overflow-y-auto">
<h3 className="font-medium text-lg">{currentStep?.title}</h3>
<p className="text-custom-text-200 text-sm mt-3">{currentStep?.description}</p>
<div className="h-full flex items-end justify-between gap-4 mt-3">
<div className="flex items-center gap-4">
{currentStep?.prevStep && (
<SecondaryButton onClick={() => setStep(currentStep.prevStep ?? "welcome")}>
Back
</SecondaryButton>
)}
{currentStep?.nextStep && (
<PrimaryButton onClick={() => setStep(currentStep.nextStep ?? "issues")}>
Next
</PrimaryButton>
)}
</div>
{TOUR_STEPS.findIndex((tourStep) => tourStep.key === step) ===
TOUR_STEPS.length - 1 && (
<PrimaryButton onClick={onComplete}>Create my first project</PrimaryButton>
)}
</div>
</div>
</div>
</div>
)}
</>
);
};

View file

@ -0,0 +1,70 @@
// icons
import { ContrastIcon, LayerDiagonalIcon, PeopleGroupIcon, ViewListIcon } from "components/icons";
import { DocumentTextIcon } from "@heroicons/react/24/outline";
// types
import { TTourSteps } from "./root";
const sidebarOptions: {
key: TTourSteps;
icon: any;
}[] = [
{
key: "issues",
icon: LayerDiagonalIcon,
},
{
key: "cycles",
icon: ContrastIcon,
},
{
key: "modules",
icon: PeopleGroupIcon,
},
{
key: "views",
icon: ViewListIcon,
},
{
key: "pages",
icon: DocumentTextIcon,
},
];
type Props = {
step: TTourSteps;
setStep: React.Dispatch<React.SetStateAction<TTourSteps>>;
};
export const TourSidebar: React.FC<Props> = ({ step, setStep }) => (
<div className="hidden lg:block col-span-3 p-8 bg-custom-background-90">
<h3 className="font-medium text-lg">
Let{"'"}s get started!
<br />
Get more out of Plane.
</h3>
<div className="mt-8 space-y-5">
{sidebarOptions.map((option) => (
<h5
key={option.key}
className={`pr-2 py-0.5 pl-3 flex items-center gap-2 capitalize font-medium text-sm border-l-[3px] cursor-pointer ${
step === option.key
? "text-custom-primary-100 border-custom-primary-100"
: "text-custom-text-200 border-transparent"
}`}
onClick={() => setStep(option.key)}
>
<option.icon
className={`h-5 w-5 flex-shrink-0 ${
step === option.key ? "text-custom-primary-100" : "text-custom-text-200"
}`}
color={`${
step === option.key ? "rgb(var(--color-primary-100))" : "rgb(var(--color-text-200))"
}`}
aria-hidden="true"
/>
{option.key}
</h5>
))}
</div>
</div>
);

View file

@ -1,7 +1,9 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// hooks
import useToast from "hooks/use-toast";
// services
@ -9,8 +11,10 @@ import userService from "services/user.service";
// ui
import { CustomSelect, Input, PrimaryButton } from "components/ui";
// types
import { IUser } from "types";
// constant
import { ICurrentUserResponse, IUser } from "types";
// fetch-keys
import { CURRENT_USER } from "constants/fetch-keys";
// constants
import { USER_ROLES } from "constants/workspace";
const defaultValues: Partial<IUser> = {
@ -21,11 +25,9 @@ const defaultValues: Partial<IUser> = {
type Props = {
user?: IUser;
setStep: React.Dispatch<React.SetStateAction<number | null>>;
setUserRole: React.Dispatch<React.SetStateAction<string | null>>;
};
export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) => {
export const UserDetails: React.FC<Props> = ({ user }) => {
const { setToastAlert } = useToast();
const {
@ -39,17 +41,40 @@ export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) =>
});
const onSubmit = async (formData: IUser) => {
if (!user) return;
const payload: Partial<IUser> = {
...formData,
onboarding_step: {
...user.onboarding_step,
profile_complete: true,
},
};
await userService
.updateUser(formData)
.updateUser(payload)
.then(() => {
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
...payload,
};
},
false
);
setToastAlert({
title: "User details updated successfully!",
type: "success",
title: "Success!",
message: "Details updated successfully.",
});
setStep(2);
})
.catch((err) => {
console.log(err);
mutate(CURRENT_USER);
});
};
@ -60,90 +85,88 @@ export const UserDetails: React.FC<Props> = ({ user, setStep, setUserRole }) =>
last_name: user.last_name,
role: user.role,
});
setUserRole(user.role);
}
}, [user, reset, setUserRole]);
}, [user, reset]);
return (
<form className="flex w-full items-center justify-center" onSubmit={handleSubmit(onSubmit)}>
<div className="flex w-full max-w-xl flex-col gap-7">
<div className="flex flex-col rounded-[10px] bg-custom-background-100 shadow-md">
<div className="flex flex-col gap-2 justify-center px-7 pt-7 pb-3.5">
<h3 className="text-base font-semibold text-custom-text-100">User Details</h3>
<p className="text-sm text-custom-text-200">
Enter your details as a first step to open your Plane account.
</p>
</div>
<form
className="h-full w-full space-y-7 sm:space-y-10 overflow-y-auto sm:flex sm:flex-col sm:items-start sm:justify-center"
onSubmit={handleSubmit(onSubmit)}
>
<div className="relative sm:text-lg">
<div className="text-custom-primary-100 absolute -top-1 -left-3">{'"'}</div>
<h5>Hey there 👋🏻</h5>
<h5 className="mt-5 mb-6">Let{"'"}s get you onboard!</h5>
<h4 className="text-xl sm:text-2xl font-semibold">Set up your Plane profile.</h4>
</div>
<div className="flex flex-col justify-between gap-4 px-7 py-3.5 sm:flex-row">
<div className="flex flex-col items-start justify-center gap-1 w-full sm:w-1/2">
<span className="mb-1.5">First name</span>
<Input
name="first_name"
autoComplete="off"
register={register}
validations={{
required: "First name is required",
}}
error={errors.first_name}
/>
</div>
<div className="flex flex-col items-start justify-center gap-1 w-full sm:w-1/2">
<span className="mb-1.5">Last name</span>
<Input
name="last_name"
autoComplete="off"
register={register}
validations={{
required: "Last name is required",
}}
error={errors.last_name}
/>
</div>
</div>
<div className="flex flex-col items-start justify-center gap-2.5 border-t border-custom-border-100 px-7 pt-3.5 pb-7">
<span>What is your role?</span>
<div className="w-full">
<Controller
name="role"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={(value: any) => {
onChange(value);
setUserRole(value ?? null);
}}
label={value ? value.toString() : "Select your role"}
input
width="w-full"
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.role && <span className="text-sm text-red-500">{errors.role.message}</span>}
</div>
</div>
<div className="space-y-7 sm:w-3/4 md:w-2/5">
<div className="space-y-1 text-sm">
<label htmlFor="firstName">First Name</label>
<Input
id="firstName"
name="first_name"
autoComplete="off"
placeholder="Enter your first name..."
register={register}
validations={{
required: "First name is required",
}}
error={errors.first_name}
/>
</div>
<div className="flex w-full items-center justify-center ">
<PrimaryButton
type="submit"
className="flex w-1/2 items-center justify-center text-center"
size="md"
disabled={isSubmitting}
>
{isSubmitting ? "Updating..." : "Continue"}
</PrimaryButton>
<div className="space-y-1 text-sm">
<label htmlFor="lastName">Last Name</label>
<Input
id="lastName"
name="last_name"
autoComplete="off"
register={register}
placeholder="Enter your last name..."
validations={{
required: "Last name is required",
}}
error={errors.last_name}
/>
</div>
<div className="space-y-1 text-sm">
<span>What{"'"}s your role?</span>
<div className="w-full">
<Controller
name="role"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={(val: any) => onChange(val)}
label={
value ? (
value.toString()
) : (
<span className="text-gray-400">Select your role...</span>
)
}
input
width="w-full"
verticalPosition="top"
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.role && <span className="text-sm text-red-500">{errors.role.message}</span>}
</div>
</div>
</div>
<PrimaryButton type="submit" size="md" disabled={isSubmitting}>
{isSubmitting ? "Updating..." : "Continue"}
</PrimaryButton>
</form>
);
};

View file

@ -1,249 +1,50 @@
import { useState } from "react";
import useSWR from "swr";
// headless ui
import { Tab } from "@headlessui/react";
// services
import workspaceService from "services/workspace.service";
// ui
import { SecondaryButton } from "components/ui";
// types
import { ICurrentUserResponse, IWorkspaceMemberInvitation } from "types";
// fetch-keys
import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
import { ICurrentUserResponse, OnboardingSteps } from "types";
// constants
import { CreateWorkspaceForm } from "components/workspace";
// ui
import { PrimaryButton } from "components/ui";
import { getFirstCharacters, truncateText } from "helpers/string.helper";
type Props = {
setStep: React.Dispatch<React.SetStateAction<number | null>>;
setWorkspace: React.Dispatch<React.SetStateAction<any>>;
user: ICurrentUserResponse | undefined;
updateLastWorkspace: () => Promise<void>;
stepChange: (steps: Partial<OnboardingSteps>) => Promise<void>;
};
export const Workspace: React.FC<Props> = ({ setStep, setWorkspace, user }) => {
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
export const Workspace: React.FC<Props> = ({ user, updateLastWorkspace, stepChange }) => {
const [defaultValues, setDefaultValues] = useState({
name: "",
slug: "",
company_size: null,
organization_size: "",
});
const [currentTab, setCurrentTab] = useState("create");
const { data: invitations, mutate } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
const completeStep = async () => {
if (!user) return;
const handleInvitation = (
workspace_invitation: IWorkspaceMemberInvitation,
action: "accepted" | "withdraw"
) => {
if (action === "accepted") {
setInvitationsRespond((prevData) => [...prevData, workspace_invitation.id]);
} else if (action === "withdraw") {
setInvitationsRespond((prevData) =>
prevData.filter((item: string) => item !== workspace_invitation.id)
);
}
await stepChange({
workspace_create: true,
});
await updateLastWorkspace();
};
const submitInvitations = async () => {
if (invitationsRespond.length <= 0) return;
setIsJoiningWorkspaces(true);
await workspaceService
.joinWorkspaces({ invitations: invitationsRespond })
.then(async () => {
await mutate();
setStep(4);
setIsJoiningWorkspaces(false);
})
.catch((err) => {
console.error(err);
setIsJoiningWorkspaces(false);
});
};
const currentTabValue = (tab: string | null) => {
switch (tab) {
case "join":
return 0;
case "create":
return 1;
default:
return 1;
}
};
console.log("invitations:", invitations);
return (
<div className="grid w-full place-items-center">
<Tab.Group
as="div"
className="flex h-[442px] w-full max-w-xl flex-col justify-between rounded-[10px] bg-custom-background-100 shadow-md"
defaultIndex={currentTabValue(currentTab)}
onChange={(i) => {
switch (i) {
case 0:
return setCurrentTab("join");
case 1:
return setCurrentTab("create");
default:
return setCurrentTab("create");
<div className="w-full space-y-7 sm:space-y-10">
<h4 className="text-xl sm:text-2xl font-semibold">Create your workspace</h4>
<div className="sm:w-3/4 md:w-2/5">
<CreateWorkspaceForm
onSubmit={completeStep}
defaultValues={defaultValues}
setDefaultValues={setDefaultValues}
user={user}
secondaryButton={
<SecondaryButton onClick={() => stepChange({ profile_complete: false })}>
Back
</SecondaryButton>
}
}}
>
<Tab.List as="div" className="flex flex-col gap-3 px-7 pt-7 pb-3.5">
<div className="flex flex-col gap-2 justify-center">
<h3 className="text-base font-semibold text-custom-text-100">Workspace</h3>
<p className="text-sm text-custom-text-200">
Create or join the workspace to get started with Plane.
</p>
</div>
<div className="text-gray-8 flex items-center justify-start gap-3 text-sm">
<Tab
className={({ selected }) =>
`rounded-3xl border px-4 py-2 outline-none ${
selected
? "border-custom-primary bg-custom-primary text-white font-medium"
: "border-custom-border-100 bg-custom-background-100 hover:bg-custom-background-80"
}`
}
>
Invited Workspace
</Tab>
<Tab
className={({ selected }) =>
`rounded-3xl border px-4 py-2 outline-none ${
selected
? "border-custom-primary bg-custom-primary text-white font-medium"
: "border-custom-border-100 bg-custom-background-100 hover:bg-custom-background-80"
}`
}
>
New Workspace
</Tab>
</div>
</Tab.List>
<Tab.Panels as="div" className="h-full">
<Tab.Panel className="h-full">
<div className="flex h-full w-full flex-col">
<div className="h-[280px] overflow-y-auto px-7">
{invitations && invitations.length > 0 ? (
invitations.map((invitation) => (
<div key={invitation.id}>
<label
className={`group relative flex cursor-pointer items-start space-x-3 border-2 border-transparent py-4`}
htmlFor={invitation.id}
>
<div className="flex-shrink-0">
<span className="inline-flex h-10 w-10 items-center justify-center rounded-lg">
{invitation.workspace.logo && invitation.workspace.logo !== "" ? (
<img
src={invitation.workspace.logo}
height="100%"
width="100%"
className="rounded"
alt={invitation.workspace.name}
/>
) : (
<span className="flex h-full w-full items-center justify-center rounded-xl bg-gray-700 p-4 uppercase text-white">
{getFirstCharacters(invitation.workspace.name)}
</span>
)}
</span>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">
{truncateText(invitation.workspace.name, 30)}
</div>
<p className="text-sm text-custom-text-200">
Invited by{" "}
{invitation.created_by_detail
? invitation.created_by_detail.first_name
: invitation.workspace.owner.first_name}
</p>
</div>
<div className="flex-shrink-0 self-center">
<button
className={`${
invitationsRespond.includes(invitation.id)
? "bg-custom-background-80 text-custom-text-200"
: "bg-custom-primary text-white"
} text-sm px-4 py-2 border border-custom-border-100 rounded-3xl`}
onClick={(e) => {
handleInvitation(
invitation,
invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted"
);
}}
>
{invitationsRespond.includes(invitation.id)
? "Invitation Accepted"
: "Accept Invitation"}
</button>
{/* <input
id={invitation.id}
aria-describedby="workspaces"
name={invitation.id}
value={
invitationsRespond.includes(invitation.id)
? "Invitation Accepted"
: "Accept Invitation"
}
onClick={(e) => {
handleInvitation(
invitation,
invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted"
);
}}
type="button"
className={`${
invitationsRespond.includes(invitation.id)
? "bg-custom-background-80 text-custom-text-200"
: "bg-custom-primary text-white"
} text-sm px-4 py-2 border border-custom-border-100 rounded-3xl`}
// className="h-4 w-4 rounded border-custom-border-100 text-custom-primary focus:ring-custom-primary"
/> */}
</div>
</label>
</div>
))
) : (
<div className="text-center">
<h3 className="text-custom-text-200">{`You don't have any invitations yet.`}</h3>
</div>
)}
</div>
<div className="flex w-full items-center justify-center rounded-b-[10px] pt-10">
<PrimaryButton
type="submit"
className="w-1/2 text-center"
size="md"
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
onClick={submitInvitations}
>
Join Workspace
</PrimaryButton>
</div>
</div>
</Tab.Panel>
<Tab.Panel className="h-full">
<CreateWorkspaceForm
onSubmit={(res) => {
setWorkspace(res);
setStep(3);
}}
defaultValues={defaultValues}
setDefaultValues={setDefaultValues}
user={user}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
/>
</div>
</div>
);
};

View file

@ -158,7 +158,7 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">

View file

@ -85,7 +85,7 @@ export const CustomSearchSelect = ({
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
} ${
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
} items-center justify-between gap-1 rounded-md shadow-sm duration-300 focus:outline-none focus:ring-1 focus:ring-brand-base ${
} items-center justify-between gap-1 rounded-md shadow-sm duration-300 focus:outline-none focus:ring-1 focus:ring-custom-border-100 ${
textAlignment === "right"
? "text-right"
: textAlignment === "center"

View file

@ -1,4 +1,4 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import { mutate } from "swr";
@ -6,27 +6,27 @@ import { mutate } from "swr";
import { Controller, useForm } from "react-hook-form";
// services
import workspaceService from "services/workspace.service";
import userService from "services/user.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { CustomSelect, Input, PrimaryButton } from "components/ui";
import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui";
// types
import { ICurrentUserResponse, IWorkspace } from "types";
// fetch-keys
import { USER_WORKSPACES } from "constants/fetch-keys";
// constants
import { COMPANY_SIZE } from "constants/workspace";
import { ORGANIZATION_SIZE } from "constants/workspace";
type Props = {
onSubmit: (res: IWorkspace) => void;
onSubmit?: (res: IWorkspace) => Promise<void>;
defaultValues: {
name: string;
slug: string;
company_size: number | null;
organization_size: string;
};
setDefaultValues: Dispatch<SetStateAction<any>>;
user: ICurrentUserResponse | undefined;
secondaryButton?: React.ReactNode;
};
const restrictedUrls = [
@ -48,6 +48,7 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
defaultValues,
setDefaultValues,
user,
secondaryButton,
}) => {
const [slugError, setSlugError] = useState(false);
const [invalidSlug, setInvalidSlug] = useState(false);
@ -69,20 +70,30 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
.then(async (res) => {
if (res.status === true && !restrictedUrls.includes(formData.slug)) {
setSlugError(false);
await workspaceService
.createWorkspace(formData, user)
.then((res) => {
.then(async (res) => {
setToastAlert({
type: "success",
title: "Success!",
message: "Workspace created successfully.",
});
mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) => [res, ...(prevData ?? [])]);
updateLastWorkspaceIdUnderUSer(res);
mutate<IWorkspace[]>(
USER_WORKSPACES,
(prevData) => [res, ...(prevData ?? [])],
false
);
if (onSubmit) await onSubmit(res);
})
.catch((err) => {
console.error(err);
});
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Workspace could not be created. Please try again.",
})
);
} else setSlugError(true);
})
.catch(() => {
@ -94,18 +105,6 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
});
};
// update last_workspace_id
const updateLastWorkspaceIdUnderUSer = (workspace: any) => {
userService
.updateUser({ last_workspace_id: workspace.id })
.then((res) => {
onSubmit(workspace);
})
.catch((err) => {
console.log(err);
});
};
useEffect(
() => () => {
// when the component unmounts set the default values to whatever user typed in
@ -115,65 +114,63 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
);
return (
<form className="flex h-full w-full flex-col" onSubmit={handleSubmit(handleCreateWorkspace)}>
<div className="divide-y h-[280px]">
<div className="flex flex-col justify-between gap-3 px-7 pb-3.5">
<div className="flex flex-col items-start justify-center gap-1">
<span className="mb-1.5 text-sm">Workspace name</span>
<form className="space-y-6 sm:space-y-9" onSubmit={handleSubmit(handleCreateWorkspace)}>
<div className="space-y-6 sm:space-y-7">
<div className="space-y-1 text-sm">
<label htmlFor="workspaceName">Workspace Name</label>
<Input
id="workspaceName"
name="name"
register={register}
autoComplete="off"
onChange={(e) =>
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"))
}
validations={{
required: "Workspace name is required",
validate: (value) =>
/^[\w\s-]*$/.test(value) ||
`Name can only contain (" "), ( - ), ( _ ) & alphanumeric characters.`,
}}
placeholder="Enter workspace name..."
error={errors.name}
/>
</div>
<div className="space-y-1 text-sm">
<label htmlFor="workspaceUrl">Workspace URL</label>
<div className="flex w-full items-center rounded-md border border-custom-border-100 px-3">
<span className="whitespace-nowrap text-sm text-custom-text-200">
{window && window.location.host}/
</span>
<Input
name="name"
register={register}
id="workspaceUrl"
mode="trueTransparent"
autoComplete="off"
onChange={(e) =>
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"))
}
name="slug"
register={register}
className="block w-full rounded-md bg-transparent py-2 !px-0 text-sm"
validations={{
required: "Workspace name is required",
validate: (value) =>
/^[\w\s-]*$/.test(value) ||
`Name can only contain (" "), ( - ), ( _ ) & Alphanumeric characters.`,
required: "Workspace URL is required",
}}
placeholder="e.g. My Workspace"
className="placeholder:text-custom-text-200"
error={errors.name}
onChange={(e) =>
/^[a-zA-Z0-9_-]+$/.test(e.target.value)
? setInvalidSlug(false)
: setInvalidSlug(true)
}
/>
</div>
<div className="flex flex-col items-start justify-center gap-1">
<span className="mb-1.5 text-sm">Workspace URL</span>
<div className="flex w-full items-center rounded-md border border-custom-border-100 px-3">
<span className="whitespace-nowrap text-sm text-custom-text-200">
{typeof window !== "undefined" && window.location.origin}/
</span>
<Input
mode="trueTransparent"
autoComplete="off"
name="slug"
register={register}
className="block w-full rounded-md bg-transparent py-2 !px-0 text-sm"
validations={{
required: "Workspace URL is required",
}}
onChange={(e) =>
/^[a-zA-Z0-9_-]+$/.test(e.target.value)
? setInvalidSlug(false)
: setInvalidSlug(true)
}
/>
</div>
{slugError && (
<span className="-mt-3 text-sm text-red-500">Workspace URL is already taken!</span>
)}
{invalidSlug && (
<span className="text-sm text-red-500">{`URL can only contain ( - ), ( _ ) & Alphanumeric characters.`}</span>
)}
</div>
{slugError && (
<span className="-mt-3 text-sm text-red-500">Workspace URL is already taken!</span>
)}
{invalidSlug && (
<span className="text-sm text-red-500">{`URL can only contain ( - ), ( _ ) & alphanumeric characters.`}</span>
)}
</div>
<div className="flex flex-col items-start justify-center gap-1 border-t border-custom-border-100 px-7 pt-3.5 ">
<span className="mb-1.5 text-sm">How large is your company?</span>
<div className="space-y-1 text-sm">
<span>What size is your organization?</span>
<div className="w-full">
<Controller
name="company_size"
name="organization_size"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
@ -181,37 +178,31 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
value={value}
onChange={onChange}
label={
value ? (
value.toString()
) : (
<span className="text-custom-text-200">Select company size</span>
ORGANIZATION_SIZE.find((c) => c === value) ?? (
<span className="text-custom-text-200">Select organization size</span>
)
}
input
width="w-full"
>
{COMPANY_SIZE?.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
{ORGANIZATION_SIZE.map((item) => (
<CustomSelect.Option key={item} value={item}>
{item}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.company_size && (
<span className="text-sm text-red-500">{errors.company_size.message}</span>
{errors.organization_size && (
<span className="text-sm text-red-500">{errors.organization_size.message}</span>
)}
</div>
</div>
</div>
<div className="flex w-full items-center justify-center rounded-b-[10px] pt-10">
<PrimaryButton
type="submit"
className="flex w-1/2 items-center justify-center text-center"
size="md"
disabled={isSubmitting}
>
<div className="flex items-center gap-4">
{secondaryButton}
<PrimaryButton type="submit" size="md" disabled={isSubmitting}>
{isSubmitting ? "Creating..." : "Create Workspace"}
</PrimaryButton>
</div>