diff --git a/web/components/account/delete-account-modal.tsx b/web/components/account/delete-account-modal.tsx new file mode 100644 index 000000000..ac9c9c375 --- /dev/null +++ b/web/components/account/delete-account-modal.tsx @@ -0,0 +1,112 @@ +// react +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 +import { AuthService } from "services/auth.service"; +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// icons +import { AlertTriangle } from "lucide-react"; + +type Props = { + isOpen: boolean; + onClose: () => void; + heading: string; +}; + +const authService = new AuthService(); + +const DeleteAccountModal: React.FC = (props) => { + const { isOpen, onClose, heading } = props; + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const router = useRouter(); + const { setToastAlert } = useToast(); + + const handleSignOut = async () => { + await authService + .signOut() + .then(() => { + router.push("/"); + }) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Failed to sign out. Please try again.", + }) + ); + }; + + const handleClose = () => { + onClose(); + }; + + return ( + + + +
+ + +
+
+ + +
+
+
+
+
+ + {heading} + +
+ +
+

+

  • Delete this account if you have another and won’t use this account.
  • +
  • Switch to another account if you’d like to come back to this account another time.
  • +

    +
    +
    +
    +
    + + Switch account + + +
    +
    +
    +
    +
    +
    +
    + ); +}; + +export default DeleteAccountModal; diff --git a/web/components/account/email-code-form.tsx b/web/components/account/email-code-form.tsx index 05bc89abd..a5250741e 100644 --- a/web/components/account/email-code-form.tsx +++ b/web/components/account/email-code-form.tsx @@ -7,6 +7,8 @@ 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"; // types type EmailCodeFormValues = { @@ -23,6 +25,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => { const [isCodeResending, setIsCodeResending] = useState(false); const [errorResendingCode, setErrorResendingCode] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [sentEmail, setSentEmail] = useState(""); const { setToastAlert } = useToast(); const { timer: resendCodeTimer, setTimer: setResendCodeTimer } = useTimer(); @@ -52,6 +55,8 @@ export const EmailCodeForm = ({ handleSignIn }: any) => { await authService .emailCode({ email }) .then((res) => { + console.log(res); + setSentEmail(email); setValue("key", res.key); setCodeSent(true); }) @@ -99,29 +104,54 @@ export const EmailCodeForm = ({ handleSignIn }: any) => { handleSubmit(onSubmit)().then(() => { setResendCodeTimer(30); }); + } else if ( + codeSent && + sentEmail != getValues("email") && + getValues("email").length > 0 && + (e.key === "Enter" || e.key === "Tab") + ) { + e.preventDefault(); + console.log("resend"); + onSubmit({ email: getValues("email") }).then(() => { + setCodeResent(true); + }); } }; - if (!codeSent) { - window.addEventListener("keydown", submitForm); - } + window.addEventListener("keydown", submitForm); return () => { window.removeEventListener("keydown", submitForm); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [handleSubmit, codeSent]); + }, [handleSubmit, codeSent, sentEmail]); return ( <> - {(codeSent || codeResent) && ( -

    - We have sent the sign in code. -
    - Please check your inbox at {watch("email")} -

    + {codeSent || codeResent ? ( + <> +

    + Moving to the runway +

    +
    +

    Paste the code you got at

    + {sentEmail} + below. +
    + + ) : ( + <> +

    + Let’s get you prepped! +

    +

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

    +

    Promise!

    + )} -
    + +
    { ) || "Email address is not valid", }} render={({ field: { value, onChange, ref } }) => ( - +
    + + {value.length > 0 && ( + setValue("email", "")} + /> + )} +
    )} />
    {codeSent && ( <> +
    + {codeResent && sentEmail === getValues("email") ? ( +
    + You got a new code at {sentEmail}. +
    + ) : sentEmail != getValues("email") && getValues("email").length > 0 ? ( +
    + Hit enter + or Tab to get a new code +
    + ) : ( +
    + )} +
    + { onChange={onChange} ref={ref} hasError={Boolean(errors.token)} - placeholder="Enter code..." - className="border-custom-border-300 h-[46px] w-full" + placeholder="get-set-fly" + className="border-custom-border-300 bg-white h-[46px] w-full" /> )} /> - )} {codeSent ? ( - +
    + {" "} + +
    +

    + When you click the button above, you agree with our{" "} + + terms and conditions of service. + {" "} +

    +
    +
    ) : ( )} diff --git a/web/components/account/github-login-button.tsx b/web/components/account/github-login-button.tsx index fc140f632..5aac56f89 100644 --- a/web/components/account/github-login-button.tsx +++ b/web/components/account/github-login-button.tsx @@ -1,4 +1,6 @@ +// react import { useEffect, useState, FC } from "react"; +// next import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/router"; @@ -41,7 +43,7 @@ export const GithubLoginButton: FC = (props) => { - -
    diff --git a/web/components/onboarding/invitations.tsx b/web/components/onboarding/invitations.tsx new file mode 100644 index 000000000..a2b817c1c --- /dev/null +++ b/web/components/onboarding/invitations.tsx @@ -0,0 +1,115 @@ +// react +import React, { useState } from "react"; +// components +import { Button } from "@plane/ui"; + +// helpers +import { truncateText } from "helpers/string.helper"; +// mobx +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 } from "lucide-react"; + +type Props = { + handleNextStep: () => void; + updateLastWorkspace: () => void; +}; +const workspaceService = new WorkspaceService(); + +const Invitations: React.FC = (props) => { + const { handleNextStep, updateLastWorkspace } = props; + const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); + const [invitationsRespond, setInvitationsRespond] = useState([]); + + const { workspace: workspaceStore } = useMobxStore(); + + const { data: invitations, mutate: mutateInvitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => + workspaceService.userWorkspaceInvitations() + ); + + const handleInvitation = (workspace_invitation: IWorkspaceMemberInvitation, action: "accepted" | "withdraw") => { + if (action === "accepted") { + setInvitationsRespond((prevData) => [...prevData, workspace_invitation.id]); + } else if (action === "withdraw") { + setInvitationsRespond((prevData) => prevData.filter((item: string) => item !== workspace_invitation.id)); + } + }; + + const submitInvitations = async () => { + if (invitationsRespond.length <= 0) return; + + setIsJoiningWorkspaces(true); + + await workspaceService + .joinWorkspaces({ invitations: invitationsRespond }) + .then(async () => { + await mutateInvitations(); + await workspaceStore.fetchWorkspaces(); + await mutate(USER_WORKSPACES); + await updateLastWorkspace(); + await handleNextStep(); + }) + .finally(() => setIsJoiningWorkspaces(false)); + }; + + return ( +
    +

    Choose a workspace to join

    +
    + {invitations && + invitations.map((invitation) => { + const isSelected = invitationsRespond.includes(invitation.id); + return ( +
    handleInvitation(invitation, isSelected ? "withdraw" : "accepted")} + > +
    +
    + {invitation.workspace.logo && invitation.workspace.logo !== "" ? ( + {invitation.workspace.name} + ) : ( + + {invitation.workspace.name[0]} + + )} +
    +
    +
    +
    {truncateText(invitation.workspace.name, 30)}
    +

    {ROLE[invitation.role]}

    +
    + + + +
    + ); + })} +
    + + +
    + ); +}; + +export default Invitations; diff --git a/web/components/onboarding/invite-members.tsx b/web/components/onboarding/invite-members.tsx index 08617ca71..af0342a44 100644 --- a/web/components/onboarding/invite-members.tsx +++ b/web/components/onboarding/invite-members.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useRef, useState } from "react"; - // headless ui import { Listbox, Transition } from "@headlessui/react"; // react-hook-form @@ -13,11 +12,12 @@ import { Button, Input } from "@plane/ui"; // hooks import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; // icons -import { Check, ChevronDown, Plus, X } from "lucide-react"; +import { Check, ChevronDown, Plus, X, XCircle } from "lucide-react"; // types import { IUser, IWorkspace, TOnboardingSteps, TUserWorkspaceRole } from "types"; // constants import { ROLE } from "constants/workspace"; +import OnboardingStepIndicator from "components/account/step-indicator"; type Props = { finishOnboarding: () => Promise; @@ -80,7 +80,7 @@ const InviteMemberForm: React.FC = (props) => { ref={ref} hasError={Boolean(errors.emails?.[index]?.email)} placeholder="Enter their email..." - className="text-xs sm:text-sm w-full" + className="text-xs sm:text-sm w-full h-11 placeholder:text-custom-text-400/50" /> )} /> @@ -104,10 +104,11 @@ const InviteMemberForm: React.FC = (props) => { type="button" ref={buttonRef} onClick={() => setIsDropdownOpen((prev) => !prev)} - className="flex items-center px-2.5 py-2 text-xs justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none" + className="flex items-center px-2.5 h-11 py-2 text-xs justify-between gap-1 w-full rounded-md border border-custom-border-200 duration-300" > {ROLE[value]} -