fix: onboarding bugs & improvements (#2839)

* fix: terms & condition alignment

* fix: onboarding page scrolling

* fix: create workspace name clear

* fix: setup profile sidebar workspace name

* fix: invite team screen button text

* fix: inner div min height

* fix: allow single invite also in invite member

* fix: UI clipping in invite members

* fix: signin screen scroll

* fix: sidebar notification icon

* fix: sidebar project name & icon

* fix: user detail bottom image alignment

* fix: step indicator in invite member

* fix: try different account modal state

* fix: setup profile remove image

* fix: workspace slug clear

* fix: invite member UI & focus

* fix: step indicator size

* fix: inner div placement

* fix: invite member validation logic

* fix: cuurent user data persistency

* fix: sidebar animation colors

* feat: signup & resend

* fix: sign out theme persist from popover

* fix: imports

* chore: signin responsiveness

* fix: sign-in, sign-up top padding
This commit is contained in:
Lakhan Baheti 2023-11-23 13:45:00 +05:30 committed by sriram veeraghanta
parent f9590929dc
commit 3c89ef8cc3
21 changed files with 607 additions and 375 deletions

View File

@ -36,6 +36,8 @@ module.exports = {
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
"onbording-shadow-sm": "var(--color-onboarding-shadow-sm)",
},
colors: {
custom: {
@ -192,6 +194,7 @@ module.exports = {
border: {
100: convertToRGB("--color-onboarding-border-100"),
200: convertToRGB("--color-onboarding-border-200"),
300: convertToRGB("--color-onboarding-border-300"),
},
},
},
@ -372,8 +375,9 @@ module.exports = {
96: "21.6rem",
},
backgroundImage: {
"onboarding-gradient-primary": "var( --gradient-onboarding-primary)",
"onboarding-gradient-secondary": "var( --gradient-onboarding-secondary)",
"onboarding-gradient-100": "var( --gradient-onboarding-100)",
"onboarding-gradient-200": "var( --gradient-onboarding-200)",
"onboarding-gradient-300": "var( --gradient-onboarding-300)",
},
},
fontFamily: {

View File

@ -2,8 +2,6 @@
import React, { useState } from "react";
// next
import { useRouter } from "next/router";
// components
import { Button } from "@plane/ui";
// hooks
import useToast from "hooks/use-toast";
// services
@ -11,8 +9,10 @@ import { AuthService } from "services/auth.service";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// icons
import { AlertTriangle } from "lucide-react";
import { Trash2 } from "lucide-react";
import { UserService } from "services/user.service";
import { useTheme } from "next-themes";
import { mutate } from "swr";
type Props = {
isOpen: boolean;
@ -25,13 +25,17 @@ const userService = new UserService();
const DeleteAccountModal: React.FC<Props> = (props) => {
const { isOpen, onClose } = props;
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter();
const { setTheme } = useTheme();
const { setToastAlert } = useToast();
const handleSignOut = async () => {
await authService
.signOut()
.then(() => {
mutate("CURRENT_USER_DETAILS", null);
setTheme("system");
router.push("/");
})
.catch(() =>
@ -53,6 +57,8 @@ const DeleteAccountModal: React.FC<Props> = (props) => {
title: "Success!",
message: "Account deleted successfully.",
});
mutate("CURRENT_USER_DETAILS", null);
setTheme("system");
router.push("/");
})
.catch((err) =>
@ -100,7 +106,7 @@ const DeleteAccountModal: React.FC<Props> = (props) => {
<div className="">
<div className="flex items-center gap-x-4">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
<Trash2 className="h-5 w-5 text-red-600" aria-hidden="true" />
</div>
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100">
Not the right workspace?

View File

@ -1,17 +1,16 @@
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
// components
import { AuthType } from "components/page-views";
// services
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
import useTimer from "hooks/use-timer";
// icons
import { XCircle } from "lucide-react";
import { useTheme } from "next-themes";
// types
type EmailCodeFormValues = {
email: string;
key?: string;
@ -20,7 +19,14 @@ type EmailCodeFormValues = {
const authService = new AuthService();
export const EmailCodeForm = ({ handleSignIn }: any) => {
type Props = {
handleSignIn: any;
authType: AuthType;
};
export const EmailCodeForm: React.FC<Props> = (Props) => {
const { handleSignIn, authType } = Props;
// states
const [codeSent, setCodeSent] = useState(false);
const [codeResent, setCodeResent] = useState(false);
const [isCodeResending, setIsCodeResending] = useState(false);
@ -37,7 +43,6 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
setError,
setValue,
getValues,
watch,
formState: { errors, isSubmitting, isValid, isDirty },
} = useForm<EmailCodeFormValues>({
defaultValues: {
@ -49,14 +54,13 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
reValidateMode: "onChange",
});
const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting || errorResendingCode;
const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting;
const onSubmit = async ({ email }: EmailCodeFormValues) => {
setErrorResendingCode(false);
await authService
.emailCode({ email })
.then((res) => {
console.log(res);
setSentEmail(email);
setValue("key", res.key);
setCodeSent(true);
@ -139,12 +143,20 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
) : (
<>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-onboarding-text-100">
Lets get you prepped!
{authType === "sign-in" ? "Get on your flight deck!" : "Lets get you prepped!"}
</h1>
<p className="text-center text-sm text-onboarding-text-200 mt-3">
This whole thing will take less than two minutes.
</p>
<p className="text-center text-sm text-onboarding-text-200 mt-1">Promise!</p>
{authType == "sign-up" ? (
<div>
<p className="text-center text-sm text-onboarding-text-200 mt-3">
This whole thing will take less than two minutes.
</p>
<p className="text-center text-sm text-onboarding-text-200 mt-1">Promise!</p>
</div>
) : (
<p className="text-center text-sm text-onboarding-text-200 px-20 mt-3">
Sign in with the email you used to sign up for Plane
</p>
)}
</>
)}
@ -216,11 +228,39 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
onChange={onChange}
ref={ref}
hasError={Boolean(errors.token)}
placeholder="get-set-fly"
placeholder="gets-sets-flys"
className="border-onboarding-border-100 h-[46px] w-full"
/>
)}
/>
{resendCodeTimer <= 0 && !isResendDisabled && (
<button
type="button"
className={`flex absolute w-fit right-3.5 justify-end text-xs outline-none cursor-pointer text-custom-primary-100`}
onClick={() => {
setIsCodeResending(true);
onSubmit({ email: getValues("email") }).then(() => {
setCodeResent(true);
setIsCodeResending(false);
setResendCodeTimer(30);
});
}}
disabled={isResendDisabled}
>
<span className="font-medium">Resend</span>
</button>
)}
</div>
<div
className={`flex w-full justify-end text-xs outline-none ${
isResendDisabled ? "cursor-default text-custom-text-200" : "cursor-pointer text-custom-primary-100"
} `}
>
{resendCodeTimer > 0 ? (
<span className="text-right">Request new code in {resendCodeTimer}s</span>
) : isCodeResending ? (
"Sending new code..."
) : null}
</div>
</>
)}
@ -238,8 +278,8 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
>
{isLoading ? "Signing in..." : "Next step"}
</Button>
<div className="w-[70%] my-4 mx-auto">
<p className="text-xs text-onboarding-text-300">
<div className="w-3/4 my-4 mx-auto">
<p className="text-xs text-center text-onboarding-text-300">
When you click the button above, you agree with our{" "}
<a
href="https://plane.so/terms-and-conditions"

View File

@ -6,16 +6,18 @@ import Image from "next/image";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// images
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
import githubLightModeImage from "/public/logos/github-black.png";
import githubDarkModeImage from "/public/logos/github-dark.svg";
import { AuthType } from "components/page-views";
export interface GithubLoginButtonProps {
handleSignIn: React.Dispatch<string>;
clientId: string;
authType: AuthType;
}
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
const { handleSignIn, clientId } = props;
const { handleSignIn, clientId, authType } = props;
// states
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
const [gitCode, setGitCode] = useState<null | string>(null);
@ -24,7 +26,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
query: { code },
} = useRouter();
// theme
const { theme } = useTheme();
const { resolvedTheme } = useTheme();
useEffect(() => {
if (code && !gitCode) {
@ -37,22 +39,23 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
setLoginCallBackURL(`${origin}/` as any);
}, []);
return (
<div className="w-full flex justify-center items-center">
<Link
href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
>
<button
className={`flex w-full items-center justify-center gap-2 hover:bg-onboarding-background-300 rounded border border-onboarding-border-200 p-2 text-sm font-medium text-custom-text-100 duration-300 h-[46px]`}
className={`flex w-full items-center justify-center gap-2 hover:bg-onboarding-background-300 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 h-[42px] ${
resolvedTheme === "dark" ? "bg-[#2F3135] border-[#43484F]" : "border-[#D9E4FF]"
}`}
>
<Image
src={theme === "dark" ? githubWhiteImage : githubBlackImage}
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
<span className="text-onboarding-text-200">Sign in with GitHub</span>
<span className="text-onboarding-text-200">{authType == "sign-in" ? "Sign-in" : "Sign-up"} with GitHub</span>
</button>
</Link>
</div>

View File

@ -1,12 +1,7 @@
import React, { useEffect } from "react";
import { Avatar, DiceIcon, PhotoFilterIcon } from "@plane/ui";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// react-hook-form
import { useTheme } from "next-themes";
import Image from "next/image";
import { Control, Controller, UseFormSetValue, UseFormWatch } from "react-hook-form";
// types
import { IWorkspace } from "types";
// icons
import {
BarChart2,
Briefcase,
@ -19,7 +14,16 @@ import {
PenSquare,
Search,
Settings,
Bell,
} from "lucide-react";
import { Avatar, DiceIcon, PhotoFilterIcon } from "@plane/ui";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// types
import { IWorkspace } from "types";
// assets
import projectEmoji from "public/emoji/project-emoji.svg";
const workspaceLinks = [
{
@ -39,7 +43,7 @@ const workspaceLinks = [
name: "All Issues",
},
{
Icon: CheckCircle,
Icon: Bell,
name: "Notifications",
},
];
@ -89,22 +93,23 @@ const DummySidebar: React.FC<Props> = (props) => {
const { workspace: workspaceStore, user: userStore } = useMobxStore();
const workspace = workspaceStore.workspaces ? workspaceStore.workspaces[0] : null;
const { resolvedTheme } = useTheme();
const handleZoomWorkspace = (value: string) => {
// console.log(lastWorkspaceName,value);
if (lastWorkspaceName === value) return;
lastWorkspaceName = value;
if (timer > 0) {
timer += 2;
timer = Math.min(timer, 4);
timer = Math.min(timer, 2);
} else {
timer = 2;
timer = Math.min(timer, 4);
timer = Math.min(timer, 2);
const interval = setInterval(() => {
if (timer < 0) {
setValue!("name", lastWorkspaceName);
clearInterval(interval);
}
console.log("timer", timer);
timer--;
}, 1000);
}
@ -112,7 +117,7 @@ const DummySidebar: React.FC<Props> = (props) => {
useEffect(() => {
if (watch) {
watch();
watch("name");
}
});
@ -126,22 +131,34 @@ const DummySidebar: React.FC<Props> = (props) => {
render={({ field: { value } }) => {
if (value.length > 0) {
handleZoomWorkspace(value);
} else {
lastWorkspaceName = "";
}
return timer > 0 ? (
<div className="py-6 pl-4 top-3 mt-4 transition-all bg-onboarding-background-200 w-full max-w-screen-sm flex items-center ml-6 border-8 border-onboarding-background-100 rounded-md">
<div className="bg-onboarding-background-100 w-full p-1 flex items-center">
<div className="flex flex-shrink-0">
<Avatar
name={value.length > 0 ? value[0].toLocaleUpperCase() : "N"}
src={""}
size={30}
shape="square"
fallbackBackgroundColor="black"
className="!text-base"
/>
</div>
<div
className={`top-3 mt-4 transition-all bg-onboarding-background-200 w-full max-w-screen-sm flex items-center ml-6 border-[6px] ${
resolvedTheme == "dark" ? "border-onboarding-background-100" : "border-custom-primary-20"
} rounded-xl`}
>
<div className="border rounded-lg py-6 pl-4 w-full border-onboarding-background-400">
<div
className={`${
resolvedTheme == "light" ? "bg-[#F5F5F5]" : "bg-[#363A40]"
} w-full p-1 flex items-center`}
>
<div className="flex flex-shrink-0">
<Avatar
name={value.length > 0 ? value[0].toLocaleUpperCase() : "N"}
src={""}
size={30}
shape="square"
fallbackBackgroundColor="black"
className="!text-base"
/>
</div>
<span className="text-xl font-medium text-onboarding-text-100 ml-2 truncate">{value}</span>
<span className="text-xl font-medium text-onboarding-text-100 ml-2 truncate">{value}</span>
</div>
</div>
</div>
) : (
@ -206,7 +223,7 @@ const DummySidebar: React.FC<Props> = (props) => {
<div className={`flex items-center justify-between w-full px-1 mb-3 gap-2 mt-4 `}>
<div
className={`relative flex items-center justify-between w-full rounded gap-1 group
px-3 shadow-custom-sidebar-shadow-2xs border-[0.5px] border-custom-border-200
px-3 shadow-custom-shadow-2xs border-onboarding-border-100 border
`}
>
<div className={`relative flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 outline-none`}>
@ -217,7 +234,7 @@ const DummySidebar: React.FC<Props> = (props) => {
<div
className={`flex items-center justify-center rounded flex-shrink-0 p-2 outline-none
shadow-custom-sidebar-shadow-2xs border-[0.5px] border-onboarding-border-200
shadow-custom-shadow-2xs border border-onboarding-border-100
`}
>
<Search className="h-4 w-4 text-onboarding-text-200" />
@ -244,11 +261,15 @@ const DummySidebar: React.FC<Props> = (props) => {
<div className="px-3">
{" "}
<div className="w-4/5 flex items-center text-base font-medium text-custom-text-200 mb-3 justify-between">
<span> Plane web</span>
<div className="flex items-center gap-x-2">
<Image src={projectEmoji} alt="Plane Logo" className="h-4 w-4" />
<span> Plane</span>
</div>
<ChevronDown className="h-4 w-4" />
</div>
{projectLinks.map((link) => (
<a className="block w-full">
<a className="block ml-6 w-full">
<div
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-base font-medium outline-none
text-custom-sidebar-text-200 focus:bg-custom-sidebar-background-80

View File

@ -2,17 +2,17 @@ import React from "react";
const OnboardingStepIndicator = ({ step }: { step: number }) => (
<div className="flex items-center justify-center">
<div className="h-4 w-4 rounded-full bg-custom-primary-100 z-10" />
<div className="h-3 w-3 rounded-full bg-custom-primary-100 z-10" />
<div className={`h-1 w-14 -ml-1 ${step >= 2 ? "bg-custom-primary-100" : "bg-onboarding-background-100"}`} />
<div
className={` z-10 -ml-1 rounded-full ${
step >= 2 ? "bg-custom-primary-100 h-4 w-4" : " h-3 w-3 bg-onboarding-background-100"
step >= 2 ? "bg-custom-primary-100 h-3 w-3" : " h-2 w-2 bg-onboarding-background-100"
}`}
/>
<div className={`h-1 w-14 -ml-1 ${step >= 3 ? "bg-custom-primary-100" : "bg-onboarding-background-100"}`} />
<div
className={`rounded-full -ml-1 z-10 ${
step >= 3 ? "bg-custom-primary-100 h-4 w-4" : "h-3 w-3 bg-onboarding-background-100"
step >= 3 ? "bg-custom-primary-100 h-3 w-3" : "h-2 w-2 bg-onboarding-background-100"
}`}
/>
</div>

View File

@ -1,9 +1,11 @@
import { Fragment } from "react";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { Menu, Transition } from "@headlessui/react";
import { Cog, LogIn, LogOut, Settings, UserCircle2 } from "lucide-react";
import { mutate } from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
@ -39,6 +41,7 @@ export const InstanceSidebarDropdown = observer(() => {
} = useMobxStore();
// hooks
const { setToastAlert } = useToast();
const { setTheme } = useTheme();
// redirect url for normal mode
const redirectWorkspaceSlug =
@ -51,6 +54,8 @@ export const InstanceSidebarDropdown = observer(() => {
await authService
.signOut()
.then(() => {
mutate("CURRENT_USER_DETAILS", null);
setTheme("system");
router.push("/");
})
.catch(() =>
@ -70,13 +75,13 @@ export const InstanceSidebarDropdown = observer(() => {
sidebarCollapsed ? "justify-center" : ""
}`}
>
<div className={`flex-shrink-0 flex items-center justify-center h-6 w-6 bg-custom-sidebar-background-80 rounded`}>
<div
className={`flex-shrink-0 flex items-center justify-center h-6 w-6 bg-custom-sidebar-background-80 rounded`}
>
<Cog className="h-5 w-5 text-custom-text-200" />
</div>
{!sidebarCollapsed && (
<h4 className="text-custom-text-200 font-medium text-base truncate">Instance Admin</h4>
)}
{!sidebarCollapsed && <h4 className="text-custom-text-200 font-medium text-base truncate">Instance Admin</h4>}
</div>
</div>

View File

@ -1,5 +1,7 @@
// react
import React, { useState } from "react";
import { CheckCircle2, Search } from "lucide-react";
import useSWR, { mutate } from "swr";
import { trackEvent } from "helpers/event-tracker.helper";
// components
import { Button, Loader } from "@plane/ui";
@ -9,16 +11,12 @@ import { truncateText } from "helpers/string.helper";
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { WorkspaceService } from "services/workspace.service";
// swr
import useSWR, { mutate } from "swr";
// contants
import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
import { ROLE } from "constants/workspace";
// types
import { IWorkspaceMemberInvitation } from "types";
// icons
import { CheckCircle2, Search } from "lucide-react";
import { trackEvent } from "helpers/event-tracker.helper";
type Props = {
handleNextStep: () => void;
@ -147,11 +145,11 @@ const Invitations: React.FC<Props> = (props) => {
</div>
</div>
) : (
<EmptyInvitation email={currentUser!.email} />
<EmptyInvitation email={currentUser!.email} setTryDiffAccount={setTryDiffAccount} />
);
};
const EmptyInvitation = ({ email }: { email: string }) => (
const EmptyInvitation = ({ email, setTryDiffAccount }: { email: string; setTryDiffAccount: () => void }) => (
<div className="items-center md:w-4/5 bg-onboarding-background-300/30 my-16 border-onboarding-border-200 py-5 px-10 rounded border justify-center ">
<p className="text-lg text-onboarding-text-300 text-center font-semibold">Is your team already on Plane?</p>
<p className="text-sm text-onboarding-text-300 mt-6 text-center">
@ -159,7 +157,7 @@ const EmptyInvitation = ({ email }: { email: string }) => (
</p>
<div
className="bg-onboarding-background-200 mt-6 py-3 text-center hover:cursor-pointer text-custom-text-200 rounded-md text-sm font-medium border border-custom-border-200"
onClick={() => {}}
onClick={setTryDiffAccount}
>
Try a different email address
</div>

View File

@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from "react";
// next
import Image from "next/image";
import { useTheme } from "next-themes";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// react-hook-form
@ -24,6 +25,8 @@ import { ROLE } from "constants/workspace";
// assets
import user1 from "public/users/user-1.png";
import user2 from "public/users/user-2.png";
import userDark from "public/onboarding/user-dark.svg";
import userLight from "public/onboarding/user-light.svg";
type Props = {
finishOnboarding: () => Promise<void>;
@ -48,13 +51,15 @@ type InviteMemberFormProps = {
field: FieldArrayWithId<FormValues, "emails", "id">;
fields: FieldArrayWithId<FormValues, "emails", "id">[];
errors: any;
isInvitationDisabled: boolean;
setIsInvitationDisabled: (value: boolean) => void;
};
// services
const workspaceService = new WorkspaceService();
const InviteMemberForm: React.FC<InviteMemberFormProps> = (props) => {
const { control, index, fields, remove, errors } = props;
const { control, index, fields, remove, errors, isInvitationDisabled, setIsInvitationDisabled } = props;
const buttonRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
@ -70,7 +75,6 @@ const InviteMemberForm: React.FC<InviteMemberFormProps> = (props) => {
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",
@ -82,7 +86,32 @@ const InviteMemberForm: React.FC<InviteMemberFormProps> = (props) => {
name={`emails.${index}.email`}
type="text"
value={value}
onChange={onChange}
onChange={(event) => {
if (event.target.value === "") {
const validEmail = !fields
.filter((ele) => {
ele.id !== `emails.${index}.email`;
})
.map((ele) => ele.email)
.includes("");
if (validEmail) {
setIsInvitationDisabled(false);
} else {
setIsInvitationDisabled(true);
}
} else if (
isInvitationDisabled &&
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(event.target.value)
) {
setIsInvitationDisabled(false);
} else if (
!isInvitationDisabled &&
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(event.target.value)
) {
setIsInvitationDisabled(true);
}
onChange(event);
}}
ref={ref}
hasError={Boolean(errors.emails?.[index]?.email)}
placeholder="Enter their email..."
@ -173,7 +202,10 @@ const InviteMemberForm: React.FC<InviteMemberFormProps> = (props) => {
export const InviteMembers: React.FC<Props> = (props) => {
const { finishOnboarding, stepChange, workspace } = props;
const [isInvitationDisabled, setIsInvitationDisabled] = useState(true);
const { setToastAlert } = useToast();
const { resolvedTheme } = useTheme();
const {
control,
@ -198,7 +230,8 @@ export const InviteMembers: React.FC<Props> = (props) => {
const onSubmit = async (formData: FormValues) => {
if (!workspace) return;
const payload = { ...formData };
let payload = { ...formData };
payload = { emails: payload.emails.filter((email) => email.email !== "") };
await workspaceService
.inviteWorkspace(workspace.slug, payload)
@ -220,36 +253,41 @@ export const InviteMembers: React.FC<Props> = (props) => {
useEffect(() => {
if (fields.length === 0) {
append([
{ email: "", role: 15 },
{ email: "", role: 15 },
{ email: "", role: 15 },
]);
append(
[
{ email: "", role: 15 },
{ email: "", role: 15 },
{ email: "", role: 15 },
],
{
focusIndex: 0,
}
);
}
}, [fields, append]);
return (
<div className="flex py-14">
<div className="flex w-full py-14 ">
<div
className={`hidden lg:block w-1/4 p-3 ml-auto rounded bg-onboarding-gradient-secondary border border-onboarding-border-100 border-opacity-10`}
className={`fixed ml-16 hidden lg:block w-1/5 p-4 pb-40 h-fit rounded bg-onboarding-gradient-300 border-x border-t border-onboarding-border-300 border-opacity-10`}
>
<p className="text-base text-onboarding-text-400 font-semibold">Members</p>
{Array.from({ length: 4 }).map(() => (
<div className="flex items-center gap-2 mt-4">
<div className="w-8 h-8 flex justify-center items-center flex-shrink-0 rounded-full bg-onboarding-background-400">
<User2 className="h-4 w-4 stroke-onboarding-background-300 fill-onboarding-background-400" />
<div className="flex items-center gap-2 mt-6">
<div className="h-8 w-8 flex justify-center items-center flex-shrink-0 rounded-full">
<Image src={resolvedTheme === "dark" ? userDark : userLight} alt="user" className="object-cover" />
</div>
<div className="w-full">
<div className="rounded-md h-2.5 my-2 bg-onboarding-background-100 w-2/3" />
<div className="rounded-md h-2 bg-onboarding-background-400 w-1/2" />
<div className="rounded-md h-2.5 bg-onboarding-background-400 my-2 w-1/2" />
<div className="rounded-md h-2 bg-onboarding-background-100 w-1/3" />
</div>
</div>
))}
<div className="relative mt-20 h-32">
<div className="flex absolute bg-onboarding-background-200 p-2 rounded-full gap-x-2 border border-onboarding-border-100 w-full mt-1 -left-1/2">
<div className="w-8 h-8 flex-shrink-0 rounded-full bg-custom-primary-10">
<div className="mt-20 relative">
<div className="flex absolute bg-onboarding-background-200 p-2 rounded-full shadow-onbording-shadow-sm gap-x-2 border border-onboarding-border-100 w-full mt-1 right-24">
<div className="w-10 h-10 flex-shrink-0 rounded-full bg-custom-primary-10">
<Image src={user2} alt="user" />
</div>
<div>
@ -258,8 +296,8 @@ export const InviteMembers: React.FC<Props> = (props) => {
</div>
</div>
<div className="flex absolute bg-onboarding-background-200 p-2 rounded-full gap-x-2 border border-onboarding-border-100 -left-1/3 mt-14 w-full">
<div className="w-8 h-8 flex-shrink-0 rounded-full bg-custom-primary-10">
<div className="flex absolute bg-onboarding-background-200 p-2 rounded-full shadow-onbording-shadow-sm gap-x-2 border border-onboarding-border-100 w-full right-12 mt-16">
<div className="w-10 h-10 flex-shrink-0 rounded-full bg-custom-primary-10">
<Image src={user1} alt="user" />
</div>
<div>
@ -269,54 +307,64 @@ export const InviteMembers: React.FC<Props> = (props) => {
</div>
</div>
</div>
<form
className="px-7 sm:px-0 md:w-4/5 lg:w-1/2 mx-auto 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 justify-between items-center">
<h2 className="text-xl sm:text-2xl font-semibold">Invite your team to work with you</h2>
<OnboardingStepIndicator step={2} />
</div>
<div className="md:w-4/5 text-sm flex flex-col overflow-hidden">
<div className="space-y-3 sm:space-y-4 mb-3 h-full overflow-y-auto">
{fields.map((field, index) => (
<InviteMemberForm
control={control}
errors={errors}
field={field}
fields={fields}
index={index}
remove={remove}
key={field.id}
/>
))}
<div className="lg:w-2/3 w-full ml-auto ">
<form
className="px-7 lg:px-0 ml-auto w-full lg:w-5/6 space-y-7 sm:space-y-10 mx-auto"
onSubmit={handleSubmit(onSubmit)}
onKeyDown={(e) => {
if (e.code === "Enter") e.preventDefault();
}}
>
<div className="flex justify-between items-center">
<h2 className="text-xl sm:text-2xl font-semibold">Invite your team to work with you</h2>
<OnboardingStepIndicator step={3} />
</div>
<button
type="button"
className="flex items-center gap-2 outline-custom-primary-100 bg-transparent text-custom-primary-100 text-sm font-semibold py-2 pr-3"
onClick={appendField}
>
<Plus className="h-3 w-3" />
Add another
</button>
</div>
<div className="flex items-center gap-4">
<Button variant="primary" type="submit" disabled={!isValid} loading={isSubmitting} size="md">
{isSubmitting ? "Sending..." : "Send Invite"}
</Button>
{/* <Button variant="outline-primary" size="md" onClick={nextStep}>
<div className="xl:w-5/6 w-full text-sm">
<div className="space-y-3 sm:space-y-4 mb-3">
{fields.map((field, index) => (
<InviteMemberForm
isInvitationDisabled={isInvitationDisabled}
setIsInvitationDisabled={(value: boolean) => setIsInvitationDisabled(value)}
control={control}
errors={errors}
field={field}
fields={fields}
index={index}
remove={remove}
key={field.id}
/>
))}
</div>
<button
type="button"
className="flex items-center gap-2 outline-custom-primary-100 bg-transparent text-custom-primary-100 text-sm font-semibold py-2 pr-3"
onClick={appendField}
>
<Plus className="h-3 w-3" />
Add another
</button>
</div>
<div className="flex items-center gap-4">
<Button
variant="primary"
type="submit"
disabled={isInvitationDisabled || !isValid}
loading={isSubmitting}
size="md"
>
{isSubmitting ? "Inviting..." : "Invite members"}
</Button>
{/* <Button variant="outline-primary" size="md" onClick={nextStep}>
Copy invite link
</Button> */}
<span className="text-sm text-onboarding-text-400 hover:cursor-pointer" onClick={nextStep}>
Do this later
</span>
</div>
</form>
<span className="text-sm text-onboarding-text-400 hover:cursor-pointer" onClick={nextStep}>
Do this later
</span>
</div>
</form>
</div>
</div>
);
};

View File

@ -1,4 +1,5 @@
import React from "react";
import { Controller, useForm } from "react-hook-form";
// hooks
import useUser from "hooks/use-user";
// components
@ -8,8 +9,6 @@ import OnboardingStepIndicator from "components/account/step-indicator";
import { Workspace } from "./workspace";
// types
import { IWorkspace, TOnboardingSteps } from "types";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
type Props = {
finishOnboarding: () => Promise<void>;
@ -39,8 +38,8 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange, setTryDiffAccount
};
return (
<div className="flex h-full w-full">
<div className="hidden lg:block w-3/12">
<div className="flex w-full">
<div className="h-full fixed hidden lg:block w-1/5 max-w-[320px]">
<Controller
control={control}
name="name"
@ -55,28 +54,29 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange, setTryDiffAccount
)}
/>
</div>
<div className="w-full lg:w-1/2 md:w-4/5 md:px-0 px-7 my-16 mx-auto">
<div className="flex justify-between items-center">
<p className="font-semibold text-onboarding-text-200 text-xl sm:text-2xl">What will your workspace be?</p>
<OnboardingStepIndicator step={1} />
</div>
<Workspace
stepChange={stepChange}
user={user}
control={control}
handleSubmit={handleSubmit}
setValue={setValue}
errors={errors}
isSubmitting={isSubmitting}
/>
<div className="flex md:w-4/5 items-center my-8">
<hr className="border-onboarding-border-100 w-full" />
<p className="text-center text-sm text-custom-text-400 mx-3 flex-shrink-0">Or</p>
<hr className="border-onboarding-border-100 w-full" />
</div>
<div className="w-full">
<Invitations setTryDiffAccount={setTryDiffAccount} handleNextStep={handleNextStep} />
<div className="lg:w-2/3 w-full ml-auto ">
<div className="w-full lg:w-4/5 px-7 lg:px-0 my-16 mx-auto">
<div className="flex justify-between items-center">
<p className="font-semibold text-onboarding-text-200 text-xl sm:text-2xl">What will your workspace be?</p>
<OnboardingStepIndicator step={1} />
</div>
<Workspace
stepChange={stepChange}
user={user}
control={control}
handleSubmit={handleSubmit}
setValue={setValue}
errors={errors}
isSubmitting={isSubmitting}
/>
<div className="flex md:w-1/2 items-center my-8">
<hr className="border-onboarding-border-100 w-full" />
<p className="text-center text-sm text-custom-text-400 mx-3 flex-shrink-0">Or</p>
<hr className="border-onboarding-border-100 w-full" />
</div>
<div className="w-full">
<Invitations setTryDiffAccount={setTryDiffAccount} handleNextStep={handleNextStep} />
</div>
</div>
</div>
</div>

View File

@ -1,9 +1,8 @@
// react
import React, { useState } from "react";
// next
import Image from "next/image";
import { Controller, useForm } from "react-hook-form";
import { observer } from "mobx-react-lite";
import { Camera, User2 } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
@ -14,11 +13,11 @@ import OnboardingStepIndicator from "components/account/step-indicator";
import { IUser } from "types";
// constants
import { TIME_ZONES } from "constants/timezones";
// services
import { FileService } from "services/file.service";
// assets
import IssuesSvg from "public/onboarding/onboarding-issues.svg";
import { ImageUploadModal } from "components/core";
// icons
import { Camera, User2 } from "lucide-react";
const defaultValues: Partial<IUser> = {
first_name: "",
@ -36,14 +35,31 @@ const timeZoneOptions = TIME_ZONES.map((timeZone) => ({
content: timeZone.label,
}));
const useCases = [
"Build Products",
"Manage Feedbacks",
"Service delivery",
"Field force management",
"Code Repository Integration",
"Bug Tracking",
"Test Case Management",
"Rescource allocation",
];
const fileService = new FileService();
export const UserDetails: React.FC<Props> = observer((props) => {
const { user } = props;
const [isRemoving, setIsRemoving] = useState(false);
const [selectedUsecase, setSelectedUsecase] = useState<number | null>();
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
const { user: userStore } = useMobxStore();
const {
user: userStore,
workspace: { workspaces },
} = useMobxStore();
const workspaceName = workspaces ? workspaces[0]?.name : "New Workspace";
const {
getValues,
handleSubmit,
control,
watch,
@ -69,28 +85,28 @@ export const UserDetails: React.FC<Props> = observer((props) => {
await userStore.updateCurrentUser(payload);
};
const handleDelete = (url: string | null | undefined) => {
if (!url) return;
const useCases = [
"Build Products",
"Manage Feedbacks",
"Service delivery",
"Field force management",
"Code Repository Integration",
"Bug Tracking",
"Test Case Management",
"Rescource allocation",
];
setIsRemoving(true);
fileService.deleteUserFile(url).finally(() => {
setValue("avatar", "");
setIsRemoving(false);
});
};
return (
<div className="h-full w-full space-y-7 sm:space-y-10 overflow-y-auto flex ">
<div className="hidden lg:block w-3/12">
<DummySidebar showProject workspaceName="New Workspace" />
<div className="w-full h-full space-y-7 sm:space-y-10 overflow-y-auto flex ">
<div className="h-full fixed hidden lg:block w-1/5 max-w-[320px]">
<DummySidebar showProject workspaceName={workspaceName} />
</div>
<ImageUploadModal
isOpen={isImageUploadModalOpen}
onClose={() => setIsImageUploadModalOpen(false)}
isRemoving={isRemoving}
handleDelete={() => {}}
handleDelete={() => {
handleDelete(getValues("avatar"));
}}
onSuccess={(url) => {
setValue("avatar", url);
setIsImageUploadModalOpen(false);
@ -98,99 +114,102 @@ export const UserDetails: React.FC<Props> = observer((props) => {
value={watch("avatar") !== "" ? watch("avatar") : undefined}
userImage
/>
<div className="flex lg:w-3/5 md:w-4/5 md:px-0 px-7 mx-auto flex-col">
<form onSubmit={handleSubmit(onSubmit)} className="md:w-11/12 mx-auto">
<div className="flex justify-between items-center">
<p className="font-semibold text-xl sm:text-2xl">What do we call you? </p>
<OnboardingStepIndicator step={2} />
</div>
<div className="flex mt-5 w-full ">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!watch("avatar") || watch("avatar") === "" ? (
<div className="h-16 hover:cursor-pointer justify-center items-center flex w-16 rounded-full flex-shrink-0 mr-3 relative bg-onboarding-background-300">
<div className="h-6 w-6 flex justify-center items-center bottom-1 border border-onboarding-border-100 -right-1 bg-onboarding-background-100 rounded-full absolute">
<Camera className="h-4 w-4 stroke-onboarding-background-400" />
<div className="lg:w-2/3 w-full flex flex-col justify-between ml-auto ">
<div className="flex lg:w-4/5 md:px-0 px-7 pt-3 mx-auto flex-col">
<form onSubmit={handleSubmit(onSubmit)} className="md:w-11/12 ml-auto">
<div className="flex justify-between items-center">
<p className="font-semibold text-xl sm:text-2xl">What do we call you? </p>
<OnboardingStepIndicator step={2} />
</div>
<div className="flex mt-6 w-full ">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!watch("avatar") || watch("avatar") === "" ? (
<div className="h-16 hover:cursor-pointer justify-center items-center flex w-16 rounded-full flex-shrink-0 mr-3 relative bg-onboarding-background-300">
<div className="h-6 w-6 flex justify-center items-center bottom-1 border border-onboarding-border-100 -right-1 bg-onboarding-background-100 rounded-full absolute">
<Camera className="h-4 w-4 stroke-onboarding-background-400" />
</div>
<User2 className="h-10 w-10 stroke-onboarding-background-300 fill-onboarding-background-400" />
</div>
<User2 className="h-10 w-10 stroke-onboarding-background-300 fill-onboarding-background-400" />
</div>
) : (
<div className="relative h-16 w-16 overflow-hidden mr-3">
<img
src={watch("avatar")}
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
onClick={() => setIsImageUploadModalOpen(true)}
alt={user?.display_name}
/>
</div>
)}
</button>
) : (
<div className="relative h-16 w-16 overflow-hidden mr-3">
<img
src={watch("avatar")}
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
onClick={() => setIsImageUploadModalOpen(true)}
alt={user?.display_name}
/>
</div>
)}
</button>
<div className="my-2 bg-onboarding-background-200 w-full mr-10 rounded-md flex text-sm">
<div className="my-2 bg-onboarding-background-200 w-full mr-10 rounded-md flex text-sm">
<Controller
control={control}
name="first_name"
rules={{
required: "First name is required",
maxLength: {
value: 24,
message: "First name cannot exceed the limit of 24 characters",
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="first_name"
name="first_name"
type="text"
value={value}
autoFocus={true}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.first_name)}
placeholder="Enter your full name..."
className="w-full focus:border-custom-primary-100 border-onboarding-border-100"
/>
)}
/>
</div>
</div>
<div className="mt-14 mb-10">
<Controller
control={control}
name="first_name"
rules={{
required: "First name is required",
maxLength: {
value: 24,
message: "First name cannot exceed the limit of 24 characters",
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="first_name"
name="first_name"
type="text"
value={value}
autoFocus={true}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.first_name)}
placeholder="Enter your full name..."
className="w-full focus:border-custom-primary-100 border-onboarding-border-100"
/>
render={({ field: { value } }) => (
<p className="font-medium text-onboarding-text-200 text-xl sm:text-2xl p-0">
And how will you use Plane{value.length > 0 ? ", " : ""}
{value}?
</p>
)}
/>
<p className="font-medium text-onboarding-text-300 text-sm my-3">Choose just one</p>
<Controller
control={control}
name="use_case"
render={({ field: { value, onChange } }) => (
<div className="flex flex-wrap break-all overflow-auto">
{useCases.map((useCase) => (
<div
className={`border mb-3 hover:cursor-pointer hover:bg-onboarding-background-300/30 flex-shrink-0 ${
value === useCase ? "border-custom-primary-100" : "border-onboarding-border-100"
} mr-3 rounded-sm p-3 text-sm font-medium`}
onClick={() => onChange(useCase)}
>
{useCase}
</div>
))}
</div>
)}
/>
</div>
</div>
<div className="mt-14 mb-10">
<Controller
control={control}
name="first_name"
render={({ field: { value } }) => (
<p className="font-medium text-onboarding-text-200 text-xl sm:text-2xl p-0">
And how will you use Plane{value.length>0?", ":""}{value}?
</p>
)}
/>
<p className="font-medium text-onboarding-text-300 text-sm my-3">Choose just one</p>
<Controller
control={control}
name="use_case"
render={({ field: { value, onChange } }) => (
<div className="flex flex-wrap break-all overflow-auto">
{useCases.map((useCase) => (
<div
className={`border mb-3 hover:cursor-pointer hover:bg-onboarding-background-300/30 flex-shrink-0 ${
value === useCase ? "border-custom-primary-100" : "border-onboarding-border-100"
} mr-3 rounded-sm p-3 text-sm font-medium`}
onClick={() => onChange(useCase)}
>
{useCase}
</div>
))}
</div>
)}
/>
</div>
<Button variant="primary" type="submit" size="md" disabled={!isValid} loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Continue"}
</Button>
</form>
<div className="mt-3 flex ml-auto">
<Button variant="primary" type="submit" size="md" disabled={!isValid} loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Continue"}
</Button>
</form>
</div>
<div className="md:w-11/12 relative flex justify-end bottom-0 ml-auto">
<Image src={IssuesSvg} className="w-2/3 h-[w-2/3] object-cover" />
</div>
</div>

View File

@ -1,4 +1,5 @@
import { useState } from "react";
import { Control, Controller, FieldErrors, UseFormHandleSubmit, UseFormSetValue } from "react-hook-form";
// ui
import { Button, Input } from "@plane/ui";
// types
@ -11,8 +12,6 @@ import { WorkspaceService } from "services/workspace.service";
import { useMobxStore } from "lib/mobx/store-provider";
// constants
import { RESTRICTED_URLS } from "constants/workspace";
// react-hook-form
import { Control, Controller, FieldErrors, UseFormHandleSubmit, UseFormSetValue } from "react-hook-form";
type Props = {
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
@ -149,8 +148,12 @@ export const Workspace: React.FC<Props> = (props) => {
onChange={(e) => {
const host = window.location.host;
const slug = e.currentTarget.value.split("/");
/^[a-zA-Z0-9_-]+$/.test(slug[slug.length - 1]) ? setInvalidSlug(false) : setInvalidSlug(true);
setValue("slug", `${host}/${slug[slug.length - 1].toLocaleLowerCase().trim().replace(/ /g, "-")}`);
if (slug.length > 1) {
/^[a-zA-Z0-9_-]+$/.test(slug[slug.length - 1]) ? setInvalidSlug(false) : setInvalidSlug(true);
setValue("slug", `${host}/${slug[slug.length - 1].toLocaleLowerCase().trim().replace(/ /g, "-")}`);
} else {
setValue("slug", `${host}/`);
}
}}
ref={ref}
hasError={Boolean(errors.slug)}

View File

@ -1,7 +1,9 @@
import { useState, useEffect, useCallback } from "react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import { Lightbulb } from "lucide-react";
// hooks
import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
@ -22,12 +24,14 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import signInIssues from "public/onboarding/onboarding-issues.svg";
// types
import { IUser, IUserSettings } from "types";
// icons
import { Lightbulb } from "lucide-react";
export type AuthType = "sign-in" | "sign-up";
const authService = new AuthService();
export const SignInView = observer(() => {
// states
const [authType, setAuthType] = useState<AuthType>("sign-up");
const {
user: { fetchCurrentUser, fetchCurrentUserSettings },
appConfig: { envConfig },
@ -39,6 +43,7 @@ export const SignInView = observer(() => {
const [isLoading, setLoading] = useState(false);
// toast
const { setToastAlert } = useToast();
const { resolvedTheme } = useTheme();
// computed.
const enableEmailPassword =
@ -184,79 +189,112 @@ export const SignInView = observer(() => {
<Spinner />
</div>
) : (
<div className={`bg-onboarding-gradient-primary h-full overflow-y-auto`}>
<div className="sm:py-5 pl-8 pb-4 sm:pl-16 lg:pl-28 ">
<div className="flex text-3xl items-center mt-16 font-semibold">
<div className="h-[30px] w-[30px] mr-2">
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
</div>
Plane
<div className={`bg-onboarding-gradient-100 h-full w-full`}>
<div className="flex items-center justify-between sm:py-5 px-8 pb-4 sm:px-16 lg:px-28 ">
<div className="flex gap-x-2 py-10 items-center">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<span className="font-semibold text-2xl sm:text-3xl">Plane</span>
</div>
<div className="">
{authType === "sign-in" && (
<div className="mx-auto text-right text-onboarding-text-300 text-sm">
New to Plane?{" "}
<p
className="text-custom-primary-100 hover text-base font-medium hover:cursor-pointer"
onClick={() => {
setAuthType("sign-up");
}}
>
Create a new account
</p>
</div>
)}
</div>
</div>
<div className="md:w-2/3 sm:w-4/5 rounded-md mx-auto shadow-sm border border-custom-border-200">
<div className={`p-4`}>
<div className={`px-7 sm:px-0 bg-onboarding-gradient-secondary h-full pt-32 pb-20 rounded-md`}>
{!envConfig ? (
<div className="pt-10 mx-auto flex justify-center">
<div>
<Loader className="space-y-4 w-full pb-4 mx-auto">
<Loader.Item height="46px" width="360px" />
<Loader.Item height="46px" width="360px" />
</Loader>
<div className="h-full bg-onboarding-gradient-100 md:w-2/3 sm:w-4/5 px-4 pt-4 rounded-t-md mx-auto shadow-sm border-x border-t border-custom-border-200 ">
<div className={`px-7 sm:px-0 bg-onboarding-gradient-200 h-full pt-24 pb-56 rounded-t-md overflow-auto`}>
{!envConfig ? (
<div className="pt-10 mx-auto flex justify-center">
<div>
<Loader className="space-y-4 w-full pb-4 mx-auto">
<Loader.Item height="46px" width="360px" />
<Loader.Item height="46px" width="360px" />
</Loader>
<Loader className="space-y-4 w-full pt-4 mx-auto">
<Loader.Item height="46px" width="360px" />
<Loader.Item height="46px" width="360px" />
</Loader>
</div>
<Loader className="space-y-4 w-full pt-4 mx-auto">
<Loader.Item height="46px" width="360px" />
<Loader.Item height="46px" width="360px" />
</Loader>
</div>
) : (
</div>
) : (
<>
<>
<>
{enableEmailPassword && <EmailPasswordForm onSubmit={handlePasswordSignIn} />}
{envConfig?.magic_login && (
<div className="flex flex-col divide-y divide-custom-border-200">
<div className="pb-2">
<EmailCodeForm handleSignIn={handleEmailCodeSignIn} />
</div>
{enableEmailPassword && <EmailPasswordForm onSubmit={handlePasswordSignIn} />}
{envConfig?.magic_login && (
<div className="sm:w-96 mx-auto flex flex-col divide-y divide-custom-border-200">
<div className="pb-2">
<EmailCodeForm authType={authType} handleSignIn={handleEmailCodeSignIn} />
</div>
)}
<div className="flex sm:w-96 items-center mt-4 mx-auto">
<hr className={`border-onboarding-border-100 w-full`} />
<p className="text-center text-sm text-onboarding-text-400 mx-3 flex-shrink-0">
Or continue with
</p>
<hr className={`border-onboarding-border-100 w-full`} />
</div>
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-96 mx-auto overflow-hidden">
{envConfig?.google_client_id && (
<GoogleLoginButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
)}
{envConfig?.github_client_id && (
<GithubLoginButton clientId={envConfig?.github_client_id} handleSignIn={handleGitHubSignIn} />
)}
</div>
</>
<div className={`flex py-2 bg-onboarding-background-100 mx-auto rounded-sm sm:w-96 mt-16`}>
<Lightbulb className="h-7 w-7 mr-2 mx-3" />
<p className={`text-sm text-left text-onboarding-text-200`}>
Try the latest features, like Tiptap editor, to write compelling responses.{" "}
<span className="font-medium underline hover:cursor-pointer" onClick={() => {}}>
See new features
</span>
)}
<div className="flex sm:w-96 items-center mt-4 mx-auto">
<hr className={`border-onboarding-border-100 w-full`} />
<p className="text-center text-sm text-onboarding-text-400 mx-3 flex-shrink-0">
Or continue with
</p>
<hr className={`border-onboarding-border-100 w-full`} />
</div>
<div className="flex justify-center sm:w-96 sm:h-64 object-cover mt-8 mx-auto rounded-md ">
<Image
src={signInIssues}
alt="Plane Logo"
className={`flex object-cover rounded-md bg-onboarding-background-100`}
/>
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:flex-row sm:w-96 mx-auto overflow-hidden">
{envConfig?.google_client_id && (
<GoogleLoginButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
)}
{envConfig?.github_client_id && (
<GithubLoginButton
authType={authType}
clientId={envConfig?.github_client_id}
handleSignIn={handleGitHubSignIn}
/>
)}
</div>
{authType === "sign-up" && (
<div className="sm:w-96 text-center mx-auto mt-6 text-onboarding-text-400 text-sm">
Already using Plane?{" "}
<span
className="text-custom-primary-80 hover text-sm font-medium underline hover:cursor-pointer"
onClick={() => {
setAuthType("sign-in");
}}
>
Sign in
</span>
</div>
)}
</>
)}
</div>
<div
className={`flex py-2 bg-onboarding-background-100 border border-onboarding-border-200 mx-auto rounded-[3.5px] sm:w-96 mt-16`}
>
<Lightbulb className="h-7 w-7 mr-2 mx-3" />
<p className={`text-sm text-left text-onboarding-text-100`}>
Try the latest features, like Tiptap editor, to write compelling responses.{" "}
<span className="font-medium text-sm underline hover:cursor-pointer" onClick={() => {}}>
See new features
</span>
</p>
</div>
<div className="flex justify-center border border-onboarding-border-200 sm:w-96 sm:h-52 object-cover mt-8 mx-auto rounded-md bg-onboarding-background-100 ">
<Image
src={signInIssues}
alt="Plane Issues"
className={`flex object-cover rounded-md ${
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
} `}
/>
</div>
</>
)}
</div>
</div>
</div>

View File

@ -2,7 +2,9 @@ import { Fragment } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useTheme } from "next-themes";
import { Menu, Transition } from "@headlessui/react";
import { mutate } from "swr";
import { Check, ChevronDown, LogOut, Plus, Settings, UserCircle2 } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
@ -57,6 +59,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
} = useMobxStore();
// hooks
const { setToastAlert } = useToast();
const { setTheme } = useTheme();
const handleWorkspaceNavigation = (workspace: IWorkspace) => {
updateCurrentUser({
@ -78,6 +81,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
await authService
.signOut()
.then(() => {
mutate("CURRENT_USER_DETAILS", null);
setTheme("system");
router.push("/");
})
.catch(() =>
@ -251,7 +256,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
{!sidebarCollapsed && (
<Menu as="div" className="relative flex-shrink-0">
<Menu.Button className="grid place-items-center outline-none">
<Avatar
<Avatar
name={currentUser?.display_name}
src={currentUser?.avatar}
size={24}

View File

@ -3,29 +3,28 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// keys
import { CURRENT_USER } from "constants/fetch-keys";
// services
import { UserService } from "services/user.service";
import { WorkspaceService } from "services/workspace.service";
// types
import type { IUser } from "types";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
const userService = new UserService();
const workspaceService = new WorkspaceService();
const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "admin") => {
const router = useRouter();
const { next_url } = router.query as { next_url: string };
const { next_url } = router.query;
const [isRouteAccess, setIsRouteAccess] = useState(true);
const {
user: { fetchCurrentUser },
} = useMobxStore();
const {
data: user,
isLoading,
error,
mutate,
} = useSWR<IUser>(CURRENT_USER, () => userService.currentUser(), {
} = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
refreshInterval: 0,
shouldRetryOnError: false,
});
@ -91,7 +90,7 @@ const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "adm
if (!isLoading) {
setIsRouteAccess(() => true);
if (user) {
if (next_url) router.push(next_url);
if (next_url) router.push(next_url.toString());
else handleUserRouteAuthentication();
} else {
if (routeAuth === "sign-in") {

View File

@ -108,8 +108,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
}}
/>
{user && step !== null ? (
<div className={` bg-onboarding-gradient-primary h-full overflow-y-auto`}>
<div className="sm:py-14 py-10 px-4 sm:px-7 md:px-14 lg:pl-28 lg:pr-24 flex items-center">
<div className={`bg-onboarding-gradient-100 h-full flex flex-col fixed w-full`}>
<div className="sm:pt-14 sm:pb-8 py-10 px-4 sm:px-7 md:px-14 lg:pl-28 lg:pr-24 flex items-center">
<div className="w-full flex items-center justify-between font-semibold ">
<div className="text-3xl flex items-center gap-x-1">
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" height={30} width={30} />
@ -164,28 +164,26 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
</div>
</div>
</div>
<div className="w-full lg:w-4/5 xl:w-3/4 sm:w-4/5 rounded-md mx-auto shadow-sm border border-custom-border-200">
<div className={`bg-onboarding-gradient-primary p-4`}>
<div className={`bg-onboarding-gradient-secondary h-full rounded-md`}>
{step === 1 ? (
<JoinWorkspaces
setTryDiffAccount={() => {
setShowDeleteModal(true);
}}
finishOnboarding={finishOnboarding}
stepChange={stepChange}
/>
) : step === 2 ? (
<UserDetails user={user} />
) : (
<InviteMembers
finishOnboarding={finishOnboarding}
stepChange={stepChange}
user={user}
workspace={workspaces?.[0]}
/>
)}
</div>
<div className="w-full h-full lg:w-4/5 xl:w-3/4 bg-onboarding-gradient-100 pt-4 px-4 sm:w-4/5 rounded-t-md mx-auto shadow-sm border-x border-t border-custom-border-200 overflow-auto">
<div className={`h-full w-full bg-onboarding-gradient-200 rounded-t-md overflow-auto`}>
{step === 1 ? (
<JoinWorkspaces
setTryDiffAccount={() => {
setShowDeleteModal(true);
}}
finishOnboarding={finishOnboarding}
stepChange={stepChange}
/>
) : step === 2 ? (
<UserDetails user={user} />
) : (
<InviteMembers
finishOnboarding={finishOnboarding}
stepChange={stepChange}
user={user}
workspace={workspaces?.[0]}
/>
)}
</div>
</div>
</div>

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5554 7.55556C15.5554 11.7284 9.67856 16 7.99989 16C6.32078 16 0.444336 11.7284 0.444336 7.55556C0.444336 3.38267 3.827 0 7.99989 0C12.1728 0 15.5554 3.38267 15.5554 7.55556Z" fill="#CCD6DD"/>
<path d="M6.00138 6.5978C7.38982 7.98625 7.95249 9.67469 7.25827 10.3689C6.56405 11.0631 4.8756 10.5009 3.48716 9.11202C2.09827 7.72358 1.5356 6.03514 2.23027 5.34091C2.92493 4.64669 4.61293 5.20936 6.00138 6.5978ZM9.99871 6.5978C8.61027 7.98625 8.0476 9.67469 8.74182 10.3689C9.43605 11.0631 11.1245 10.5009 12.5129 9.11202C13.9018 7.72314 14.4645 6.03514 13.7698 5.34091C13.0756 4.64669 11.3872 5.20891 9.99871 6.5978ZM8.00005 13.7778C6.95693 13.7778 6.41116 13.2622 6.35249 13.2031C6.26856 13.1204 6.22095 13.0077 6.22011 12.8898C6.21928 12.772 6.2653 12.6586 6.34805 12.5747C6.43079 12.4908 6.54349 12.4431 6.66135 12.4423C6.7792 12.4415 6.89256 12.4875 6.97649 12.5702C6.99916 12.5911 7.33471 12.8889 8.00005 12.8889C8.67516 12.8889 9.01027 12.5827 9.02449 12.5694C9.10979 12.4893 9.22304 12.4458 9.34001 12.4482C9.45697 12.4506 9.56834 12.4987 9.65027 12.5822C9.73156 12.6652 9.77686 12.7769 9.77636 12.893C9.77586 13.0092 9.7296 13.1204 9.6476 13.2027C9.58893 13.2622 9.04316 13.7778 8.00005 13.7778Z" fill="#292F33"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 0.890625C8.35093 0.890625 7.21312 1.11695 6.15152 1.55668C5.08992 1.99641 4.12533 2.64093 3.31282 3.45344C1.67187 5.09438 0.75 7.31998 0.75 9.64062C0.75 13.5081 3.26125 16.7894 6.735 17.9531C7.1725 18.0231 7.3125 17.7519 7.3125 17.5156V16.0369C4.88875 16.5619 4.3725 14.8644 4.3725 14.8644C3.97 13.8494 3.40125 13.5781 3.40125 13.5781C2.605 13.0356 3.4625 13.0531 3.4625 13.0531C4.3375 13.1144 4.80125 13.9544 4.80125 13.9544C5.5625 15.2844 6.84875 14.8906 7.3475 14.6806C7.42625 14.1119 7.65375 13.7269 7.89875 13.5081C5.95625 13.2894 3.9175 12.5369 3.9175 9.20312C3.9175 8.23187 4.25 7.45312 4.81875 6.83187C4.73125 6.61312 4.425 5.70312 4.90625 4.52188C4.90625 4.52188 5.64125 4.28562 7.3125 5.41438C8.00375 5.22188 8.75625 5.12563 9.5 5.12563C10.2437 5.12563 10.9963 5.22188 11.6875 5.41438C13.3588 4.28562 14.0938 4.52188 14.0938 4.52188C14.575 5.70312 14.2688 6.61312 14.1813 6.83187C14.75 7.45312 15.0825 8.23187 15.0825 9.20312C15.0825 12.5456 13.035 13.2806 11.0838 13.4994C11.3988 13.7706 11.6875 14.3044 11.6875 15.1181V17.5156C11.6875 17.7519 11.8275 18.0319 12.2738 17.9531C15.7475 16.7806 18.25 13.5081 18.25 9.64062C18.25 8.49156 18.0237 7.35374 17.5839 6.29214C17.1442 5.23055 16.4997 4.26595 15.6872 3.45344C14.8747 2.64093 13.9101 1.99641 12.8485 1.55668C11.7869 1.11695 10.6491 0.890625 9.5 0.890625Z" fill="#B0B4BB"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,14 @@
<svg width="30" height="29" viewBox="0 0 30 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_422_19191)">
<rect width="30" height="29" rx="14.5" fill="#5B6069"/>
<path d="M24.6255 24.1658C24.6255 21.8239 23.4282 19.578 21.6591 17.9221C19.8899 16.2662 17.4904 15.3359 14.9884 15.3359C12.4864 15.3359 10.0869 16.2662 8.31776 17.9221C6.5486 19.578 5.55469 21.8239 5.55469 24.1658" fill="#43484F"/>
<path d="M24.6255 24.1658C24.6255 21.8239 23.4282 19.578 21.6591 17.9221C19.8899 16.2662 17.4904 15.3359 14.9884 15.3359C12.4864 15.3359 10.0869 16.2662 8.31776 17.9221C6.5486 19.578 5.55469 21.8239 5.55469 24.1658C6.80126 28.8931 22.7194 29.9478 24.6255 24.1658Z" stroke="#43484F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24.5002 23.2546C24.799 24.4372 23.5063 25.9588 21.7371 26.9349C19.9679 27.911 17.8578 28.3064 15.3558 28.3064C12.8538 28.3064 10.4543 27.758 8.68516 26.7819C6.85637 26.0934 5.00781 24.3616 5.92208 23.1016L15.3558 23.1016L24.5002 23.2546Z" fill="#43484F"/>
<path d="M15.0029 14.9375C18.1095 14.9375 20.6279 12.4191 20.6279 9.3125C20.6279 6.2059 18.1095 3.6875 15.0029 3.6875C11.8963 3.6875 9.37793 6.2059 9.37793 9.3125C9.37793 12.4191 11.8963 14.9375 15.0029 14.9375Z" fill="#43484F"/>
</g>
<defs>
<clipPath id="clip0_422_19191">
<rect width="30" height="29" rx="14.5" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,14 @@
<svg width="30" height="29" viewBox="0 0 30 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_11_12473)">
<rect width="30" height="29" rx="14.5" fill="#ECF1FF"/>
<path d="M24.6255 24.1658C24.6255 21.8239 23.4282 19.578 21.6591 17.9221C19.8899 16.2662 17.4904 15.3359 14.9884 15.3359C12.4864 15.3359 10.0869 16.2662 8.31776 17.9221C6.5486 19.578 5.55469 21.8239 5.55469 24.1658" fill="#C2D0F2"/>
<path d="M24.6255 24.1658C24.6255 21.8239 23.4282 19.578 21.6591 17.9221C19.8899 16.2662 17.4904 15.3359 14.9884 15.3359C12.4864 15.3359 10.0869 16.2662 8.31776 17.9221C6.5486 19.578 5.55469 21.8239 5.55469 24.1658C6.80126 28.8931 22.7194 29.9478 24.6255 24.1658Z" stroke="#C2D0F2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24.5002 23.2546C24.799 24.4372 23.5063 25.9588 21.7371 26.9349C19.9679 27.911 17.8578 28.3064 15.3558 28.3064C12.8538 28.3064 10.4543 27.758 8.68516 26.7819C6.85637 26.0934 5.00781 24.3616 5.92208 23.1016L15.3558 23.1016L24.5002 23.2546Z" fill="#C2D0F2"/>
<path d="M15.0029 14.9375C18.1095 14.9375 20.6279 12.4191 20.6279 9.3125C20.6279 6.2059 18.1095 3.6875 15.0029 3.6875C11.8963 3.6875 9.37793 6.2059 9.37793 9.3125C9.37793 12.4191 11.8963 14.9375 15.0029 14.9375Z" fill="#CAD9FF"/>
</g>
<defs>
<clipPath id="clip0_11_12473">
<rect width="30" height="29" rx="14.5" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -127,8 +127,10 @@
--color-border-400: 185, 185, 185; /* strong border- 2 */
/* onboarding colors */
--gradient-onboarding-primary: linear-gradient(106deg, #F2F6FF 29.8%, #E1EAFF 99.34%);
--gradient-onboarding-secondary: linear-gradient(129deg, rgba(255, 255, 255, 0.00) -22.23%, rgba(255, 255, 255, 0.80) 62.98%);
--gradient-onboarding-100: linear-gradient(106deg, #F2F6FF 29.8%, #E1EAFF 99.34%);
--gradient-onboarding-200: linear-gradient(129deg, rgba(255, 255, 255, 0.00) -22.23%, rgba(255, 255, 255, 0.80) 62.98%);
--gradient-onboarding-300: linear-gradient(164deg, #FFF 4.25%, rgba(255, 255, 255, 0.06) 93.5%);
--gradient-onboarding-400: linear-gradient(129deg, rgba(255, 255, 255, 0.00) -22.23%, rgba(255, 255, 255, 0.80) 62.98%);
--color-onboarding-text-100: 23, 23, 23;
--color-onboarding-text-200: 58, 58, 58;
@ -142,6 +144,9 @@
--color-onboarding-border-100: 229, 229, 229;
--color-onboarding-border-200: 217, 228, 255;
--color-onboarding-border-300: 229, 229, 229, 0.5;
--color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(126, 139, 171, 0.10);
}
@ -194,8 +199,9 @@
/* onboarding colors */
--gradient-onboarding-primary: linear-gradient(106deg, #18191B 25.17%, #18191B 99.34%);
--gradient-onboarding-secondary: linear-gradient(129deg, rgba(47, 49, 53, 0.80) -22.23%, rgba(33, 34, 37, 0.80) 62.98%);
--gradient-onboarding-100: linear-gradient(106deg, #18191B 25.17%, #18191B 99.34%);
--gradient-onboarding-200: linear-gradient(129deg, rgba(47, 49, 53, 0.80) -22.23%, rgba(33, 34, 37, 0.80) 62.98%);
--gradient-onboarding-300: linear-gradient(167deg, rgba(47, 49, 53, 0.45) 19.22%, #212225 98.48%);
--color-onboarding-text-100: 237, 238, 240;
--color-onboarding-text-200: 176, 180, 187;
@ -209,6 +215,10 @@
--color-onboarding-border-100: 54, 58, 64;
--color-onboarding-border-200: 54, 58, 64;
--color-onboarding-border-300: 34, 35, 38, 0.5;
--color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(39, 44, 56, 0.10);
}
[data-theme="dark-contrast"] {