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>

View File

@ -15,58 +15,21 @@ export const ROLE = {
20: "Admin",
};
export const COMPANY_SIZE = [
{ value: 5, label: "5" },
{ value: 10, label: "10" },
{ value: 25, label: "25" },
{ value: 50, label: "50" },
];
export const ORGANIZATION_SIZE = ["Just myself", "2-10", "11-50", "51-200", "201-500", "500+"];
export const USER_ROLES = [
{ value: "Founder or leadership team", label: "Founder or leadership team" },
{ value: "Product manager", label: "Product manager" },
{ value: "Designer", label: "Designer" },
{ value: "Software developer", label: "Software developer" },
{ value: "Freelancer", label: "Freelancer" },
{ value: "Product / Project Manager", label: "Product / Project Manager" },
{ value: "Development / Engineering", label: "Development / Engineering" },
{ value: "Founder / Executive", label: "Founder / Executive" },
{ value: "Freelancer / Consultant", label: "Freelancer / Consultant" },
{ value: "Marketing / Growth", label: "Marketing / Growth" },
{ value: "Sales / Business Development", label: "Sales / Business Development" },
{ value: "Support / Operations", label: "Support / Operations" },
{ value: "Student / Professor", label: "Student / Professor" },
{ value: "Human Resources", label: "Human Resources" },
{ value: "Other", label: "Other" },
];
export const ONBOARDING_CARDS = {
welcome: {
imgURL: Welcome,
step: "1/5",
title: "Welcome to Plane",
description: "Plane helps you plan your issues, cycles, and product modules to ship faster.",
},
issue: {
imgURL: Issue,
step: "2/5",
title: "Plan with Issues",
description:
"Issues are the building blocks of Plane. Most concepts in Plane are associated with issues or their properties.",
},
cycle: {
imgURL: Cycle,
step: "3/5",
title: "Move with Cycles",
description:
"Cycles help you and your team progress faster, similar to sprints commonly used in agile development.",
},
module: {
imgURL: Module,
step: "4/5",
title: "Break into Modules ",
description:
"Modules break your big thoughts into Projects or Features, to help you organize better.",
},
commandMenu: {
imgURL: CommandMenu,
step: "5 /5",
title: "Command Menu",
description: "With Command Menu, you can create, update, and navigate across the platform.",
},
};
export const IMPORTERS_EXPORTERS_LIST = [
{
provider: "github",

View File

@ -1,10 +1,10 @@
import useSWR from "swr";
import { useRouter } from "next/router";
// types
import { IWorkspace } from "types";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
// constants
// fetch-keys
import { USER_WORKSPACES } from "constants/fetch-keys";
const useWorkspaces = () => {
@ -12,11 +12,7 @@ const useWorkspaces = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
// API to fetch user information
const {
data = [],
error,
mutate,
} = useSWR<IWorkspace[]>(USER_WORKSPACES, () => workspaceService.userWorkspaces());
const { data, error, mutate } = useSWR(USER_WORKSPACES, () => workspaceService.userWorkspaces());
// active workspace
const activeWorkspace = data?.find((w) => w.slug === workspaceSlug);

View File

@ -9,7 +9,7 @@ type Props = {
};
const DefaultLayout: React.FC<Props> = ({ children }) => (
<div className="h-screen w-full overflow-auto bg-custom-background-90">
<div className="h-screen w-full overflow-hidden bg-custom-background-100">
<>{children}</>
</div>
);

View File

@ -19,6 +19,7 @@ import {
IssuesPieChart,
IssuesStats,
} from "components/workspace";
import { TourRoot } from "components/onboarding";
// ui
import { PrimaryButton, ProductUpdatesModal } from "components/ui";
// images
@ -26,9 +27,10 @@ import emptyDashboard from "public/empty-state/dashboard.svg";
// helpers
import { render12HourFormatTime, renderShortDate } from "helpers/date-time.helper";
// types
import { ICurrentUserResponse } from "types";
import type { NextPage } from "next";
// fetch-keys
import { USER_WORKSPACE_DASHBOARD } from "constants/fetch-keys";
import { CURRENT_USER, USER_WORKSPACE_DASHBOARD } from "constants/fetch-keys";
// constants
import { DAYS } from "constants/project";
@ -65,6 +67,28 @@ const WorkspacePage: NextPage = () => {
setIsOpen={setIsProductUpdatesModalOpen}
/>
)}
{user && !user.is_tour_completed && (
<div className="fixed top-0 left-0 h-full w-full bg-custom-backdrop bg-opacity-50 transition-opacity z-20 grid place-items-center">
<TourRoot
onComplete={() => {
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
is_tour_completed: true,
};
},
false
);
userService.updateUserTourCompleted(user).catch(() => mutate(CURRENT_USER));
}}
/>
</div>
)}
{projects ? (
projects.length > 0 ? (
<div className="p-8">

View File

@ -31,12 +31,12 @@ import type { NextPage } from "next";
// fetch-keys
import { WORKSPACE_DETAILS, USER_WORKSPACES } from "constants/fetch-keys";
// constants
import { COMPANY_SIZE } from "constants/workspace";
import { ORGANIZATION_SIZE } from "constants/workspace";
const defaultValues: Partial<IWorkspace> = {
name: "",
url: "",
company_size: null,
organization_size: "2-10",
logo: null,
};
@ -80,7 +80,7 @@ const WorkspaceSettings: NextPage = () => {
const payload: Partial<IWorkspace> = {
logo: formData.logo,
name: formData.name,
company_size: formData.company_size,
organization_size: formData.organization_size,
};
await workspaceService
@ -281,18 +281,18 @@ const WorkspaceSettings: NextPage = () => {
</div>
<div className="col-span-12 sm:col-span-6">
<Controller
name="company_size"
name="organization_size"
control={control}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select company size"}
label={ORGANIZATION_SIZE.find((c) => c === value) ?? "Select company size"}
input
>
{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>

View File

@ -1,60 +1,100 @@
import React from "react";
import React, { useState } from "react";
import { useRouter } from "next/router";
import Image from "next/image";
import { mutate } from "swr";
// next-themes
import { useTheme } from "next-themes";
// services
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// components
import { OnboardingLogo } from "components/onboarding";
// layouts
import DefaultLayout from "layouts/default-layout";
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
// images
import Logo from "public/onboarding/logo.svg";
// types
import type { NextPage } from "next";
// constants
// components
import { CreateWorkspaceForm } from "components/workspace";
// images
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// types
import { ICurrentUserResponse, IWorkspace } from "types";
import type { NextPage } from "next";
// fetch-keys
import { CURRENT_USER } from "constants/fetch-keys";
const CreateWorkspace: NextPage = () => {
const router = useRouter();
const defaultValues = {
const [defaultValues, setDefaultValues] = useState({
name: "",
slug: "",
company_size: null,
};
organization_size: "",
});
const router = useRouter();
const { theme } = useTheme();
const { user } = useUser();
const onSubmit = async (workspace: IWorkspace) => {
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
last_workspace_id: workspace.id,
workspace: {
...prevData.workspace,
fallback_workspace_id: workspace.id,
fallback_workspace_slug: workspace.slug,
last_workspace_id: workspace.id,
last_workspace_slug: workspace.slug,
},
};
},
false
);
await userService
.updateUser({ last_workspace_id: workspace.id })
.then(() => router.push(`/${workspace.slug}`));
};
return (
<UserAuthorizationLayout>
<DefaultLayout>
<div className="relative grid h-full place-items-center p-5">
<div className="h-full flex flex-col items-center justify-center w-full py-4">
<div className="mb-7 flex items-center justify-center text-center">
<OnboardingLogo className="h-12 w-48 fill-current text-custom-text-100" />
</div>
<div className="flex h-[366px] w-full max-w-xl flex-col justify-between rounded-[10px] bg-custom-background-100 shadow-md">
<div className="flex items-center justify-start gap-3 px-7 pt-7 pb-3.5 text-gray-8 text-sm">
<div className="flex flex-col gap-2 justify-center ">
<h3 className="text-base font-semibold text-custom-text-100">Create Workspace</h3>
<p className="text-sm text-custom-text-200">
Create or join the workspace to get started with Plane.
</p>
</div>
<div className="flex h-full flex-col gap-y-2 sm:gap-y-0 sm:flex-row overflow-hidden">
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
<div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0" />
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 sm:py-5 left-5 sm:left-1/2 md:left-1/3 sm:-translate-x-[15px] top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12">
<div className="h-[30px] w-[133px]">
{theme === "light" ? (
<Image src={BlackHorizontalLogo} alt="Plane black logo" />
) : (
<Image src={WhiteHorizontalLogo} alt="Plane white logo" />
)}
</div>
<CreateWorkspaceForm
defaultValues={defaultValues}
setDefaultValues={() => {}}
onSubmit={(res) => router.push(`/${res.slug}`)}
user={user}
/>
</div>
<div className="absolute sm:fixed text-custom-text-100 text-sm right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
{user?.email}
</div>
</div>
<div className="absolute flex flex-col gap-1 justify-center items-start left-5 top-5">
<span className="text-xs text-custom-text-200">Logged in:</span>
<span className="text-sm text-custom-text-100">{user?.email}</span>
<div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5">
<div className="w-full space-y-7 sm:space-y-10">
<h4 className="text-2xl font-semibold">Create your workspace</h4>
<div className="sm:w-3/4 md:w-2/5">
<CreateWorkspaceForm
onSubmit={onSubmit}
defaultValues={defaultValues}
setDefaultValues={setDefaultValues}
user={user}
/>
</div>
</div>
</div>
</div>
</DefaultLayout>

View File

@ -20,7 +20,7 @@ import {
// ui
import { Spinner } from "components/ui";
// icons
import Logo from "public/logo.png";
import Logo from "public/plane-logos/blue-without-text.png";
// types
type EmailPasswordFormValues = {
email: string;

View File

@ -1,10 +1,12 @@
import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import Image from "next/image";
import useSWR from "swr";
import useSWR, { mutate } from "swr";
// next-themes
import { useTheme } from "next-themes";
// services
import workspaceService from "services/workspace.service";
// hooks
@ -13,36 +15,37 @@ import useToast from "hooks/use-toast";
// layouts
import DefaultLayout from "layouts/default-layout";
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
// components
import SingleInvitation from "components/workspace/single-invitation";
import { OnboardingLogo } from "components/onboarding";
// ui
import { Spinner, EmptySpace, EmptySpaceItem, SecondaryButton, PrimaryButton } from "components/ui";
import { SecondaryButton, PrimaryButton } from "components/ui";
// icons
import { CubeIcon, PlusIcon } from "@heroicons/react/24/outline";
import { CheckCircleIcon } from "@heroicons/react/24/outline";
// images
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import type { NextPage } from "next";
import type { IWorkspaceMemberInvitation } from "types";
// fetch-keys
import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
const OnBoard: NextPage = () => {
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
const { theme } = useTheme();
const { user } = useUser();
const router = useRouter();
const { setToastAlert } = useToast();
const { data: invitations, mutate: mutateInvitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
const { data: workspaces, mutate: mutateWorkspaces } = useSWR("USER_WORKSPACES", () =>
workspaceService.userWorkspaces()
);
const handleInvitation = (
workspace_invitation: IWorkspaceMemberInvitation,
action: "accepted" | "withdraw"
@ -57,118 +60,120 @@ const OnBoard: NextPage = () => {
};
const submitInvitations = () => {
// userService.updateUserOnBoard();
if (invitationsRespond.length === 0) {
setToastAlert({
type: "error",
title: "Error!",
message: "Please select atleast one invitation.",
message: "Please select at least one invitation.",
});
return;
}
setIsJoiningWorkspaces(true);
workspaceService
.joinWorkspaces({ invitations: invitationsRespond })
.then(() => {
mutateInvitations();
mutateWorkspaces();
mutate("USER_WORKSPACES");
setIsJoiningWorkspaces(false);
})
.catch((err) => {
console.log(err);
});
.catch((err) => setIsJoiningWorkspaces(false));
};
return (
<UserAuthorizationLayout>
<DefaultLayout>
<div className="relative grid h-full place-items-center p-5">
<div className="h-full flex flex-col items-center justify-center w-full py-4">
<div className="mb-7 flex items-center justify-center text-center">
<OnboardingLogo className="h-12 w-48 fill-current text-custom-text-100" />
</div>
<div className="flex h-[436px] w-full max-w-xl rounded-[10px] p-7 bg-custom-background-100 shadow-md">
{invitations && workspaces ? (
invitations.length > 0 ? (
<div className="flex w-full flex-col gap-3 justify-between">
<div className="flex flex-col gap-2 justify-center ">
<h3 className="text-base font-semibold text-custom-text-100">
Workspace Invitations
</h3>
<p className="text-sm text-custom-text-200">
Create or join the workspace to get started with Plane.
</p>
</div>
<ul role="list" className="h-[255px] w-full overflow-y-auto">
{invitations.map((invitation) => (
<SingleInvitation
key={invitation.id}
invitation={invitation}
invitationsRespond={invitationsRespond}
handleInvitation={handleInvitation}
/>
))}
</ul>
<div className="flex items-center gap-3">
<Link href="/">
<a className="w-full">
<SecondaryButton className="w-full">Go Home</SecondaryButton>
</a>
</Link>
<PrimaryButton className="w-full" onClick={submitInvitations}>
Accept and Continue
</PrimaryButton>
</div>
</div>
) : workspaces && workspaces.length > 0 ? (
<div className="flex flex-col w-full overflow-auto gap-y-3">
<h2 className="mb-4 text-xl font-medium">Your workspaces</h2>
{workspaces.map((workspace) => (
<Link key={workspace.id} href={workspace.slug}>
<a>
<div className="mb-2 flex items-center justify-between rounded border border-custom-border-100 px-4 py-2">
<div className="flex items-center gap-x-2 text-sm">
<CubeIcon className="h-5 w-5 text-custom-text-200" />
{workspace.name}
</div>
<div className="flex items-center gap-x-2 text-xs text-custom-text-200">
{workspace.owner.first_name}
</div>
</div>
</a>
</Link>
))}
</div>
<div className="flex h-full flex-col gap-y-2 sm:gap-y-0 sm:flex-row overflow-hidden">
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
<div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0" />
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 sm:py-5 left-5 sm:left-1/2 md:left-1/3 sm:-translate-x-[15px] top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12">
<div className="h-[30px] w-[133px]">
{theme === "light" ? (
<Image src={BlackHorizontalLogo} alt="Plane black logo" />
) : (
invitations.length === 0 &&
workspaces.length === 0 && (
<EmptySpace
title="You don't have any workspaces yet"
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
>
<EmptySpaceItem
Icon={PlusIcon}
title={"Create your Workspace"}
action={() => {
router.push("/create-workspace");
}}
/>
</EmptySpace>
)
)
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
<Image src={WhiteHorizontalLogo} alt="Plane white logo" />
)}
</div>
</div>
<div className="absolute sm:fixed text-custom-text-100 text-sm right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
{user?.email}
</div>
</div>
<div className="absolute flex flex-col gap-1 justify-center items-start left-5 top-5">
<span className="text-xs text-custom-text-200">Logged in:</span>
<span className="text-sm text-custom-text-100">{user?.email}</span>
<div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5">
<div className="w-full space-y-10">
<h5 className="text-lg">We see that someone has invited you to</h5>
<h4 className="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>
<Link href="/">
<a>
<SecondaryButton size="md" outline>
Go Home
</SecondaryButton>
</a>
</Link>
</div>
</div>
</div>
</div>
</DefaultLayout>

View File

@ -1,134 +1,198 @@
import { useEffect, useState } from "react";
// next imports
import Router from "next/router";
import Image from "next/image";
import useSWR, { mutate } from "swr";
// next-themes
import { useTheme } from "next-themes";
// services
import userService from "services/user.service";
import workspaceService from "services/workspace.service";
// hooks
import useUserAuth from "hooks/use-user-auth";
import useWorkspaces from "hooks/use-workspaces";
// layouts
import DefaultLayout from "layouts/default-layout";
// components
import {
InviteMembers,
OnboardingCard,
OnboardingLogo,
UserDetails,
Workspace,
} from "components/onboarding";
import { InviteMembers, JoinWorkspaces, UserDetails, Workspace } from "components/onboarding";
// ui
import { PrimaryButton, Spinner } from "components/ui";
// constant
import { ONBOARDING_CARDS } from "constants/workspace";
import { Spinner } from "components/ui";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// types
import { ICurrentUserResponse, IUser, OnboardingSteps } from "types";
import type { NextPage } from "next";
// fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
const Onboarding: NextPage = () => {
const [step, setStep] = useState<null | number>(null);
const [userRole, setUserRole] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState<number | null>(null);
const [workspace, setWorkspace] = useState();
const { theme } = useTheme();
const { user, isLoading: userLoading, mutateUser } = useUserAuth("onboarding");
const { user, isLoading: userLoading } = useUserAuth("onboarding");
const { workspaces } = useWorkspaces();
const userWorkspaces = workspaces?.filter((w) => w.created_by === user?.id);
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
const updateLastWorkspace = async () => {
if (!userWorkspaces) return;
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
last_workspace_id: userWorkspaces[0]?.id,
workspace: {
...prevData.workspace,
fallback_workspace_id: userWorkspaces[0]?.id,
fallback_workspace_slug: userWorkspaces[0]?.slug,
last_workspace_id: userWorkspaces[0]?.id,
last_workspace_slug: userWorkspaces[0]?.slug,
},
};
},
false
);
await userService.updateUser({ last_workspace_id: userWorkspaces?.[0]?.id });
};
const stepChange = async (steps: Partial<OnboardingSteps>) => {
if (!user) return;
const payload: Partial<IUser> = {
onboarding_step: {
...user.onboarding_step,
...steps,
},
};
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
...payload,
};
},
false
);
await userService.updateUser(payload);
};
useEffect(() => {
if (user && step === null) {
let currentStep: number = 1;
if (user?.role) currentStep = 2;
if (user?.last_workspace_id) currentStep = 4;
setStep(() => currentStep);
}
}, [step, user]);
const handleStepChange = async () => {
if (!user || !userWorkspaces || !invitations) return;
const onboardingStep = user.onboarding_step;
if (!onboardingStep.profile_complete && step !== 1) setStep(1);
if (onboardingStep.profile_complete && !onboardingStep.workspace_create && step !== 2)
setStep(2);
if (
onboardingStep.profile_complete &&
onboardingStep.workspace_create &&
!onboardingStep.workspace_invite &&
step !== 3
)
setStep(3);
if (
onboardingStep.profile_complete &&
onboardingStep.workspace_create &&
onboardingStep.workspace_invite &&
!onboardingStep.workspace_join &&
step !== 4
) {
if (invitations.length > 0) setStep(4);
else await Router.push("/");
}
};
handleStepChange();
}, [user, invitations, userWorkspaces, step]);
if (userLoading || step === null)
return (
<div className="grid h-screen place-items-center">
<Spinner />
</div>
);
return (
<DefaultLayout>
{userLoading || isLoading || step === null ? (
<div className="grid h-screen place-items-center">
<Spinner />
</div>
) : (
<div className="relative grid h-full place-items-center p-5">
{step <= 3 ? (
<div className="h-full flex flex-col justify-center w-full py-4">
<div className="mb-7 flex items-center justify-center text-center">
<OnboardingLogo className="h-12 w-48 fill-current text-custom-text-100" />
<div className="flex h-full w-full flex-col gap-y-2 sm:gap-y-0 sm:flex-row overflow-hidden">
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
<div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0 z-10" />
{step === 1 ? (
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 py-5 left-2 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12 z-10">
<div className="h-[30px] w-[30px]">
<Image src={BluePlaneLogoWithoutText} alt="Plane logo" />
</div>
{step === 1 ? (
<UserDetails user={user} setStep={setStep} setUserRole={setUserRole} />
) : step === 2 ? (
<Workspace setStep={setStep} setWorkspace={setWorkspace} user={user} />
) : (
step === 3 && <InviteMembers setStep={setStep} workspace={workspace} user={user} />
)}
</div>
) : (
<div className="flex w-full max-w-2xl flex-col gap-12">
<div className="flex flex-col items-center justify-center gap-7 rounded-[10px] bg-custom-background-100 pb-7 text-center shadow-md">
{step === 4 ? (
<OnboardingCard data={ONBOARDING_CARDS.welcome} />
) : step === 5 ? (
<OnboardingCard data={ONBOARDING_CARDS.issue} gradient />
) : step === 6 ? (
<OnboardingCard data={ONBOARDING_CARDS.cycle} gradient />
) : step === 7 ? (
<OnboardingCard data={ONBOARDING_CARDS.module} gradient />
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 sm:py-5 left-5 sm:left-1/2 md:left-1/3 sm:-translate-x-[15px] top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12 z-10">
<div className="h-[30px] w-[133px]">
{theme === "light" ? (
<Image src={BlackHorizontalLogo} alt="Plane black logo" />
) : (
step === 8 && <OnboardingCard data={ONBOARDING_CARDS.commandMenu} />
<Image src={WhiteHorizontalLogo} alt="Plane white logo" />
)}
<div className="mx-auto flex h-1/4 items-end lg:w-1/2">
<PrimaryButton
type="button"
className="flex w-full items-center justify-center text-center "
size="md"
onClick={() => {
if (step === 8) {
setIsLoading(true);
userService
.updateUserOnBoard({ userRole }, user)
.then(async () => {
mutateUser();
const userWorkspaces = await workspaceService.userWorkspaces();
const lastActiveWorkspace =
userWorkspaces.find(
(workspace) => workspace.id === user?.last_workspace_id
) ?? userWorkspaces[0];
if (lastActiveWorkspace) {
mutateUser();
Router.push(`/${lastActiveWorkspace.slug}`);
return;
} else {
const invitations = await workspaceService.userWorkspaceInvitations();
if (invitations.length > 0) {
Router.push(`/invitations`);
return;
} else {
Router.push(`/create-workspace`);
return;
}
}
})
.catch((err) => {
setIsLoading(false);
console.log(err);
});
} else setStep((prevData) => (prevData != null ? prevData + 1 : prevData));
}}
>
{step === 4 || step === 8 ? "Get Started" : "Next"}
</PrimaryButton>
</div>
</div>
</div>
)}
<div className="absolute flex flex-col gap-1 justify-center items-start left-5 top-5">
<span className="text-xs text-custom-text-200">Logged in:</span>
<span className="text-sm text-custom-text-100">{user?.email}</span>
<div className="absolute sm:fixed text-custom-text-100 text-sm right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
{user?.email}
</div>
</div>
)}
<div className="relative flex justify-center sm:items-center h-full px-8 pb-0 sm:px-0 sm:py-12 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5 overflow-hidden">
{step === 1 ? (
<UserDetails user={user} />
) : step === 2 ? (
<Workspace
user={user}
updateLastWorkspace={updateLastWorkspace}
stepChange={stepChange}
/>
) : step === 3 ? (
<InviteMembers workspace={userWorkspaces?.[0]} user={user} stepChange={stepChange} />
) : (
step === 4 && <JoinWorkspaces stepChange={stepChange} />
)}
</div>
{step !== 4 && (
<div className="sticky sm:fixed bottom-0 md:bottom-14 md:right-16 py-6 md:py-0 flex justify-center md:justify-end bg-custom-background-100 md:bg-transparent pointer-events-none w-full z-[1]">
<div className="w-3/4 md:w-1/5 space-y-1">
<p className="text-xs text-custom-text-200">{step} of 3 steps</p>
<div className="relative h-1 w-full rounded bg-custom-background-80">
<div
className="absolute top-0 left-0 h-1 rounded bg-custom-primary-100 duration-300"
style={{
width: `${((step / 3) * 100).toFixed(0)}%`,
}}
/>
</div>
</div>
</div>
)}
</div>
</DefaultLayout>
);
};

View File

@ -14,7 +14,7 @@ import DefaultLayout from "layouts/default-layout";
// ui
import { Input, SecondaryButton } from "components/ui";
// icons
import Logo from "public/logo.png";
import Logo from "public/plane-logos/blue-without-text.png";
// types
import type { NextPage } from "next";

View File

@ -13,7 +13,7 @@ import DefaultLayout from "layouts/default-layout";
// components
import { EmailPasswordForm } from "components/account";
// images
import Logo from "public/logo.png";
import Logo from "public/plane-logos/blue-without-text.png";
// types
import type { NextPage } from "next";
type EmailPasswordFormValues = {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 66 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.4 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -1,6 +0,0 @@
<svg width="201" height="51" viewBox="0 0 201 51" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M68.7311 32.3701V15.9189H74.9003C76.1641 15.9189 77.2244 16.1546 78.0813 16.6258C78.9434 17.0971 79.5941 17.7451 80.0332 18.5698C80.4777 19.3891 80.6999 20.3209 80.6999 21.3652C80.6999 22.4202 80.4777 23.3573 80.0332 24.1767C79.5887 24.996 78.9327 25.6413 78.0652 26.1126C77.1976 26.5785 76.1293 26.8114 74.8601 26.8114H70.7714V24.3614H74.4585C75.1975 24.3614 75.8026 24.2329 76.2739 23.9758C76.7451 23.7188 77.0932 23.3653 77.3181 22.9155C77.5484 22.4657 77.6635 21.9489 77.6635 21.3652C77.6635 20.7815 77.5484 20.2674 77.3181 19.8229C77.0932 19.3784 76.7425 19.033 76.2658 18.7866C75.7946 18.535 75.1868 18.4091 74.4424 18.4091H71.7112V32.3701H68.7311ZM97.9918 32.3701V15.9189H100.972V29.8719H108.218V32.3701H97.9918ZM128.238 32.3701H125.057L130.849 15.9189H134.528L140.328 32.3701H137.147L132.753 19.2927H132.624L128.238 32.3701ZM128.343 25.9198H137.018V28.3136H128.343V25.9198ZM170.717 15.9189V32.3701H168.066L160.315 21.1644H160.178V32.3701H157.198V15.9189H159.865L167.608 27.1327H167.753V15.9189H170.717ZM188.668 32.3701V15.9189H199.367V18.4171H191.648V22.8834H198.813V25.3816H191.648V29.8719H199.432V32.3701H188.668Z" fill="#212529"/>
<path d="M40.2549 10.6406H20.5122V20.5124H30.3832V30.3834H40.2549V10.6406Z" fill="#3F76FF"/>
<path d="M20.5121 20.5127H10.6411V30.3616H20.5121V20.5127Z" fill="#3F76FF"/>
<path d="M30.3831 30.3838H20.5342V40.2548H30.3831V30.3838Z" fill="#3F76FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.4 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 353 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 254 KiB

View File

@ -1,5 +0,0 @@
<svg width="450" height="180" viewBox="0 0 450 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M293.949 21.0508H202.016V67.0193H247.981V112.984H293.949V21.0508Z" fill="#3F76FF"/>
<path d="M202.016 67.0195H156.051V112.881H202.016V67.0195Z" fill="#3F76FF"/>
<path d="M247.98 112.984H202.118V158.949H247.98V112.984Z" fill="#3F76FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 352 B

View File

@ -0,0 +1,17 @@
<svg width="133" height="30" viewBox="0 0 133 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_372_3489)">
<path d="M35.7598 0.00235398H44.7151C46.4036 -0.0301787 48.0819 0.274777 49.6538 0.899752C51.0897 1.48694 52.3232 2.48876 53.2005 3.7804C54.1414 5.27171 54.6058 7.02009 54.5305 8.78788C54.6025 10.5948 54.1394 12.3823 53.2005 13.921C52.338 15.271 51.1056 16.3375 49.6538 16.9901C48.0968 17.6881 46.409 18.0371 44.7062 18.0131H40.3882V30.0024H35.7598V0.00235398ZM40.3882 14.1812H43.4472C44.5022 14.1924 45.5515 14.0225 46.5505 13.6787C47.4425 13.3684 48.2171 12.7851 48.7672 12.0095C49.3447 11.1092 49.6299 10.0487 49.5829 8.97633C49.6547 7.88162 49.368 6.79348 48.7672 5.88031C48.2009 5.1354 47.4202 4.58669 46.5328 4.30986C45.5354 3.98896 44.4937 3.83143 43.4472 3.84322H40.3882V14.1812Z" fill="#262626"/>
<path d="M63.6538 30.0023H58.9102V0.00234985H63.6538V30.0023Z" fill="#262626"/>
<path d="M83.1961 30.0023V26.0717C82.9541 26.623 82.6146 27.1249 82.1941 27.5524C81.5462 28.2456 80.7841 28.8194 79.942 29.2485C78.8993 29.7731 77.7457 30.0319 76.5816 30.0023C74.9896 30.0315 73.4209 29.6134 72.0497 28.7943C70.6785 27.9753 69.5585 26.7874 68.8144 25.3628C68.0116 23.8079 67.6091 22.0734 67.644 20.3194C67.6069 18.5595 68.0094 16.8186 68.8144 15.2581C69.5577 13.8315 70.677 12.6411 72.048 11.819C73.419 10.9969 74.9882 10.5752 76.5816 10.6006C77.7134 10.57 78.8375 10.7969 79.8711 11.2647C80.698 11.6453 81.4539 12.1675 82.1055 12.8082C82.5675 13.2374 82.9256 13.7687 83.1518 14.3607V11.139H87.8333V29.9844L83.1961 30.0023ZM72.299 20.3194C72.2745 21.3974 72.5412 22.4617 73.0704 23.3975C73.548 24.2312 74.2333 24.9235 75.0579 25.4053C75.8825 25.8872 76.8175 26.1417 77.7697 26.1435C78.7342 26.1735 79.6872 25.9254 80.5177 25.4281C81.3482 24.9308 82.0218 24.2047 82.4602 23.3347C82.9374 22.3947 83.1751 21.349 83.1518 20.2925C83.1763 19.233 82.9385 18.1841 82.4602 17.2413C81.9964 16.419 81.3267 15.7349 80.5183 15.2581C79.6888 14.7549 78.7367 14.4969 77.7697 14.5133C76.8353 14.5149 75.9179 14.7656 75.1097 15.2401C74.2725 15.73 73.5757 16.4321 73.0881 17.2772C72.5606 18.2015 72.288 19.2521 72.299 20.3194Z" fill="#262626"/>
<path d="M103.536 10.6096C104.757 10.611 105.964 10.8677 107.082 11.3634C108.238 11.8795 109.223 12.7205 109.92 13.7864C110.714 15.0781 111.104 16.5829 111.037 18.1029V30.0023H106.293V18.9733C106.348 18.3184 106.273 17.6591 106.074 17.0337C105.874 16.4082 105.554 15.8291 105.132 15.3299C104.739 14.9381 104.272 14.6317 103.758 14.4296C103.244 14.2274 102.695 14.1337 102.144 14.1543C101.323 14.1599 100.521 14.3994 99.8295 14.8453C99.0977 15.3167 98.4893 15.9602 98.0562 16.7209C97.5907 17.539 97.3518 18.469 97.3646 19.4131V30.0023H92.6387V11.157H97.3646V14.244C97.5688 13.6234 97.9368 13.0711 98.4286 12.6467C99.081 12.0457 99.8309 11.563 100.645 11.2198C101.557 10.8175 102.541 10.6097 103.536 10.6096Z" fill="#262626"/>
<path d="M118.973 21.4322C118.987 22.346 119.221 23.2426 119.656 24.0436C120.095 24.8189 120.753 25.4438 121.545 25.8384C122.494 26.3044 123.541 26.5293 124.595 26.4935C125.5 26.5093 126.4 26.3635 127.255 26.0628C127.932 25.8094 128.569 25.4556 129.143 25.0128C129.56 24.6821 129.93 24.2957 130.243 23.8642L132.335 26.7448C131.854 27.3909 131.273 27.9545 130.615 28.414C129.834 28.9511 128.963 29.3402 128.044 29.5626C126.808 29.8643 125.538 30.0031 124.267 29.9754C122.476 30.0226 120.705 29.599 119.124 28.746C117.69 27.949 116.513 26.7492 115.737 25.291C114.909 23.68 114.498 21.8834 114.54 20.0682C114.532 18.3948 114.924 16.7444 115.684 15.2581C116.41 13.8364 117.524 12.6558 118.894 11.857C120.412 11.0607 122.093 10.6338 123.803 10.6105C125.513 10.5871 127.205 10.9678 128.744 11.7224C130.081 12.4518 131.174 13.5669 131.883 14.9261C132.667 16.483 133.051 18.2141 133 19.9605C133 20.0771 133 20.3284 133 20.6963C133.005 20.9431 132.984 21.1897 132.938 21.4322H118.973ZM128.567 17.9503C128.53 17.3776 128.37 16.8201 128.097 16.317C127.762 15.6679 127.264 15.12 126.652 14.7287C125.854 14.2334 124.926 13.9955 123.992 14.0466C123.03 14.0014 122.076 14.2282 121.234 14.7017C120.661 15.0464 120.172 15.5175 119.804 16.08C119.435 16.6424 119.197 17.2817 119.106 17.9503H128.567Z" fill="#262626"/>
<path d="M29.127 0.271576H9.70898V10.0981H19.418V19.9246H29.127V0.271576Z" fill="#3F76FF"/>
<path d="M9.709 10.0981H0V19.9066H9.709V10.0981Z" fill="#3F76FF"/>
<path d="M19.4266 19.9246H9.73535V29.7511H19.4266V19.9246Z" fill="#3F76FF"/>
</g>
<defs>
<clipPath id="clip0_372_3489">
<rect width="133" height="30" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,17 @@
<svg width="133" height="30" viewBox="0 0 133 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_372_3489)">
<path d="M35.7598 0.00235398H44.7151C46.4036 -0.0301787 48.0819 0.274777 49.6538 0.899752C51.0897 1.48694 52.3232 2.48876 53.2005 3.7804C54.1414 5.27171 54.6058 7.02009 54.5305 8.78788C54.6025 10.5948 54.1394 12.3823 53.2005 13.921C52.338 15.271 51.1056 16.3375 49.6538 16.9901C48.0968 17.6881 46.409 18.0371 44.7062 18.0131H40.3882V30.0024H35.7598V0.00235398ZM40.3882 14.1812H43.4472C44.5022 14.1924 45.5515 14.0225 46.5505 13.6787C47.4425 13.3684 48.2171 12.7851 48.7672 12.0095C49.3447 11.1092 49.6299 10.0487 49.5829 8.97633C49.6547 7.88162 49.368 6.79348 48.7672 5.88031C48.2009 5.1354 47.4202 4.58669 46.5328 4.30986C45.5354 3.98896 44.4937 3.83143 43.4472 3.84322H40.3882V14.1812Z" fill="#e9f4fc"/>
<path d="M63.6538 30.0023H58.9102V0.00234985H63.6538V30.0023Z" fill="#e9f4fc"/>
<path d="M83.1961 30.0023V26.0717C82.9541 26.623 82.6146 27.1249 82.1941 27.5524C81.5462 28.2456 80.7841 28.8194 79.942 29.2485C78.8993 29.7731 77.7457 30.0319 76.5816 30.0023C74.9896 30.0315 73.4209 29.6134 72.0497 28.7943C70.6785 27.9753 69.5585 26.7874 68.8144 25.3628C68.0116 23.8079 67.6091 22.0734 67.644 20.3194C67.6069 18.5595 68.0094 16.8186 68.8144 15.2581C69.5577 13.8315 70.677 12.6411 72.048 11.819C73.419 10.9969 74.9882 10.5752 76.5816 10.6006C77.7134 10.57 78.8375 10.7969 79.8711 11.2647C80.698 11.6453 81.4539 12.1675 82.1055 12.8082C82.5675 13.2374 82.9256 13.7687 83.1518 14.3607V11.139H87.8333V29.9844L83.1961 30.0023ZM72.299 20.3194C72.2745 21.3974 72.5412 22.4617 73.0704 23.3975C73.548 24.2312 74.2333 24.9235 75.0579 25.4053C75.8825 25.8872 76.8175 26.1417 77.7697 26.1435C78.7342 26.1735 79.6872 25.9254 80.5177 25.4281C81.3482 24.9308 82.0218 24.2047 82.4602 23.3347C82.9374 22.3947 83.1751 21.349 83.1518 20.2925C83.1763 19.233 82.9385 18.1841 82.4602 17.2413C81.9964 16.419 81.3267 15.7349 80.5183 15.2581C79.6888 14.7549 78.7367 14.4969 77.7697 14.5133C76.8353 14.5149 75.9179 14.7656 75.1097 15.2401C74.2725 15.73 73.5757 16.4321 73.0881 17.2772C72.5606 18.2015 72.288 19.2521 72.299 20.3194Z" fill="#e9f4fc"/>
<path d="M103.536 10.6096C104.757 10.611 105.964 10.8677 107.082 11.3634C108.238 11.8795 109.223 12.7205 109.92 13.7864C110.714 15.0781 111.104 16.5829 111.037 18.1029V30.0023H106.293V18.9733C106.348 18.3184 106.273 17.6591 106.074 17.0337C105.874 16.4082 105.554 15.8291 105.132 15.3299C104.739 14.9381 104.272 14.6317 103.758 14.4296C103.244 14.2274 102.695 14.1337 102.144 14.1543C101.323 14.1599 100.521 14.3994 99.8295 14.8453C99.0977 15.3167 98.4893 15.9602 98.0562 16.7209C97.5907 17.539 97.3518 18.469 97.3646 19.4131V30.0023H92.6387V11.157H97.3646V14.244C97.5688 13.6234 97.9368 13.0711 98.4286 12.6467C99.081 12.0457 99.8309 11.563 100.645 11.2198C101.557 10.8175 102.541 10.6097 103.536 10.6096Z" fill="#e9f4fc"/>
<path d="M118.973 21.4322C118.987 22.346 119.221 23.2426 119.656 24.0436C120.095 24.8189 120.753 25.4438 121.545 25.8384C122.494 26.3044 123.541 26.5293 124.595 26.4935C125.5 26.5093 126.4 26.3635 127.255 26.0628C127.932 25.8094 128.569 25.4556 129.143 25.0128C129.56 24.6821 129.93 24.2957 130.243 23.8642L132.335 26.7448C131.854 27.3909 131.273 27.9545 130.615 28.414C129.834 28.9511 128.963 29.3402 128.044 29.5626C126.808 29.8643 125.538 30.0031 124.267 29.9754C122.476 30.0226 120.705 29.599 119.124 28.746C117.69 27.949 116.513 26.7492 115.737 25.291C114.909 23.68 114.498 21.8834 114.54 20.0682C114.532 18.3948 114.924 16.7444 115.684 15.2581C116.41 13.8364 117.524 12.6558 118.894 11.857C120.412 11.0607 122.093 10.6338 123.803 10.6105C125.513 10.5871 127.205 10.9678 128.744 11.7224C130.081 12.4518 131.174 13.5669 131.883 14.9261C132.667 16.483 133.051 18.2141 133 19.9605C133 20.0771 133 20.3284 133 20.6963C133.005 20.9431 132.984 21.1897 132.938 21.4322H118.973ZM128.567 17.9503C128.53 17.3776 128.37 16.8201 128.097 16.317C127.762 15.6679 127.264 15.12 126.652 14.7287C125.854 14.2334 124.926 13.9955 123.992 14.0466C123.03 14.0014 122.076 14.2282 121.234 14.7017C120.661 15.0464 120.172 15.5175 119.804 16.08C119.435 16.6424 119.197 17.2817 119.106 17.9503H128.567Z" fill="#e9f4fc"/>
<path d="M29.127 0.271576H9.70898V10.0981H19.418V19.9246H29.127V0.271576Z" fill="#3F76FF"/>
<path d="M9.709 10.0981H0V19.9066H9.709V10.0981Z" fill="#3F76FF"/>
<path d="M19.4266 19.9246H9.73535V29.7511H19.4266V19.9246Z" fill="#3F76FF"/>
</g>
<defs>
<clipPath id="clip0_372_3489">
<rect width="133" height="30" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,17 @@
<svg width="178" height="40" viewBox="0 0 178 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_435_3747)">
<path d="M47.8574 -4.66756e-06H59.8428C62.1026 -0.0433815 64.3487 0.363226 66.4525 1.19653C68.3742 1.97944 70.025 3.3152 71.1992 5.03739C72.4584 7.02581 73.0799 9.35697 72.9792 11.714C73.0756 14.1233 72.4557 16.5067 71.1992 18.5582C70.0448 20.3582 68.3954 21.7802 66.4525 22.6503C64.3686 23.581 62.1098 24.0463 59.8309 24.0144H54.0518V40H47.8574V-4.66756e-06ZM54.0518 18.9052H58.1458C59.5579 18.9201 60.9622 18.6935 62.2992 18.2351C63.493 17.8214 64.5297 17.0436 65.2658 16.0096C66.0388 14.8091 66.4205 13.3952 66.3576 11.9653C66.4536 10.5057 66.0699 9.05483 65.2658 7.83727C64.508 6.84406 63.4631 6.11244 62.2754 5.74334C60.9405 5.31547 59.5464 5.10542 58.1458 5.12114H54.0518V18.9052Z" fill="#F2F2F2"/>
<path d="M85.1905 40H78.8418V0H85.1905V40Z" fill="#F2F2F2"/>
<path d="M111.345 40V34.7592C111.021 35.4943 110.566 36.1634 110.004 36.7335C109.136 37.6576 108.117 38.4228 106.99 38.9949C105.594 39.6944 104.05 40.0394 102.492 40C100.362 40.0389 98.262 39.4814 96.4269 38.3893C94.5917 37.2972 93.0929 35.7134 92.0969 33.8139C91.0225 31.7408 90.4838 29.4281 90.5305 27.0894C90.4808 24.7429 91.0196 22.4217 92.0969 20.341C93.0918 18.4388 94.5897 16.8517 96.4246 15.7555C98.2595 14.6594 100.36 14.0971 102.492 14.131C104.007 14.0902 105.511 14.3928 106.895 15.0165C108.001 15.524 109.013 16.2202 109.885 17.0745C110.503 17.6468 110.983 18.3551 111.285 19.1445V14.8489H117.551V39.9761L111.345 40ZM96.7605 27.0894C96.7277 28.5267 97.0847 29.9458 97.7929 31.1935C98.4321 32.3051 99.3493 33.2282 100.453 33.8706C101.556 34.5131 102.808 34.8525 104.082 34.8549C105.373 34.8949 106.649 34.5641 107.76 33.901C108.871 33.2379 109.773 32.2699 110.36 31.1098C110.998 29.8565 111.317 28.4623 111.285 27.0536C111.318 25.6409 111 24.2424 110.36 22.9853C109.739 21.8888 108.843 20.9767 107.761 20.341C106.651 19.6701 105.376 19.3261 104.082 19.3479C102.832 19.3501 101.604 19.6844 100.522 20.3171C99.4017 20.9702 98.4693 21.9063 97.8166 23.0332C97.1106 24.2655 96.7459 25.6664 96.7605 27.0894Z" fill="#F2F2F2"/>
<path d="M138.567 14.143C140.201 14.1449 141.816 14.4871 143.313 15.1481C144.86 15.8362 146.178 16.9575 147.111 18.3787C148.174 20.101 148.695 22.1074 148.606 24.134V40H142.257V25.2946C142.33 24.4215 142.23 23.5424 141.963 22.7085C141.696 21.8745 141.268 21.1023 140.703 20.4367C140.177 19.9144 139.551 19.5058 138.864 19.2363C138.176 18.9667 137.441 18.8418 136.703 18.8693C135.606 18.8767 134.532 19.196 133.606 19.7906C132.627 20.4192 131.813 21.2771 131.233 22.2914C130.61 23.3822 130.29 24.6223 130.307 25.8809V40H123.982V14.8729H130.307V18.9889C130.581 18.1614 131.073 17.425 131.731 16.8591C132.605 16.0578 133.608 15.4143 134.698 14.9566C135.918 14.4202 137.235 14.1432 138.567 14.143Z" fill="#F2F2F2"/>
<path d="M159.226 28.5731C159.245 29.7916 159.558 30.987 160.14 32.055C160.728 33.0887 161.608 33.922 162.668 34.4481C163.938 35.0694 165.339 35.3692 166.75 35.3216C167.961 35.3426 169.166 35.1482 170.31 34.7472C171.217 34.4094 172.068 33.9376 172.837 33.3473C173.394 32.9063 173.889 32.3911 174.309 31.8157L177.109 35.6566C176.465 36.5181 175.688 37.2695 174.807 37.8821C173.761 38.5983 172.596 39.1172 171.366 39.4137C169.711 39.8159 168.012 40.0009 166.311 39.9641C163.915 40.027 161.543 39.4622 159.428 38.3249C157.508 37.2622 155.934 35.6624 154.895 33.7182C153.787 31.5703 153.236 29.1747 153.293 26.7544C153.282 24.5233 153.807 22.3227 154.824 20.341C155.795 18.4454 157.286 16.8713 159.12 15.8062C161.152 14.7444 163.402 14.1753 165.69 14.1442C167.979 14.113 170.243 14.6206 172.303 15.6267C174.093 16.5992 175.555 18.086 176.504 19.8983C177.552 21.9742 178.067 24.2823 177.999 26.6108C177.999 26.7664 177.999 27.1014 177.999 27.592C178.005 27.921 177.977 28.2498 177.916 28.5731H159.226ZM172.066 23.9306C172.017 23.167 171.802 22.4237 171.437 21.7529C170.989 20.8874 170.322 20.1568 169.503 19.6351C168.435 18.9747 167.194 18.6575 165.943 18.7257C164.656 18.6653 163.378 18.9678 162.252 19.5992C161.485 20.0587 160.831 20.6868 160.338 21.4368C159.844 22.1867 159.526 23.0391 159.404 23.9306H172.066Z" fill="#F2F2F2"/>
<path d="M38.9821 0.358963H12.9941V13.461H25.9881V26.563H38.9821V0.358963Z" fill="#F2F2F2"/>
<path d="M12.994 13.461H0V26.539H12.994V13.461Z" fill="#F2F2F2"/>
<path d="M25.9996 26.563H13.0293V39.665H25.9996V26.563Z" fill="#F2F2F2"/>
</g>
<defs>
<clipPath id="clip0_435_3747">
<rect width="178" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,57 +0,0 @@
<svg width="748" height="813" viewBox="0 0 748 813" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_491_6452)">
<path d="M150.685 667.271C161.118 667.271 169.575 658.815 169.575 648.383C169.575 637.951 161.118 629.494 150.685 629.494C140.253 629.494 131.796 637.951 131.796 648.383C131.796 658.815 140.253 667.271 150.685 667.271Z" fill="#F0F0F0"/>
<path d="M115.725 670.654C123.044 670.654 128.976 664.722 128.976 657.404C128.976 650.086 123.044 644.154 115.725 644.154C108.407 644.154 102.475 650.086 102.475 657.404C102.475 664.722 108.407 670.654 115.725 670.654Z" fill="#F0F0F0"/>
<path d="M142.954 78.4899C151.345 78.4899 158.147 71.6884 158.147 63.2983C158.147 54.9082 151.345 48.1067 142.954 48.1067C134.564 48.1067 127.762 54.9082 127.762 63.2983C127.762 71.6884 134.564 78.4899 142.954 78.4899Z" fill="#CCCCCC"/>
<path d="M689.884 255.726C698.274 255.726 705.076 248.924 705.076 240.534C705.076 232.144 698.274 225.342 689.884 225.342C681.493 225.342 674.691 232.144 674.691 240.534C674.691 248.924 681.493 255.726 689.884 255.726Z" fill="#CCCCCC"/>
<path d="M167.009 350.673H141.688V375.992H167.009V350.673Z" fill="#CCCCCC"/>
<path d="M11.287 164.576H8.75488V0H170.808V2.53194H11.287V164.576Z" fill="#CCCCCC"/>
<path d="M748 164.576H745.468V2.53194H585.947V0H748V164.576Z" fill="#CCCCCC"/>
<path d="M11.287 303.832H8.75488V468.408H170.808V465.876H11.287V303.832Z" fill="#CCCCCC"/>
<path d="M748 303.832H745.468V465.876H585.947V468.408H748V303.832Z" fill="#CCCCCC"/>
<path d="M231.578 77.2241H54.332V79.7561H231.578V77.2241Z" fill="#CCCCCC"/>
<path d="M104.973 54.4365H102.441V77.8569H104.973V54.4365Z" fill="#CCCCCC"/>
<path d="M182.202 54.4365H179.67V77.8569H182.202V54.4365Z" fill="#CCCCCC"/>
<path d="M710.14 255.726H532.895V258.258H710.14V255.726Z" fill="#CCCCCC"/>
<path d="M583.536 232.938H581.004V256.358H583.536V232.938Z" fill="#CCCCCC"/>
<path d="M660.765 232.938H658.232V256.358H660.765V232.938Z" fill="#CCCCCC"/>
<path d="M242.972 374.726H65.7266V377.258H242.972V374.726Z" fill="#CCCCCC"/>
<path d="M116.368 351.939H113.836V375.359H116.368V351.939Z" fill="#CCCCCC"/>
<path d="M193.597 351.939H191.064V375.359H193.597V351.939Z" fill="#CCCCCC"/>
<path d="M375.769 529.726C414.925 529.726 446.668 522.358 446.668 513.269C446.668 504.18 414.925 496.811 375.769 496.811C336.613 496.811 304.871 504.18 304.871 513.269C304.871 522.358 336.613 529.726 375.769 529.726Z" fill="#3F3D56"/>
<path d="M432.648 677.22L364.281 682.275L364.468 684.799L432.835 679.744L432.648 677.22Z" fill="#CACACA"/>
<path d="M319.407 668.507L318.188 670.725L343.508 684.651L344.728 682.432L319.407 668.507Z" fill="#CACACA"/>
<path d="M390.962 659.488H368.173V662.02H390.962V659.488Z" fill="#CACACA"/>
<path d="M308.667 817.82L306.14 817.648L325.13 527.741L327.657 527.914L308.667 817.82Z" fill="#CACACA"/>
<path d="M402.358 817.796L389.697 550.677L392.227 550.553L404.887 817.672L402.358 817.796Z" fill="#CACACA"/>
<path d="M427.673 526.457L425.149 526.666L449.204 817.838L451.728 817.63L427.673 526.457Z" fill="#CACACA"/>
<path d="M347.917 817.764L345.385 817.704L352.981 550.584L355.513 550.645L347.917 817.764Z" fill="#CACACA"/>
<path d="M335.799 650.316L318.984 660.274L272.565 600.157L297.385 585.458L335.799 650.316Z" fill="#FFB6B6"/>
<path d="M349.742 664.075L295.521 696.187L295.115 695.501C291.8 689.904 290.844 683.22 292.458 676.919C294.071 670.617 298.122 665.215 303.719 661.899L303.72 661.898L336.837 642.286L349.742 664.075Z" fill="#2F2E41"/>
<path d="M417.006 650.316L433.821 660.274L480.239 600.157L455.421 585.458L417.006 650.316Z" fill="#FFB6B6"/>
<path d="M415.968 642.286L449.085 661.898L449.086 661.899C454.683 665.215 458.734 670.617 460.347 676.919C461.96 683.22 461.005 689.904 457.69 695.501L457.284 696.187L403.062 664.075L415.968 642.286Z" fill="#2F2E41"/>
<path d="M428.561 420.934C428.561 420.934 373.697 459.575 320.294 424.706L319.458 449.013C319.458 449.013 205.066 497.525 223.844 535.423C242.623 573.321 282.082 612.647 282.082 612.647L303.605 594.924L279.55 534.157L344.981 506.439L402.023 506.727L481.564 538.918L443.583 595.887L477.052 606.317C477.052 606.317 556.813 532.891 539.721 512.003C526.022 495.26 444.016 457.721 444.016 457.721L428.561 420.934Z" fill="#2F2E41"/>
<path d="M447.655 279.181L400.168 263.44L388.43 246.783H354.247L344.12 263.704L300.086 279.181C297.686 279.867 295.623 281.416 294.294 283.529C292.966 285.643 292.465 288.174 292.888 290.634L312.468 451.869L344.119 449.338C344.119 449.338 386.762 482.61 412.485 444.274L413.859 424.942L440.338 450.604L454.854 290.634C455.277 288.174 454.776 285.643 453.448 283.529C452.119 281.416 450.056 279.867 447.655 279.181Z" fill="#6C63FF"/>
<path d="M411.197 169.842C397.156 146.12 369.379 145.014 369.379 145.014C369.379 145.014 342.311 141.553 324.948 177.682C308.764 211.357 286.427 232.478 321.352 240.361L327.66 220.728L331.567 241.823C336.54 242.181 341.528 242.266 346.51 242.078C383.912 240.871 419.531 242.432 418.384 229.011C416.859 211.171 424.706 192.667 411.197 169.842Z" fill="#2F2E41"/>
<path d="M372.605 240.35C392.882 240.35 409.32 223.913 409.32 203.637C409.32 183.361 392.882 166.924 372.605 166.924C352.328 166.924 335.89 183.361 335.89 203.637C335.89 223.913 352.328 240.35 372.605 240.35Z" fill="#FFB8B8"/>
<path d="M406.255 166.289L378.812 151.915L340.916 157.795L333.075 192.423L352.592 191.673L358.045 178.951V191.464L367.052 191.117L372.278 170.863L375.546 192.423L407.563 191.77L406.255 166.289Z" fill="#2F2E41"/>
<path opacity="0.2" d="M315.229 313.96L343.715 425.354C343.715 425.354 330.422 327.886 315.229 313.96Z" fill="black"/>
<path opacity="0.2" d="M400.227 426.62L428.712 315.226C413.52 329.151 400.227 426.62 400.227 426.62Z" fill="black"/>
<path d="M406.861 523.935C408.395 523.124 409.734 521.987 410.782 520.604C411.829 519.22 412.561 517.624 412.925 515.927C413.289 514.23 413.277 512.474 412.888 510.783C412.5 509.091 411.746 507.505 410.679 506.137L436.539 435.412L416.744 425.335L393.273 504.511C391.092 506.456 389.701 509.134 389.363 512.036C389.025 514.938 389.764 517.864 391.439 520.258C393.114 522.652 395.609 524.349 398.451 525.026C401.293 525.704 404.286 525.316 406.861 523.935Z" fill="#FFB6B6"/>
<path d="M431.848 307.001L402.996 432.46C402.717 433.757 402.959 435.111 403.668 436.231C404.377 437.351 405.497 438.149 406.787 438.453L434.574 442.456C435.258 442.617 435.967 442.633 436.658 442.505C437.349 442.377 438.005 442.106 438.586 441.711C439.166 441.315 439.658 440.803 440.03 440.207C440.402 439.611 440.645 438.945 440.746 438.25L449.2 307.549L431.848 307.001Z" fill="#6C63FF"/>
<path d="M340.88 526.467C339.345 525.656 338.007 524.519 336.959 523.136C335.911 521.752 335.18 520.156 334.816 518.459C334.452 516.762 334.464 515.006 334.852 513.315C335.24 511.623 335.994 510.037 337.062 508.669L311.201 437.944L330.996 427.867L354.468 507.043C356.648 508.988 358.039 511.666 358.377 514.568C358.715 517.47 357.977 520.396 356.302 522.79C354.627 525.184 352.132 526.881 349.289 527.558C346.447 528.236 343.455 527.848 340.88 526.467Z" fill="#FFB6B6"/>
<path d="M315.892 309.533L344.745 434.992C345.023 436.289 344.782 437.643 344.073 438.763C343.364 439.883 342.244 440.681 340.953 440.985L313.167 444.988C312.483 445.149 311.773 445.165 311.082 445.037C310.392 444.909 309.735 444.638 309.155 444.243C308.574 443.847 308.083 443.335 307.711 442.739C307.339 442.143 307.095 441.477 306.995 440.782L296.009 310.081L315.892 309.533Z" fill="#6C63FF"/>
<path d="M635.059 687.141L636.005 685.792C652.256 697.201 664.111 722.581 671.24 761.225C674.499 779.274 676.455 797.535 677.093 815.865L675.444 815.897C675.425 814.879 673.021 713.794 635.059 687.141Z" fill="#E4E4E4"/>
<path d="M637.532 760.074L638.479 758.724C655.419 770.617 656.449 813.645 656.485 815.471L654.836 815.503C654.828 815.064 653.789 771.487 637.532 760.074Z" fill="#E4E4E4"/>
<path d="M625.64 686.466C630.193 686.466 633.883 682.776 633.883 678.223C633.883 673.671 630.193 669.98 625.64 669.98C621.087 669.98 617.396 673.671 617.396 678.223C617.396 682.776 621.087 686.466 625.64 686.466Z" fill="#E4E4E4"/>
<path d="M629.762 758.18C634.315 758.18 638.005 754.49 638.005 749.937C638.005 745.385 634.315 741.694 629.762 741.694C625.209 741.694 621.519 745.385 621.519 749.937C621.519 754.49 625.209 758.18 629.762 758.18Z" fill="#E4E4E4"/>
<path d="M666.778 675.144C669.398 681.022 670.262 687.532 669.266 693.889C668.271 700.247 665.458 706.181 661.168 710.977C658.548 705.099 657.684 698.589 658.679 692.232C659.674 685.874 662.487 679.94 666.778 675.144Z" fill="#E4E4E4"/>
<path d="M616.317 705.311C622.562 703.756 629.123 704.039 635.21 706.127C641.298 708.215 646.651 712.018 650.626 717.079C644.382 718.634 637.821 718.351 631.733 716.263C625.646 714.175 620.292 710.372 616.317 705.311Z" fill="#E4E4E4"/>
<path d="M620.623 771.366C625 770.276 629.599 770.475 633.865 771.938C638.132 773.402 641.885 776.067 644.671 779.615C640.294 780.705 635.695 780.506 631.428 779.043C627.162 777.579 623.409 774.913 620.623 771.366Z" fill="#E4E4E4"/>
</g>
<defs>
<clipPath id="clip0_491_6452">
<rect width="748" height="819" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -193,6 +193,23 @@ class TrackEventServices extends APIService {
});
}
async trackUserTourCompleteEvent(
data: any,
user: ICurrentUserResponse | undefined
): Promise<any> {
return this.request({
url: "/api/track-event",
method: "POST",
data: {
eventName: "USER_TOUR_COMPLETE",
extra: {
...data,
},
user: user,
},
});
}
async trackIssueEvent(
data: IIssue | any,
eventName: IssueEventType,

View File

@ -69,6 +69,25 @@ class UserService extends APIService {
});
}
async updateUserTourCompleted(user: ICurrentUserResponse): Promise<any> {
return this.patch("/api/users/me/tour-completed/", {
is_tour_completed: true,
})
.then((response) => {
if (trackEvent)
trackEventServices.trackUserTourCompleteEvent(
{
user_role: user.role ?? "None",
},
user
);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async getUserActivity(): Promise<IUserActivityResponse> {
return this.get("/api/users/activities/")
.then((response) => response?.data)

View File

@ -13,6 +13,7 @@ import {
IWorkspaceSearchResults,
IProductUpdateResponse,
ICurrentUserResponse,
IWorkspaceBulkInviteFormData,
} from "types";
const trackEvent =
@ -87,7 +88,7 @@ class WorkspaceService extends APIService {
async inviteWorkspace(
workspaceSlug: string,
data: any,
data: IWorkspaceBulkInviteFormData,
user: ICurrentUserResponse | undefined
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/invite/`, data)

View File

@ -45,26 +45,6 @@
--color-text-100: 23, 23, 23; /* primary text */
--color-text-200: 82, 82, 82; /* secondary text */
--color-text-300: 115, 115, 115; /* tertiary text */
--color-sidebar-background-100: 255, 255, 255; /* primary sidebar bg */
--color-sidebar-background-90: 250, 250, 250; /* secondary sidebar bg */
--color-sidebar-background-80: 245, 245, 245; /* tertiary sidebar bg */
--color-sidebar-text-100: 23, 23, 23; /* primary sidebar text */
--color-sidebar-text-200: 82, 82, 82; /* secondary sidebar text */
--color-sidebar-text-300: 115, 115, 115; /* tertiary sidebar text */
}
[data-theme="light"] {
color-scheme: light !important;
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 250, 250, 250; /* secondary bg */
--color-background-80: 245, 245, 245; /* tertiary bg */
--color-text-100: 23, 23, 23; /* primary text */
--color-text-200: 58, 58, 58; /* secondary text */
--color-text-300: 82, 82, 82; /* tertiary text */
--color-text-400: 163, 163, 163; /* placeholder text */
--color-border-100: 245, 245, 245; /* subtle border= 1 */

View File

@ -1,29 +1,30 @@
import { IIssue, IIssueLite, IWorkspace, NestedKeyOf, Properties } from "./";
export interface IUser {
id: readonly string;
last_login: readonly Date;
avatar: string;
username: string;
mobile_number: string;
created_at: readonly Date;
created_location: readonly string;
date_joined: readonly Date;
email: string;
first_name: string;
last_name: string;
date_joined: readonly Date;
created_at: readonly Date;
updated_at: readonly Date;
last_location: readonly string;
created_location: readonly string;
id: readonly string;
is_email_verified: boolean;
is_onboarded: boolean;
token: string;
role: string;
theme: ICustomTheme;
my_issues_prop?: {
is_tour_completed: boolean;
last_location: readonly string;
last_login: readonly Date;
last_name: string;
mobile_number: string;
my_issues_prop: {
properties: Properties;
groupBy: NestedKeyOf<IIssue> | null;
};
} | null;
onboarding_step: OnboardingSteps;
role: string;
token: string;
theme: ICustomTheme;
updated_at: readonly Date;
username: string;
[...rest: string]: any;
}
@ -40,9 +41,15 @@ export interface ICustomTheme {
export interface ICurrentUserResponse extends IUser {
assigned_issues: number;
// user: IUser;
last_workspace_id: string | null;
workspace_invites: number;
is_onboarded: boolean;
workspace: {
fallback_workspace_id: string | null;
fallback_workspace_slug: string | null;
invites: number;
last_workspace_id: string | null;
last_workspace_slug: string | null;
};
}
export interface IUserLite {
@ -119,3 +126,10 @@ export type UserAuth = {
isViewer: boolean;
isGuest: boolean;
};
export type OnboardingSteps = {
profile_complete: boolean;
workspace_create: boolean;
workspace_invite: boolean;
workspace_join: boolean;
};

View File

@ -13,7 +13,7 @@ export interface IWorkspace {
readonly slug: string;
readonly created_by: string;
readonly updated_by: string;
company_size: number | null;
organization_size: string;
total_issues: number | null;
}
@ -35,6 +35,10 @@ export interface IWorkspaceMemberInvitation {
workspace: IWorkspace;
}
export interface IWorkspaceBulkInviteFormData {
emails: { email: string; role: 5 | 10 | 15 | 20 }[];
}
export type Properties = {
assignee: boolean;
due_date: boolean;