style: onboarding, chore: refactoring (#474)
* style: onboarding screens * style: onboarding card component and refactoring * fix: onboarding card text fix * fix: merge conflict fix
@ -1,33 +0,0 @@
|
|||||||
// next
|
|
||||||
import Image from "next/image";
|
|
||||||
// images
|
|
||||||
import Module from "public/onboarding/module.png";
|
|
||||||
|
|
||||||
const BreakIntoModules: React.FC = () => (
|
|
||||||
<div className="h-full space-y-4">
|
|
||||||
<div className="relative h-1/2">
|
|
||||||
<div
|
|
||||||
className="absolute bottom-0 z-10 h-8 w-full bg-white"
|
|
||||||
style={{
|
|
||||||
background: "linear-gradient(0deg, #fff 84.2%, rgba(255, 255, 255, 0) 34.35%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src={Module}
|
|
||||||
className="h-full"
|
|
||||||
objectFit="contain"
|
|
||||||
layout="fill"
|
|
||||||
alt="Plane- Modules"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto h-1/2 space-y-4 lg:w-1/2">
|
|
||||||
<h2 className="text-2xl font-medium">Break into Modules</h2>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
Modules break your big thing into Projects or Features, to help you organize better.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">4/5</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default BreakIntoModules;
|
|
@ -1,24 +0,0 @@
|
|||||||
// next
|
|
||||||
import Image from "next/image";
|
|
||||||
// images
|
|
||||||
import Commands from "public/onboarding/command-menu.png";
|
|
||||||
|
|
||||||
const CommandMenu: React.FC = () => (
|
|
||||||
<div className="h-full space-y-4">
|
|
||||||
<div className="h-1/2 space-y-4">
|
|
||||||
<h5 className="text-sm text-gray-500">Open the contextual menu with:</h5>
|
|
||||||
<div className="relative h-1/2">
|
|
||||||
<Image src={Commands} objectFit="contain" layout="fill" alt="Plane- Issues" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto h-1/2 space-y-4 lg:w-2/3">
|
|
||||||
<h2 className="text-2xl font-medium">Command Menu</h2>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
With Command Menu, you can create, update and navigate across the platform.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">5/5</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default CommandMenu;
|
|
4
apps/app/components/onboarding/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./invite-members";
|
||||||
|
export * from "./onboarding-card";
|
||||||
|
export * from "./user-details";
|
||||||
|
export * from "./workspace";
|
@ -4,14 +4,15 @@ import useToast from "hooks/use-toast";
|
|||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
import { IUser } from "types";
|
import { IUser } from "types";
|
||||||
// ui components
|
// ui components
|
||||||
import { MultiInput, OutlineButton } from "components/ui";
|
import { MultiInput, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||||
workspace: any;
|
workspace: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InviteMembers: React.FC<Props> = ({ setStep, workspace }) => {
|
export const InviteMembers: React.FC<Props> = ({ setStep, workspace }) => {
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -39,43 +40,50 @@ const InviteMembers: React.FC<Props> = ({ setStep, workspace }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="grid w-full place-items-center space-y-8"
|
className="flex w-full items-center justify-center"
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.code === "Enter") e.preventDefault();
|
if (e.code === "Enter") e.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full space-y-8 rounded-lg bg-white p-8 md:w-2/5">
|
<div className="flex w-full max-w-xl flex-col gap-12">
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col gap-6 rounded-[10px] bg-white px-10 py-7 shadow-md">
|
||||||
<h2 className="text-2xl font-medium">Invite co-workers to your team</h2>
|
<h2 className="text-2xl font-medium ">Invite co-workers to your team</h2>
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col items-start justify-center gap-2.5 ">
|
||||||
<div className="col-span-2 space-y-2">
|
<span>Email</span>
|
||||||
|
<div className="w-full">
|
||||||
<MultiInput
|
<MultiInput
|
||||||
label="Enter e-mails to invite"
|
|
||||||
name="emails"
|
name="emails"
|
||||||
placeholder="dummy@plane.so"
|
placeholder="Enter co-workers email id"
|
||||||
watch={watch}
|
watch={watch}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto flex h-1/4 gap-2 lg:w-1/2">
|
<div className="flex w-full flex-col items-center justify-center gap-3 ">
|
||||||
<button
|
<PrimaryButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full rounded-md bg-gray-200 px-4 py-2 text-sm"
|
className="flex w-1/2 items-center justify-center text-center"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Inviting..." : "Continue"}
|
||||||
|
</PrimaryButton>
|
||||||
|
|
||||||
|
<SecondaryButton
|
||||||
|
type="button"
|
||||||
|
className="w-1/2 rounded-lg bg-transparent border-none"
|
||||||
|
size="md"
|
||||||
|
outline
|
||||||
|
onClick={() => setStep(4)}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Inviting..." : "Invite"}
|
|
||||||
</button>
|
|
||||||
<OutlineButton theme="secondary" className="w-full" onClick={() => setStep(4)}>
|
|
||||||
Skip
|
Skip
|
||||||
</OutlineButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InviteMembers;
|
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
// next
|
|
||||||
import Image from "next/image";
|
|
||||||
// images
|
|
||||||
import Cycle from "public/onboarding/cycle.png";
|
|
||||||
|
|
||||||
const MoveWithCycles: React.FC = () => (
|
|
||||||
<div className="h-full space-y-4">
|
|
||||||
<div className="relative h-1/2">
|
|
||||||
<div
|
|
||||||
className="absolute bottom-0 z-10 h-8 w-full bg-white"
|
|
||||||
style={{
|
|
||||||
background: "linear-gradient(0deg, #fff 84.2%, rgba(255, 255, 255, 0) 34.35%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src={Cycle}
|
|
||||||
className="h-full"
|
|
||||||
objectFit="contain"
|
|
||||||
layout="fill"
|
|
||||||
alt="Plane- Cycles"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto h-1/2 space-y-4 lg:w-2/3">
|
|
||||||
<h2 className="text-2xl font-medium">Move with Cycles</h2>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
Cycles help you and your team to progress faster, similar to the sprints commonly used in
|
|
||||||
agile development.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">3/5</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default MoveWithCycles;
|
|
24
apps/app/components/onboarding/onboarding-card.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OnboardingCard: React.FC<Props> = ({ data }) => (
|
||||||
|
<>
|
||||||
|
<div className="h-44 w-full">
|
||||||
|
<Image src={data.imgURL} height="180" width="450" alt={data.title} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-medium">{data.title}</h3>
|
||||||
|
<p className="text-base text-gray-400">{data.description}</p>
|
||||||
|
<span className="text-base text-gray-400">{data.step}</span>
|
||||||
|
</>
|
||||||
|
);
|
@ -1,34 +0,0 @@
|
|||||||
// next
|
|
||||||
import Image from "next/image";
|
|
||||||
// images
|
|
||||||
import Issue from "public/onboarding/issue.png";
|
|
||||||
|
|
||||||
const PlanWithIssues: React.FC = () => (
|
|
||||||
<div className="h-full space-y-4">
|
|
||||||
<div className="relative h-1/2">
|
|
||||||
<div
|
|
||||||
className="absolute bottom-0 z-10 h-8 w-full bg-white"
|
|
||||||
style={{
|
|
||||||
background: "linear-gradient(0deg, #fff 84.2%, rgba(255, 255, 255, 0) 34.35%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src={Issue}
|
|
||||||
className="h-full"
|
|
||||||
objectFit="contain"
|
|
||||||
layout="fill"
|
|
||||||
alt="Plane- Issues"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto h-1/2 space-y-4 lg:w-2/3">
|
|
||||||
<h2 className="text-2xl font-medium">Plan with Issues</h2>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
The issue is the building block of the Plane. Most concepts in Plane are either associated
|
|
||||||
with issues and their properties.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">2/5</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default PlanWithIssues;
|
|
@ -1,15 +1,17 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
import userService from "services/user.service";
|
import userService from "services/user.service";
|
||||||
// ui
|
// ui
|
||||||
import { Input } from "components/ui";
|
import { CustomSelect, Input, PrimaryButton } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import { IUser } from "types";
|
import { IUser } from "types";
|
||||||
|
// constant
|
||||||
|
import { USER_ROLE } from "constants/workspace";
|
||||||
|
|
||||||
const defaultValues: Partial<IUser> = {
|
const defaultValues: Partial<IUser> = {
|
||||||
first_name: "",
|
first_name: "",
|
||||||
@ -22,12 +24,13 @@ type Props = {
|
|||||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserDetails: React.FC<Props> = ({ user, setStep }) => {
|
export const UserDetails: React.FC<Props> = ({ user, setStep }) => {
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
control,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<IUser>({
|
} = useForm<IUser>({
|
||||||
@ -59,61 +62,72 @@ const UserDetails: React.FC<Props> = ({ user, setStep }) => {
|
|||||||
}, [user, reset]);
|
}, [user, reset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="grid w-full place-items-center" onSubmit={handleSubmit(onSubmit)}>
|
<form className="flex w-full items-center justify-center" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="w-full space-y-8 rounded-lg bg-white p-8 md:w-2/5">
|
<div className="flex w-full max-w-xl flex-col gap-12">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="flex flex-col rounded-[10px] bg-white shadow-md">
|
||||||
<div>
|
<div className="flex flex-col justify-between gap-3 px-10 py-7 sm:flex-row">
|
||||||
<Input
|
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||||
label="First Name"
|
<span>First name</span>
|
||||||
name="first_name"
|
<Input
|
||||||
placeholder="Enter first name"
|
name="first_name"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
register={register}
|
register={register}
|
||||||
validations={{
|
validations={{
|
||||||
required: "First name is required",
|
required: "First name is required",
|
||||||
}}
|
}}
|
||||||
error={errors.first_name}
|
error={errors.first_name}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||||
|
<span>Last name</span>
|
||||||
|
<Input
|
||||||
|
name="last_name"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Last name is required",
|
||||||
|
}}
|
||||||
|
error={errors.last_name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-col items-start justify-center gap-2.5 border-t border-gray-300 px-10 py-7">
|
||||||
<Input
|
<span>What is your role?</span>
|
||||||
label="Last Name"
|
<div className="w-full">
|
||||||
name="last_name"
|
<Controller
|
||||||
placeholder="Enter last name"
|
name="role"
|
||||||
autoComplete="off"
|
control={control}
|
||||||
register={register}
|
rules={{ required: "This field is required" }}
|
||||||
validations={{
|
render={({ field: { value, onChange } }) => (
|
||||||
required: "Last name is required",
|
<CustomSelect
|
||||||
}}
|
value={value}
|
||||||
error={errors.last_name}
|
onChange={onChange}
|
||||||
/>
|
label={value ? value.toString() : "Select your role"}
|
||||||
</div>
|
input
|
||||||
<div className="col-span-2">
|
width="w-full"
|
||||||
<Input
|
>
|
||||||
label="Role"
|
{USER_ROLE?.map((item) => (
|
||||||
name="role"
|
<CustomSelect.Option key={item.value} value={item.value}>
|
||||||
placeholder="What is your role?"
|
{item.label}
|
||||||
autoComplete="off"
|
</CustomSelect.Option>
|
||||||
register={register}
|
))}
|
||||||
validations={{
|
</CustomSelect>
|
||||||
required: "Role is required",
|
)}
|
||||||
}}
|
/>
|
||||||
error={errors.role}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto h-1/4 lg:w-1/2">
|
<div className="flex w-full items-center justify-center ">
|
||||||
<button
|
<PrimaryButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full rounded-md bg-gray-200 px-4 py-2 text-sm"
|
className="flex w-1/2 items-center justify-center text-center"
|
||||||
|
size="md"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Updating..." : "Continue"}
|
{isSubmitting ? "Updating..." : "Continue"}
|
||||||
</button>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserDetails;
|
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
// next
|
|
||||||
import Image from "next/image";
|
|
||||||
// icons
|
|
||||||
import Logo from "public/logo.png";
|
|
||||||
|
|
||||||
const Welcome: React.FC = () => (
|
|
||||||
<div className="h-full space-y-4">
|
|
||||||
<div className="h-1/2">
|
|
||||||
<Image src={Logo} height={100} width={100} alt="Plane Logo" />
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto h-1/2 space-y-4 lg:w-2/3">
|
|
||||||
<h2 className="text-2xl font-medium">Welcome to Plane</h2>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
Plane helps you plan your issues, cycles, and product modules to ship faster.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">1/5</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Welcome;
|
|
@ -14,13 +14,16 @@ import { IWorkspaceMemberInvitation } from "types";
|
|||||||
import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { CreateWorkspaceForm } from "components/workspace";
|
import { CreateWorkspaceForm } from "components/workspace";
|
||||||
|
// ui
|
||||||
|
import { PrimaryButton } from "components/ui";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||||
setWorkspace: React.Dispatch<React.SetStateAction<any>>;
|
setWorkspace: React.Dispatch<React.SetStateAction<any>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
export const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
||||||
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
|
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
|
||||||
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
||||||
|
|
||||||
@ -59,28 +62,39 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid w-full place-items-center">
|
<div className="grid w-full place-items-center">
|
||||||
<Tab.Group as="div" className="w-full rounded-lg bg-white p-8 md:w-2/5">
|
<Tab.Group
|
||||||
|
as="div"
|
||||||
|
className="flex w-full max-w-xl flex-col rounded-[10px] bg-white shadow-md"
|
||||||
|
>
|
||||||
<Tab.List
|
<Tab.List
|
||||||
as="div"
|
as="div"
|
||||||
className="grid grid-cols-2 items-center gap-2 rounded-lg bg-gray-100 p-2 text-sm"
|
className="text-gray-8 flex items-center justify-start gap-3 px-10 pt-7 text-base"
|
||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
|
`rounded-3xl border px-5 py-2 outline-none ${
|
||||||
|
selected
|
||||||
|
? "border-theme bg-theme text-white"
|
||||||
|
: "border-gray-300 bg-white hover:bg-hover-gray"
|
||||||
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
New workspace
|
New Workspace
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
|
`rounded-3xl border px-5 py-2 outline-none ${
|
||||||
|
selected
|
||||||
|
? "border-theme bg-theme text-white"
|
||||||
|
: "border-gray-300 bg-white hover:bg-hover-gray"
|
||||||
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Invited workspaces
|
Invited Workspace
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
<Tab.Panel className="pt-4">
|
<Tab.Panel>
|
||||||
<CreateWorkspaceForm
|
<CreateWorkspaceForm
|
||||||
onSubmit={(res) => {
|
onSubmit={(res) => {
|
||||||
setWorkspace(res);
|
setWorkspace(res);
|
||||||
@ -89,8 +103,8 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<div className="mt-4 space-y-8">
|
<div className="mt-6" >
|
||||||
<div className="divide-y">
|
<div className="divide-y py-8">
|
||||||
{invitations && invitations.length > 0 ? (
|
{invitations && invitations.length > 0 ? (
|
||||||
invitations.map((invitation) => (
|
invitations.map((invitation) => (
|
||||||
<div key={invitation.id}>
|
<div key={invitation.id}>
|
||||||
@ -149,19 +163,19 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto h-1/4 lg:w-1/2">
|
<div className="flex w-full items-center justify-center rounded-b-[10px] py-7 ">
|
||||||
<button
|
<PrimaryButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className={`w-full rounded-md bg-gray-200 px-4 py-2 text-sm ${
|
className={`flex w-1/2 items-center justify-center text-center text-sm ${
|
||||||
isJoiningWorkspaces || invitationsRespond.length === 0
|
isJoiningWorkspaces || invitationsRespond.length === 0
|
||||||
? "cursor-not-allowed opacity-80"
|
? "cursor-not-allowed opacity-80"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
|
size="md"
|
||||||
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
|
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
|
||||||
onClick={submitInvitations}
|
|
||||||
>
|
>
|
||||||
Join Workspace
|
Join Workspace
|
||||||
</button>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
@ -170,5 +184,3 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Workspace;
|
|
||||||
|
@ -9,7 +9,7 @@ import workspaceService from "services/workspace.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { CustomSelect, Input } from "components/ui";
|
import { CustomSelect, Input, PrimaryButton } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import { IWorkspace } from "types";
|
import { IWorkspace } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -17,6 +17,7 @@ import { USER_WORKSPACES } from "constants/fetch-keys";
|
|||||||
// constants
|
// constants
|
||||||
import { COMPANY_SIZE } from "constants/workspace";
|
import { COMPANY_SIZE } from "constants/workspace";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSubmit: (res: IWorkspace) => void;
|
onSubmit: (res: IWorkspace) => void;
|
||||||
};
|
};
|
||||||
@ -77,78 +78,87 @@ export const CreateWorkspaceForm: React.FC<Props> = ({ onSubmit }) => {
|
|||||||
}, [reset]);
|
}, [reset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="space-y-8" onSubmit={handleSubmit(handleCreateWorkspace)}>
|
<form
|
||||||
<div className="w-full space-y-4 bg-white">
|
className="flex w-full items-center justify-center"
|
||||||
<div className="grid grid-cols-1 gap-4">
|
onSubmit={handleSubmit(handleCreateWorkspace)}
|
||||||
<div>
|
>
|
||||||
<Input
|
<div className="flex w-full max-w-xl flex-col">
|
||||||
name="name"
|
<div className="flex flex-col rounded-[10px] bg-white shadow-md">
|
||||||
register={register}
|
<div className="flex flex-col justify-between gap-3 px-10 py-7">
|
||||||
label="Workspace name"
|
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||||
placeholder="Enter name"
|
<span>Workspace name</span>
|
||||||
autoComplete="off"
|
|
||||||
onChange={(e) =>
|
|
||||||
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"))
|
|
||||||
}
|
|
||||||
validations={{
|
|
||||||
required: "Workspace name is required",
|
|
||||||
}}
|
|
||||||
error={errors.name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h6 className="text-gray-500">Workspace slug</h6>
|
|
||||||
<div className="flex items-center rounded-md border border-gray-300 px-3">
|
|
||||||
<span className="text-sm text-slate-600">{"https://app.plane.so/"}</span>
|
|
||||||
<Input
|
<Input
|
||||||
mode="trueTransparent"
|
name="name"
|
||||||
autoComplete="off"
|
|
||||||
name="slug"
|
|
||||||
register={register}
|
register={register}
|
||||||
className="block w-full rounded-md bg-transparent py-2 px-0 text-sm"
|
autoComplete="off"
|
||||||
|
onChange={(e) =>
|
||||||
|
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"))
|
||||||
|
}
|
||||||
|
validations={{
|
||||||
|
required: "Workspace name is required",
|
||||||
|
}}
|
||||||
|
error={errors.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{slugError && (
|
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||||
<span className="-mt-3 text-sm text-red-500">Workspace URL is already taken!</span>
|
<span>Workspace URL</span>
|
||||||
)}
|
<div className="flex w-full items-center rounded-md border border-gray-300 px-3">
|
||||||
</div>
|
<span className="text-sm text-slate-600">{"https://app.plane.so/"}</span>
|
||||||
<div>
|
<Input
|
||||||
<h6 className="text-gray-500">Company size</h6>
|
mode="trueTransparent"
|
||||||
<Controller
|
autoComplete="off"
|
||||||
name="company_size"
|
name="slug"
|
||||||
control={control}
|
register={register}
|
||||||
rules={{ required: "This field is required" }}
|
className="block w-full rounded-md bg-transparent py-2 px-0 text-sm"
|
||||||
render={({ field: { value, onChange } }) => (
|
/>
|
||||||
<CustomSelect
|
</div>
|
||||||
value={value}
|
{slugError && (
|
||||||
onChange={onChange}
|
<span className="-mt-3 text-sm text-red-500">Workspace URL is already taken!</span>
|
||||||
label={value ? value.toString() : "Select company size"}
|
|
||||||
input
|
|
||||||
width="w-full"
|
|
||||||
>
|
|
||||||
{COMPANY_SIZE?.map((item) => (
|
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
|
||||||
{item.label}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
{errors.company_size && (
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-start justify-center gap-2.5 border-t border-gray-300 px-10 py-7">
|
||||||
|
<span>How large is your company</span>
|
||||||
|
<div className="w-full">
|
||||||
|
<Controller
|
||||||
|
name="company_size"
|
||||||
|
control={control}
|
||||||
|
rules={{ required: "This field is required" }}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<CustomSelect
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
label={value ? value.toString() : "Select company size"}
|
||||||
|
input
|
||||||
|
width="w-full"
|
||||||
|
>
|
||||||
|
{COMPANY_SIZE?.map((item) => (
|
||||||
|
<CustomSelect.Option key={item.value} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.company_size && (
|
||||||
<span className="text-sm text-red-500">{errors.company_size.message}</span>
|
<span className="text-sm text-red-500">{errors.company_size.message}</span>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full items-center justify-center rounded-b-[10px] py-7 ">
|
||||||
|
<PrimaryButton
|
||||||
|
type="submit"
|
||||||
|
className="flex w-1/2 items-center justify-center text-center"
|
||||||
|
size="md"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Creating..." : "Create Workspace"}
|
||||||
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto h-1/4 lg:w-1/2">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full rounded-md bg-gray-200 px-4 py-2 text-sm"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? "Creating..." : "Continue"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
import Welcome from "public/onboarding/welcome.svg";
|
||||||
|
import Issue from "public/onboarding/issue.svg";
|
||||||
|
import Cycle from "public/onboarding/cycle.svg";
|
||||||
|
import Module from "public/onboarding/module.svg";
|
||||||
|
import CommandMenu from "public/onboarding/command-menu.svg";
|
||||||
|
|
||||||
export const ROLE = {
|
export const ROLE = {
|
||||||
5: "Guest",
|
5: "Guest",
|
||||||
10: "Viewer",
|
10: "Viewer",
|
||||||
@ -11,3 +17,49 @@ export const COMPANY_SIZE = [
|
|||||||
{ value: 25, label: "25" },
|
{ value: 25, label: "25" },
|
||||||
{ value: 50, label: "50" },
|
{ value: 50, label: "50" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const USER_ROLE = [
|
||||||
|
{ 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: "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:
|
||||||
|
"The issue is the building block of the Plane. Most concepts in Plane are either associated with issues and their properties.",
|
||||||
|
},
|
||||||
|
cycle: {
|
||||||
|
imgURL: Cycle,
|
||||||
|
step: "3/5",
|
||||||
|
title: "Move with Cycles",
|
||||||
|
description:
|
||||||
|
"Cycles help you and your team to progress faster, similar to the sprints commonly used in agile development.",
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
imgURL: Module,
|
||||||
|
step: "4/5",
|
||||||
|
title: "Break into Modules ",
|
||||||
|
description:
|
||||||
|
"Modules break your big think 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.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -12,14 +12,11 @@ import useUser from "hooks/use-user";
|
|||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import DefaultLayout from "layouts/default-layout";
|
||||||
// components
|
// components
|
||||||
import Welcome from "components/onboarding/welcome";
|
import { InviteMembers, OnboardingCard, UserDetails, Workspace } from "components/onboarding";
|
||||||
import PlanWithIssues from "components/onboarding/plan-with-issues";
|
// ui
|
||||||
import MoveWithCycles from "components/onboarding/move-with-cycles";
|
import { PrimaryButton } from "components/ui";
|
||||||
import BreakIntoModules from "components/onboarding/break-into-modules";
|
// constant
|
||||||
import UserDetails from "components/onboarding/user-details";
|
import { ONBOARDING_CARDS } from "constants/workspace";
|
||||||
import Workspace from "components/onboarding/workspace";
|
|
||||||
import InviteMembers from "components/onboarding/invite-members";
|
|
||||||
import CommandMenu from "components/onboarding/command-menu";
|
|
||||||
// images
|
// images
|
||||||
import Logo from "public/onboarding/logo.svg";
|
import Logo from "public/onboarding/logo.svg";
|
||||||
// types
|
// types
|
||||||
@ -38,9 +35,9 @@ const Onboarding: NextPage = () => {
|
|||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<div className="grid h-full place-items-center p-5">
|
<div className="grid h-full place-items-center p-5">
|
||||||
{step <= 3 ? (
|
{step <= 3 ? (
|
||||||
<div className="w-full space-y-4">
|
<div className="w-full">
|
||||||
<div className="text-center">
|
<div className="text-center mb-8">
|
||||||
<Image src={Logo} height="40" alt="Plane Logo" />
|
<Image src={Logo} height="50" alt="Plane Logo" />
|
||||||
</div>
|
</div>
|
||||||
{step === 1 ? (
|
{step === 1 ? (
|
||||||
<UserDetails user={user} setStep={setStep} />
|
<UserDetails user={user} setStep={setStep} />
|
||||||
@ -51,39 +48,40 @@ const Onboarding: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-max min-h-[360px] w-full rounded-lg bg-white px-8 py-10 text-center md:w-1/2">
|
<div className="flex w-full max-w-2xl flex-col gap-12">
|
||||||
<div className="h-3/4 w-full">
|
<div className="flex flex-col items-center justify-center gap-7 rounded-[10px] bg-white px-14 py-10 text-center shadow-md">
|
||||||
{step === 4 ? (
|
{step === 4 ? (
|
||||||
<Welcome />
|
<OnboardingCard data={ONBOARDING_CARDS.welcome} />
|
||||||
) : step === 5 ? (
|
) : step === 5 ? (
|
||||||
<PlanWithIssues />
|
<OnboardingCard data={ONBOARDING_CARDS.issue} />
|
||||||
) : step === 6 ? (
|
) : step === 6 ? (
|
||||||
<MoveWithCycles />
|
<OnboardingCard data={ONBOARDING_CARDS.cycle} />
|
||||||
) : step === 7 ? (
|
) : step === 7 ? (
|
||||||
<BreakIntoModules />
|
<OnboardingCard data={ONBOARDING_CARDS.module} />
|
||||||
) : (
|
) : (
|
||||||
<CommandMenu />
|
<OnboardingCard data={ONBOARDING_CARDS.commandMenu} />
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="mx-auto flex h-1/4 items-end lg:w-1/2">
|
||||||
<div className="mx-auto flex h-1/4 items-end lg:w-1/2">
|
<PrimaryButton
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="flex w-full items-center justify-center text-center "
|
||||||
className="w-full rounded-md bg-gray-200 px-4 py-2 text-sm"
|
size="md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (step === 8) {
|
if (step === 8) {
|
||||||
userService
|
userService
|
||||||
.updateUserOnBoard()
|
.updateUserOnBoard()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
} else setStep((prevData) => prevData + 1);
|
} else setStep((prevData) => prevData + 1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{step === 4 || step === 8 ? "Get Started" : "Next"}
|
{step === 4 || step === 8 ? "Get Started" : "Next"}
|
||||||
</button>
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
Before Width: | Height: | Size: 8.1 KiB |
7
apps/app/public/onboarding/command-menu.svg
Normal file
After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 55 KiB |
43
apps/app/public/onboarding/cycle.svg
Normal file
After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 32 KiB |
31
apps/app/public/onboarding/issue.svg
Normal file
After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 33 KiB |
48
apps/app/public/onboarding/module.svg
Normal file
After Width: | Height: | Size: 80 KiB |
5
apps/app/public/onboarding/welcome.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 352 B |