From 3c89ef8cc3bbbd738f2d29ee836a21cab3f501e8 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:45:00 +0530 Subject: [PATCH] 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 --- .../tailwind-config-custom/tailwind.config.js | 8 +- .../account/delete-account-modal.tsx | 14 +- web/components/account/email-code-form.tsx | 72 ++++-- .../account/github-login-button.tsx | 19 +- web/components/account/sidebar.tsx | 79 +++--- web/components/account/step-indicator.tsx | 6 +- web/components/instance/sidebar-dropdown.tsx | 13 +- web/components/onboarding/invitations.tsx | 16 +- web/components/onboarding/invite-members.tsx | 178 +++++++++----- web/components/onboarding/join-workspaces.tsx | 52 ++-- web/components/onboarding/user-details.tsx | 227 ++++++++++-------- web/components/onboarding/workspace.tsx | 11 +- web/components/page-views/signin.tsx | 164 ++++++++----- web/components/workspace/sidebar-dropdown.tsx | 7 +- web/hooks/use-user-auth.tsx | 17 +- web/pages/onboarding/index.tsx | 46 ++-- web/public/emoji/project-emoji.svg | 4 + web/public/logos/github-dark.svg | 3 + web/public/onboarding/user-dark.svg | 14 ++ web/public/onboarding/user-light.svg | 14 ++ web/styles/globals.css | 18 +- 21 files changed, 607 insertions(+), 375 deletions(-) create mode 100644 web/public/emoji/project-emoji.svg create mode 100644 web/public/logos/github-dark.svg create mode 100644 web/public/onboarding/user-dark.svg create mode 100644 web/public/onboarding/user-light.svg diff --git a/packages/tailwind-config-custom/tailwind.config.js b/packages/tailwind-config-custom/tailwind.config.js index 4c7ebf963..97f7cab84 100644 --- a/packages/tailwind-config-custom/tailwind.config.js +++ b/packages/tailwind-config-custom/tailwind.config.js @@ -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: { diff --git a/web/components/account/delete-account-modal.tsx b/web/components/account/delete-account-modal.tsx index 844c3837e..41533d69c 100644 --- a/web/components/account/delete-account-modal.tsx +++ b/web/components/account/delete-account-modal.tsx @@ -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) => { 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) => { 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) => {
-
Not the right workspace? diff --git a/web/components/account/email-code-form.tsx b/web/components/account/email-code-form.tsx index 560306c3b..ffaca2e6b 100644 --- a/web/components/account/email-code-form.tsx +++ b/web/components/account/email-code-form.tsx @@ -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) => { + 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({ 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) => { ) : ( <>

- Let’s get you prepped! + {authType === "sign-in" ? "Get on your flight deck!" : "Let’s get you prepped!"}

-

- This whole thing will take less than two minutes. -

-

Promise!

+ {authType == "sign-up" ? ( +
+

+ This whole thing will take less than two minutes. +

+

Promise!

+
+ ) : ( +

+ Sign in with the email you used to sign up for Plane +

+ )} )} @@ -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 && ( + + )} +
+
+ {resendCodeTimer > 0 ? ( + Request new code in {resendCodeTimer}s + ) : isCodeResending ? ( + "Sending new code..." + ) : null}
)} @@ -238,8 +278,8 @@ export const EmailCodeForm = ({ handleSignIn }: any) => { > {isLoading ? "Signing in..." : "Next step"} -
-

+

+

When you click the button above, you agree with our{" "} ; clientId: string; + authType: AuthType; } export const GithubLoginButton: FC = (props) => { - const { handleSignIn, clientId } = props; + const { handleSignIn, clientId, authType } = props; // states const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); const [gitCode, setGitCode] = useState(null); @@ -24,7 +26,7 @@ export const GithubLoginButton: FC = (props) => { query: { code }, } = useRouter(); // theme - const { theme } = useTheme(); + const { resolvedTheme } = useTheme(); useEffect(() => { if (code && !gitCode) { @@ -37,22 +39,23 @@ export const GithubLoginButton: FC = (props) => { const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; setLoginCallBackURL(`${origin}/` as any); }, []); - return (

diff --git a/web/components/account/sidebar.tsx b/web/components/account/sidebar.tsx index 284719176..3604df485 100644 --- a/web/components/account/sidebar.tsx +++ b/web/components/account/sidebar.tsx @@ -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) => { 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) => { useEffect(() => { if (watch) { - watch(); + watch("name"); } }); @@ -126,22 +131,34 @@ const DummySidebar: React.FC = (props) => { render={({ field: { value } }) => { if (value.length > 0) { handleZoomWorkspace(value); + } else { + lastWorkspaceName = ""; } return timer > 0 ? ( -
-
-
- 0 ? value[0].toLocaleUpperCase() : "N"} - src={""} - size={30} - shape="square" - fallbackBackgroundColor="black" - className="!text-base" - /> -
+
+
+
+
+ 0 ? value[0].toLocaleUpperCase() : "N"} + src={""} + size={30} + shape="square" + fallbackBackgroundColor="black" + className="!text-base" + /> +
- {value} + {value} +
) : ( @@ -206,7 +223,7 @@ const DummySidebar: React.FC = (props) => {
@@ -217,7 +234,7 @@ const DummySidebar: React.FC = (props) => {
@@ -244,11 +261,15 @@ const DummySidebar: React.FC = (props) => {
{" "}
- Plane web +
+ Plane Logo + Plane +
+
{projectLinks.map((link) => ( -
+
(
-
+
= 2 ? "bg-custom-primary-100" : "bg-onboarding-background-100"}`} />
= 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" }`} />
= 3 ? "bg-custom-primary-100" : "bg-onboarding-background-100"}`} />
= 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" }`} />
diff --git a/web/components/instance/sidebar-dropdown.tsx b/web/components/instance/sidebar-dropdown.tsx index 94575b5e9..8989dc698 100644 --- a/web/components/instance/sidebar-dropdown.tsx +++ b/web/components/instance/sidebar-dropdown.tsx @@ -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" : "" }`} > -
+
- {!sidebarCollapsed && ( -

Instance Admin

- )} + {!sidebarCollapsed &&

Instance Admin

}
diff --git a/web/components/onboarding/invitations.tsx b/web/components/onboarding/invitations.tsx index c5a733ae4..3de4b59c7 100644 --- a/web/components/onboarding/invitations.tsx +++ b/web/components/onboarding/invitations.tsx @@ -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) => {
) : ( - + ); }; -const EmptyInvitation = ({ email }: { email: string }) => ( +const EmptyInvitation = ({ email, setTryDiffAccount }: { email: string; setTryDiffAccount: () => void }) => (

Is your team already on Plane?

@@ -159,7 +157,7 @@ const EmptyInvitation = ({ email }: { email: string }) => (

{}} + onClick={setTryDiffAccount} > Try a different email address
diff --git a/web/components/onboarding/invite-members.tsx b/web/components/onboarding/invite-members.tsx index db7e5d0ba..bb1d6b84c 100644 --- a/web/components/onboarding/invite-members.tsx +++ b/web/components/onboarding/invite-members.tsx @@ -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; @@ -48,13 +51,15 @@ type InviteMemberFormProps = { field: FieldArrayWithId; fields: FieldArrayWithId[]; errors: any; + isInvitationDisabled: boolean; + setIsInvitationDisabled: (value: boolean) => void; }; // services const workspaceService = new WorkspaceService(); const InviteMemberForm: React.FC = (props) => { - const { control, index, fields, remove, errors } = props; + const { control, index, fields, remove, errors, isInvitationDisabled, setIsInvitationDisabled } = props; const buttonRef = useRef(null); const dropdownRef = useRef(null); @@ -70,7 +75,6 @@ const InviteMemberForm: React.FC = (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 = (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 = (props) => { export const InviteMembers: React.FC = (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) => { 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) => { 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 ( -
+