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 { IUser } from "types";
|
||||
// ui components
|
||||
import { MultiInput, OutlineButton } from "components/ui";
|
||||
import { MultiInput, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
|
||||
|
||||
type Props = {
|
||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||
workspace: any;
|
||||
};
|
||||
|
||||
const InviteMembers: React.FC<Props> = ({ setStep, workspace }) => {
|
||||
export const InviteMembers: React.FC<Props> = ({ setStep, workspace }) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
@ -39,43 +40,50 @@ const InviteMembers: React.FC<Props> = ({ setStep, workspace }) => {
|
||||
|
||||
return (
|
||||
<form
|
||||
className="grid w-full place-items-center space-y-8"
|
||||
className="flex w-full items-center justify-center"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
onKeyDown={(e) => {
|
||||
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="space-y-4">
|
||||
<h2 className="text-2xl font-medium">Invite co-workers to your team</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="col-span-2 space-y-2">
|
||||
<div className="flex w-full max-w-xl flex-col gap-12">
|
||||
<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>
|
||||
<div className="flex flex-col items-start justify-center gap-2.5 ">
|
||||
<span>Email</span>
|
||||
<div className="w-full">
|
||||
<MultiInput
|
||||
label="Enter e-mails to invite"
|
||||
name="emails"
|
||||
placeholder="dummy@plane.so"
|
||||
placeholder="Enter co-workers email id"
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto flex h-1/4 gap-2 lg:w-1/2">
|
||||
<button
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3 ">
|
||||
<PrimaryButton
|
||||
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}
|
||||
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
|
||||
</OutlineButton>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</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 { useForm } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// ui
|
||||
import { Input } from "components/ui";
|
||||
import { CustomSelect, Input, PrimaryButton } from "components/ui";
|
||||
// types
|
||||
import { IUser } from "types";
|
||||
// constant
|
||||
import { USER_ROLE } from "constants/workspace";
|
||||
|
||||
const defaultValues: Partial<IUser> = {
|
||||
first_name: "",
|
||||
@ -22,12 +24,13 @@ type Props = {
|
||||
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 {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IUser>({
|
||||
@ -59,61 +62,72 @@ const UserDetails: React.FC<Props> = ({ user, setStep }) => {
|
||||
}, [user, reset]);
|
||||
|
||||
return (
|
||||
<form className="grid w-full place-items-center" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="w-full space-y-8 rounded-lg bg-white p-8 md:w-2/5">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Input
|
||||
label="First Name"
|
||||
name="first_name"
|
||||
placeholder="Enter first name"
|
||||
autoComplete="off"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "First name is required",
|
||||
}}
|
||||
error={errors.first_name}
|
||||
/>
|
||||
<form className="flex w-full items-center justify-center" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex w-full max-w-xl flex-col gap-12">
|
||||
<div className="flex flex-col rounded-[10px] bg-white shadow-md">
|
||||
<div className="flex flex-col justify-between gap-3 px-10 py-7 sm:flex-row">
|
||||
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||
<span>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-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>
|
||||
<Input
|
||||
label="Last Name"
|
||||
name="last_name"
|
||||
placeholder="Enter last name"
|
||||
autoComplete="off"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Last name is required",
|
||||
}}
|
||||
error={errors.last_name}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Input
|
||||
label="Role"
|
||||
name="role"
|
||||
placeholder="What is your role?"
|
||||
autoComplete="off"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Role is required",
|
||||
}}
|
||||
error={errors.role}
|
||||
/>
|
||||
<div className="flex flex-col items-start justify-center gap-2.5 border-t border-gray-300 px-10 py-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={onChange}
|
||||
label={value ? value.toString() : "Select your role"}
|
||||
input
|
||||
width="w-full"
|
||||
>
|
||||
{USER_ROLE?.map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto h-1/4 lg:w-1/2">
|
||||
<button
|
||||
<div className="flex w-full items-center justify-center ">
|
||||
<PrimaryButton
|
||||
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}
|
||||
>
|
||||
{isSubmitting ? "Updating..." : "Continue"}
|
||||
</button>
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</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";
|
||||
// constants
|
||||
import { CreateWorkspaceForm } from "components/workspace";
|
||||
// ui
|
||||
import { PrimaryButton } from "components/ui";
|
||||
|
||||
|
||||
type Props = {
|
||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||
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 [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
||||
|
||||
@ -59,28 +62,39 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
||||
|
||||
return (
|
||||
<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
|
||||
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
|
||||
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
|
||||
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.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel className="pt-4">
|
||||
<Tab.Panel>
|
||||
<CreateWorkspaceForm
|
||||
onSubmit={(res) => {
|
||||
setWorkspace(res);
|
||||
@ -89,8 +103,8 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<div className="mt-4 space-y-8">
|
||||
<div className="divide-y">
|
||||
<div className="mt-6" >
|
||||
<div className="divide-y py-8">
|
||||
{invitations && invitations.length > 0 ? (
|
||||
invitations.map((invitation) => (
|
||||
<div key={invitation.id}>
|
||||
@ -149,19 +163,19 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mx-auto h-1/4 lg:w-1/2">
|
||||
<button
|
||||
<div className="flex w-full items-center justify-center rounded-b-[10px] py-7 ">
|
||||
<PrimaryButton
|
||||
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
|
||||
? "cursor-not-allowed opacity-80"
|
||||
: ""
|
||||
}`}
|
||||
size="md"
|
||||
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
|
||||
onClick={submitInvitations}
|
||||
>
|
||||
Join Workspace
|
||||
</button>
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
@ -170,5 +184,3 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Workspace;
|
||||
|
@ -9,7 +9,7 @@ import workspaceService from "services/workspace.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { CustomSelect, Input } from "components/ui";
|
||||
import { CustomSelect, Input, PrimaryButton } from "components/ui";
|
||||
// types
|
||||
import { IWorkspace } from "types";
|
||||
// fetch-keys
|
||||
@ -17,6 +17,7 @@ import { USER_WORKSPACES } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { COMPANY_SIZE } from "constants/workspace";
|
||||
|
||||
|
||||
type Props = {
|
||||
onSubmit: (res: IWorkspace) => void;
|
||||
};
|
||||
@ -77,78 +78,87 @@ export const CreateWorkspaceForm: React.FC<Props> = ({ onSubmit }) => {
|
||||
}, [reset]);
|
||||
|
||||
return (
|
||||
<form className="space-y-8" onSubmit={handleSubmit(handleCreateWorkspace)}>
|
||||
<div className="w-full space-y-4 bg-white">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<Input
|
||||
name="name"
|
||||
register={register}
|
||||
label="Workspace name"
|
||||
placeholder="Enter name"
|
||||
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>
|
||||
<form
|
||||
className="flex w-full items-center justify-center"
|
||||
onSubmit={handleSubmit(handleCreateWorkspace)}
|
||||
>
|
||||
<div className="flex w-full max-w-xl flex-col">
|
||||
<div className="flex flex-col rounded-[10px] bg-white shadow-md">
|
||||
<div className="flex flex-col justify-between gap-3 px-10 py-7">
|
||||
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||
<span>Workspace name</span>
|
||||
<Input
|
||||
mode="trueTransparent"
|
||||
autoComplete="off"
|
||||
name="slug"
|
||||
name="name"
|
||||
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>
|
||||
{slugError && (
|
||||
<span className="-mt-3 text-sm text-red-500">Workspace URL is already taken!</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="text-gray-500">Company size</h6>
|
||||
<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>
|
||||
<div className="flex flex-col items-start justify-center gap-2.5">
|
||||
<span>Workspace URL</span>
|
||||
<div className="flex w-full items-center rounded-md border border-gray-300 px-3">
|
||||
<span className="text-sm text-slate-600">{"https://app.plane.so/"}</span>
|
||||
<Input
|
||||
mode="trueTransparent"
|
||||
autoComplete="off"
|
||||
name="slug"
|
||||
register={register}
|
||||
className="block w-full rounded-md bg-transparent py-2 px-0 text-sm"
|
||||
/>
|
||||
</div>
|
||||
{slugError && (
|
||||
<span className="-mt-3 text-sm text-red-500">Workspace URL is already taken!</span>
|
||||
)}
|
||||
/>
|
||||
{errors.company_size && (
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
</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 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>
|
||||
);
|
||||
};
|
||||
|
@ -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 = {
|
||||
5: "Guest",
|
||||
10: "Viewer",
|
||||
@ -11,3 +17,49 @@ export const COMPANY_SIZE = [
|
||||
{ value: 25, label: "25" },
|
||||
{ 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
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
// components
|
||||
import Welcome from "components/onboarding/welcome";
|
||||
import PlanWithIssues from "components/onboarding/plan-with-issues";
|
||||
import MoveWithCycles from "components/onboarding/move-with-cycles";
|
||||
import BreakIntoModules from "components/onboarding/break-into-modules";
|
||||
import UserDetails from "components/onboarding/user-details";
|
||||
import Workspace from "components/onboarding/workspace";
|
||||
import InviteMembers from "components/onboarding/invite-members";
|
||||
import CommandMenu from "components/onboarding/command-menu";
|
||||
import { InviteMembers, OnboardingCard, UserDetails, Workspace } from "components/onboarding";
|
||||
// ui
|
||||
import { PrimaryButton } from "components/ui";
|
||||
// constant
|
||||
import { ONBOARDING_CARDS } from "constants/workspace";
|
||||
// images
|
||||
import Logo from "public/onboarding/logo.svg";
|
||||
// types
|
||||
@ -38,9 +35,9 @@ const Onboarding: NextPage = () => {
|
||||
<DefaultLayout>
|
||||
<div className="grid h-full place-items-center p-5">
|
||||
{step <= 3 ? (
|
||||
<div className="w-full space-y-4">
|
||||
<div className="text-center">
|
||||
<Image src={Logo} height="40" alt="Plane Logo" />
|
||||
<div className="w-full">
|
||||
<div className="text-center mb-8">
|
||||
<Image src={Logo} height="50" alt="Plane Logo" />
|
||||
</div>
|
||||
{step === 1 ? (
|
||||
<UserDetails user={user} setStep={setStep} />
|
||||
@ -51,39 +48,40 @@ const Onboarding: NextPage = () => {
|
||||
)}
|
||||
</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="h-3/4 w-full">
|
||||
<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-white px-14 py-10 text-center shadow-md">
|
||||
{step === 4 ? (
|
||||
<Welcome />
|
||||
<OnboardingCard data={ONBOARDING_CARDS.welcome} />
|
||||
) : step === 5 ? (
|
||||
<PlanWithIssues />
|
||||
<OnboardingCard data={ONBOARDING_CARDS.issue} />
|
||||
) : step === 6 ? (
|
||||
<MoveWithCycles />
|
||||
<OnboardingCard data={ONBOARDING_CARDS.cycle} />
|
||||
) : 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">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded-md bg-gray-200 px-4 py-2 text-sm"
|
||||
onClick={() => {
|
||||
if (step === 8) {
|
||||
userService
|
||||
.updateUserOnBoard()
|
||||
.then(() => {
|
||||
router.push("/");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
} else setStep((prevData) => prevData + 1);
|
||||
}}
|
||||
>
|
||||
{step === 4 || step === 8 ? "Get Started" : "Next"}
|
||||
</button>
|
||||
<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) {
|
||||
userService
|
||||
.updateUserOnBoard()
|
||||
.then(() => {
|
||||
router.push("/");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
} else setStep((prevData) => prevData + 1);
|
||||
}}
|
||||
>
|
||||
{step === 4 || step === 8 ? "Get Started" : "Next"}
|
||||
</PrimaryButton>
|
||||
</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 |