From 96399c7112573427cb7ac8489d413c67e28305f2 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Wed, 30 Aug 2023 12:49:15 +0530 Subject: [PATCH] feat: public board (#2017) * feat: filters in plane deploy implemented multi-level dropdown for plane deploy * style: spacing and fonts * feat: plane deploy implemented authentication/theming, created/modified all the required store & services * devL reactions, voting, comments and theme --- .../app/(auth)/components/email-code-form.tsx | 216 +++ .../(auth)/components/email-password-form.tsx | 116 ++ .../components/email-reset-password-form.tsx | 89 ++ .../(auth)/components/github-login-button.tsx | 57 + .../app/(auth)/components/google-login.tsx | 59 + .../[project_slug]/layout.tsx | 12 +- .../[workspace_slug]/[project_slug]/page.tsx | 87 +- apps/space/app/layout.tsx | 17 +- .../onboarding/components/user-details.tsx | 200 +++ apps/space/app/onboarding/page.tsx | 62 + apps/space/app/page.tsx | 172 ++- .../issues/board-views/block-due-date.tsx | 2 +- .../issues/board-views/block-labels.tsx | 10 +- .../issues/board-views/block-priority.tsx | 4 +- .../issues/board-views/block-state.tsx | 10 +- .../issues/board-views/kanban/block.tsx | 21 +- .../issues/board-views/kanban/header.tsx | 6 +- .../issues/board-views/kanban/index.tsx | 6 +- .../issues/board-views/list/block.tsx | 71 +- .../issues/board-views/list/header.tsx | 8 +- .../issues/board-views/list/index.tsx | 10 +- .../components/issues/filters-render/date.tsx | 38 - .../issues/filters-render/index.tsx | 52 +- .../label/filter-label-block.tsx | 36 +- .../issues/filters-render/label/index.tsx | 31 +- .../priority/filter-priority-block.tsx | 29 +- .../issues/filters-render/priority/index.tsx | 38 +- .../state/filter-state-block.tsx | 30 +- .../issues/filters-render/state/index.tsx | 31 +- apps/space/components/issues/navbar/index.tsx | 18 +- .../issues/navbar/issue-board-view.tsx | 24 +- .../components/issues/navbar/issue-filter.tsx | 108 +- apps/space/components/issues/navbar/theme.tsx | 18 +- .../issues/peek-overview/add-comment.tsx | 126 ++ .../peek-overview/comment-detail-card.tsx | 228 +++ .../peek-overview/full-screen-peek-view.tsx | 70 + .../issues/peek-overview/header.tsx | 99 ++ .../components/issues/peek-overview/index.ts | 12 + .../issues/peek-overview/issue-activity.tsx | 42 + .../issues/peek-overview/issue-details.tsx | 18 + .../peek-overview/issue-emoji-reactions.tsx | 94 ++ .../issues/peek-overview/issue-properties.tsx | 149 ++ .../issues/peek-overview/issue-reaction.tsx | 18 + .../peek-overview/issue-vote-reactions.tsx | 102 ++ .../issues/peek-overview/layout.tsx | 98 ++ .../issues/peek-overview/side-peek-view.tsx | 67 + .../components/tiptap/bubble-menu/index.tsx | 119 ++ .../tiptap/bubble-menu/link-selector.tsx | 90 ++ .../tiptap/bubble-menu/node-selector.tsx | 130 ++ .../bubble-menu/utils/link-validator.tsx | 12 + .../tiptap/extensions/image-resize.tsx | 57 + .../components/tiptap/extensions/index.tsx | 137 ++ .../tiptap/extensions/updated-image.tsx | 22 + apps/space/components/tiptap/index.tsx | 100 ++ .../tiptap/plugins/delete-image.tsx | 56 + .../tiptap/plugins/upload-image.tsx | 127 ++ apps/space/components/tiptap/props.tsx | 56 + .../components/tiptap/slash-command/index.tsx | 339 +++++ apps/space/components/tiptap/utils.ts | 6 + apps/space/components/ui/dropdown.tsx | 149 ++ apps/space/components/ui/icon.tsx | 10 + apps/space/components/ui/index.ts | 6 + apps/space/components/ui/input.tsx | 37 + apps/space/components/ui/primary-button.tsx | 35 + .../space/components/ui/reaction-selector.tsx | 77 + apps/space/components/ui/secondary-button.tsx | 35 + apps/space/components/ui/toast-alert.tsx | 67 + apps/space/constants/data.ts | 20 +- apps/space/constants/workspace.ts | 12 + apps/space/contexts/toast.context.tsx | 97 ++ apps/space/helpers/date-time.helper.ts | 14 + apps/space/helpers/emoji.helper.tsx | 56 + apps/space/helpers/string.helper.ts | 31 + apps/space/hooks/use-outside-click.tsx | 21 + apps/space/hooks/use-timer.tsx | 19 + apps/space/hooks/use-toast.tsx | 9 + apps/space/lib/mobx/store-init.tsx | 17 + apps/space/package.json | 2 + apps/space/public/logos/github-black.png | Bin 0 -> 14032 bytes apps/space/public/logos/github-square.png | Bin 0 -> 2352 bytes apps/space/public/logos/github-white.png | Bin 0 -> 16559 bytes .../black-horizontal-with-blue-logo.svg | 17 + .../public/plane-logos/blue-without-text.png | Bin 0 -> 2460 bytes .../white-horizontal-with-blue-logo.svg | 17 + .../public/plane-logos/white-horizontal.svg | 17 + apps/space/services/api.service.ts | 10 + apps/space/services/authentication.service.ts | 92 ++ apps/space/services/file.service.ts | 101 ++ apps/space/services/issue.service.ts | 131 +- apps/space/services/user.service.ts | 8 + apps/space/store/issue.ts | 458 +++++- apps/space/store/root.ts | 6 +- apps/space/store/types/issue.ts | 117 +- apps/space/store/user.ts | 36 +- apps/space/styles/globals.css | 145 ++ apps/space/tailwind.config.js | 172 ++- yarn.lock | 1279 ++++++++++------- 97 files changed, 6578 insertions(+), 804 deletions(-) create mode 100644 apps/space/app/(auth)/components/email-code-form.tsx create mode 100644 apps/space/app/(auth)/components/email-password-form.tsx create mode 100644 apps/space/app/(auth)/components/email-reset-password-form.tsx create mode 100644 apps/space/app/(auth)/components/github-login-button.tsx create mode 100644 apps/space/app/(auth)/components/google-login.tsx create mode 100644 apps/space/app/onboarding/components/user-details.tsx create mode 100644 apps/space/app/onboarding/page.tsx delete mode 100644 apps/space/components/issues/filters-render/date.tsx create mode 100644 apps/space/components/issues/peek-overview/add-comment.tsx create mode 100644 apps/space/components/issues/peek-overview/comment-detail-card.tsx create mode 100644 apps/space/components/issues/peek-overview/full-screen-peek-view.tsx create mode 100644 apps/space/components/issues/peek-overview/header.tsx create mode 100644 apps/space/components/issues/peek-overview/index.ts create mode 100644 apps/space/components/issues/peek-overview/issue-activity.tsx create mode 100644 apps/space/components/issues/peek-overview/issue-details.tsx create mode 100644 apps/space/components/issues/peek-overview/issue-emoji-reactions.tsx create mode 100644 apps/space/components/issues/peek-overview/issue-properties.tsx create mode 100644 apps/space/components/issues/peek-overview/issue-reaction.tsx create mode 100644 apps/space/components/issues/peek-overview/issue-vote-reactions.tsx create mode 100644 apps/space/components/issues/peek-overview/layout.tsx create mode 100644 apps/space/components/issues/peek-overview/side-peek-view.tsx create mode 100644 apps/space/components/tiptap/bubble-menu/index.tsx create mode 100644 apps/space/components/tiptap/bubble-menu/link-selector.tsx create mode 100644 apps/space/components/tiptap/bubble-menu/node-selector.tsx create mode 100644 apps/space/components/tiptap/bubble-menu/utils/link-validator.tsx create mode 100644 apps/space/components/tiptap/extensions/image-resize.tsx create mode 100644 apps/space/components/tiptap/extensions/index.tsx create mode 100644 apps/space/components/tiptap/extensions/updated-image.tsx create mode 100644 apps/space/components/tiptap/index.tsx create mode 100644 apps/space/components/tiptap/plugins/delete-image.tsx create mode 100644 apps/space/components/tiptap/plugins/upload-image.tsx create mode 100644 apps/space/components/tiptap/props.tsx create mode 100644 apps/space/components/tiptap/slash-command/index.tsx create mode 100644 apps/space/components/tiptap/utils.ts create mode 100644 apps/space/components/ui/dropdown.tsx create mode 100644 apps/space/components/ui/icon.tsx create mode 100644 apps/space/components/ui/index.ts create mode 100644 apps/space/components/ui/input.tsx create mode 100644 apps/space/components/ui/primary-button.tsx create mode 100644 apps/space/components/ui/reaction-selector.tsx create mode 100644 apps/space/components/ui/secondary-button.tsx create mode 100644 apps/space/components/ui/toast-alert.tsx create mode 100644 apps/space/constants/workspace.ts create mode 100644 apps/space/contexts/toast.context.tsx create mode 100644 apps/space/helpers/date-time.helper.ts create mode 100644 apps/space/helpers/emoji.helper.tsx create mode 100644 apps/space/helpers/string.helper.ts create mode 100644 apps/space/hooks/use-outside-click.tsx create mode 100644 apps/space/hooks/use-timer.tsx create mode 100644 apps/space/hooks/use-toast.tsx create mode 100644 apps/space/public/logos/github-black.png create mode 100644 apps/space/public/logos/github-square.png create mode 100644 apps/space/public/logos/github-white.png create mode 100644 apps/space/public/plane-logos/black-horizontal-with-blue-logo.svg create mode 100644 apps/space/public/plane-logos/blue-without-text.png create mode 100644 apps/space/public/plane-logos/white-horizontal-with-blue-logo.svg create mode 100644 apps/space/public/plane-logos/white-horizontal.svg create mode 100644 apps/space/services/authentication.service.ts create mode 100644 apps/space/services/file.service.ts diff --git a/apps/space/app/(auth)/components/email-code-form.tsx b/apps/space/app/(auth)/components/email-code-form.tsx new file mode 100644 index 000000000..b760ccfbb --- /dev/null +++ b/apps/space/app/(auth)/components/email-code-form.tsx @@ -0,0 +1,216 @@ +import React, { useEffect, useState, useCallback } from "react"; + +// react hook form +import { useForm } from "react-hook-form"; + +// services +import authenticationService from "services/authentication.service"; + +// hooks +import useToast from "hooks/use-toast"; +import useTimer from "hooks/use-timer"; + +// ui +import { Input, PrimaryButton } from "components/ui"; + +// types +type EmailCodeFormValues = { + email: string; + key?: string; + token?: string; +}; + +export const EmailCodeForm = ({ handleSignIn }: any) => { + const [codeSent, setCodeSent] = useState(false); + const [codeResent, setCodeResent] = useState(false); + const [isCodeResending, setIsCodeResending] = useState(false); + const [errorResendingCode, setErrorResendingCode] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const { setToastAlert } = useToast(); + const { timer: resendCodeTimer, setTimer: setResendCodeTimer } = useTimer(); + + const { + register, + handleSubmit, + setError, + setValue, + getValues, + watch, + formState: { errors, isSubmitting, isValid, isDirty }, + } = useForm({ + defaultValues: { + email: "", + key: "", + token: "", + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting || errorResendingCode; + + const onSubmit = useCallback( + async ({ email }: EmailCodeFormValues) => { + setErrorResendingCode(false); + await authenticationService + .emailCode({ email }) + .then((res) => { + setValue("key", res.key); + setCodeSent(true); + }) + .catch((err) => { + setErrorResendingCode(true); + setToastAlert({ + title: "Oops!", + type: "error", + message: err?.error, + }); + }); + }, + [setToastAlert, setValue] + ); + + const handleSignin = async (formData: EmailCodeFormValues) => { + setIsLoading(true); + await authenticationService + .magicSignIn(formData) + .then((response) => { + setIsLoading(false); + handleSignIn(response); + }) + .catch((error) => { + setIsLoading(false); + setToastAlert({ + title: "Oops!", + type: "error", + message: error?.response?.data?.error ?? "Enter the correct code to sign in", + }); + setError("token" as keyof EmailCodeFormValues, { + type: "manual", + message: error?.error, + }); + }); + }; + + const emailOld = getValues("email"); + + useEffect(() => { + setErrorResendingCode(false); + }, [emailOld]); + + useEffect(() => { + const submitForm = (e: KeyboardEvent) => { + if (!codeSent && e.key === "Enter") { + e.preventDefault(); + handleSubmit(onSubmit)().then(() => { + setResendCodeTimer(30); + }); + } + }; + + if (!codeSent) { + window.addEventListener("keydown", submitForm); + } + + return () => { + window.removeEventListener("keydown", submitForm); + }; + }, [handleSubmit, codeSent, onSubmit, setResendCodeTimer]); + + return ( + <> + {(codeSent || codeResent) && ( +

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

+ )} +
+
+ + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ) || "Email address is not valid", + })} + /> + {errors.email &&
{errors.email.message}
} +
+ + {codeSent && ( + <> + + {errors.token &&
{errors.token.message}
} + + + )} + {codeSent ? ( + + {isLoading ? "Signing in..." : "Sign in"} + + ) : ( + { + handleSubmit(onSubmit)().then(() => { + setResendCodeTimer(30); + }); + }} + disabled={!isValid && isDirty} + loading={isSubmitting} + > + {isSubmitting ? "Sending code..." : "Send sign in code"} + + )} +
+ + ); +}; diff --git a/apps/space/app/(auth)/components/email-password-form.tsx b/apps/space/app/(auth)/components/email-password-form.tsx new file mode 100644 index 000000000..23742eefe --- /dev/null +++ b/apps/space/app/(auth)/components/email-password-form.tsx @@ -0,0 +1,116 @@ +import React, { useState } from "react"; + +import { useRouter } from "next/router"; +import Link from "next/link"; + +// react hook form +import { useForm } from "react-hook-form"; +// components +import { EmailResetPasswordForm } from "./email-reset-password-form"; +// ui +import { Input, PrimaryButton } from "components/ui"; +// types +type EmailPasswordFormValues = { + email: string; + password?: string; + medium?: string; +}; + +type Props = { + onSubmit: (formData: EmailPasswordFormValues) => Promise; +}; + +export const EmailPasswordForm: React.FC = ({ onSubmit }) => { + const [isResettingPassword, setIsResettingPassword] = useState(false); + + const router = useRouter(); + const isSignUpPage = router.pathname === "/sign-up"; + + const { + register, + handleSubmit, + formState: { errors, isSubmitting, isValid, isDirty }, + } = useForm({ + defaultValues: { + email: "", + password: "", + medium: "email", + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + return ( + <> +

+ {isResettingPassword ? "Reset your password" : isSignUpPage ? "Sign up on Plane" : "Sign in to Plane"} +

+ {isResettingPassword ? ( + + ) : ( +
+
+ + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ) || "Email address is not valid", + })} + placeholder="Enter your email address..." + className="border-custom-border-300 h-[46px]" + /> + {errors.email &&
{errors.email.message}
} +
+
+ + {errors.password &&
{errors.password.message}
} +
+
+ {isSignUpPage ? ( + + Already have an account? Sign in. + + ) : ( + + )} +
+
+ + {isSignUpPage ? (isSubmitting ? "Signing up..." : "Sign up") : isSubmitting ? "Signing in..." : "Sign in"} + + {!isSignUpPage && ( + + + Don{"'"}t have an account? Sign up. + + + )} +
+
+ )} + + ); +}; diff --git a/apps/space/app/(auth)/components/email-reset-password-form.tsx b/apps/space/app/(auth)/components/email-reset-password-form.tsx new file mode 100644 index 000000000..c850b305c --- /dev/null +++ b/apps/space/app/(auth)/components/email-reset-password-form.tsx @@ -0,0 +1,89 @@ +import React from "react"; + +// react hook form +import { useForm } from "react-hook-form"; +// services +import userService from "services/user.service"; +// hooks +// import useToast from "hooks/use-toast"; +// ui +import { Input, PrimaryButton, SecondaryButton } from "components/ui"; +// types +type Props = { + setIsResettingPassword: React.Dispatch>; +}; + +export const EmailResetPasswordForm: React.FC = ({ setIsResettingPassword }) => { + // const { setToastAlert } = useToast(); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + email: "", + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const forgotPassword = async (formData: any) => { + const payload = { + email: formData.email, + }; + + // await userService + // .forgotPassword(payload) + // .then(() => + // setToastAlert({ + // type: "success", + // title: "Success!", + // message: "Password reset link has been sent to your email address.", + // }) + // ) + // .catch((err) => { + // if (err.status === 400) + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: "Please check the Email ID entered.", + // }); + // else + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: "Something went wrong. Please try again.", + // }); + // }); + }; + + return ( +
+
+ + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ) || "Email address is not valid", + })} + placeholder="Enter registered email address.." + className="border-custom-border-300 h-[46px]" + /> + {errors.email &&
{errors.email.message}
} +
+
+ setIsResettingPassword(false)}> + Go Back + + + {isSubmitting ? "Sending link..." : "Send reset link"} + +
+
+ ); +}; diff --git a/apps/space/app/(auth)/components/github-login-button.tsx b/apps/space/app/(auth)/components/github-login-button.tsx new file mode 100644 index 000000000..54f215421 --- /dev/null +++ b/apps/space/app/(auth)/components/github-login-button.tsx @@ -0,0 +1,57 @@ +import { useEffect, useState, FC } from "react"; + +import Link from "next/link"; +import Image from "next/image"; +import { useSearchParams } from "next/navigation"; + +// next-themes +import { useTheme } from "next-themes"; +// images +import githubBlackImage from "/public/logos/github-black.png"; +import githubWhiteImage from "/public/logos/github-white.png"; + +export interface GithubLoginButtonProps { + handleSignIn: React.Dispatch; +} + +export const GithubLoginButton: FC = ({ handleSignIn }) => { + const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); + const [gitCode, setGitCode] = useState(null); + + const searchParams = useSearchParams(); + + const code = searchParams?.get("code"); + + const { theme } = useTheme(); + + useEffect(() => { + if (code && !gitCode) { + setGitCode(code.toString()); + handleSignIn(code.toString()); + } + }, [code, gitCode, handleSignIn]); + + useEffect(() => { + const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + setLoginCallBackURL(`${origin}/` as any); + }, []); + + return ( +
+ + + +
+ ); +}; diff --git a/apps/space/app/(auth)/components/google-login.tsx b/apps/space/app/(auth)/components/google-login.tsx new file mode 100644 index 000000000..82916d7b5 --- /dev/null +++ b/apps/space/app/(auth)/components/google-login.tsx @@ -0,0 +1,59 @@ +import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react"; + +import Script from "next/script"; + +export interface IGoogleLoginButton { + text?: string; + handleSignIn: React.Dispatch; + styles?: CSSProperties; +} + +export const GoogleLoginButton: FC = ({ handleSignIn }) => { + const googleSignInButton = useRef(null); + const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false); + + const loadScript = useCallback(() => { + if (!googleSignInButton.current || gsiScriptLoaded) return; + + (window as any)?.google?.accounts.id.initialize({ + client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "", + callback: handleSignIn, + }); + + try { + (window as any)?.google?.accounts.id.renderButton( + googleSignInButton.current, + { + type: "standard", + theme: "outline", + size: "large", + logo_alignment: "center", + width: 360, + text: "signin_with", + } as any // customization attributes + ); + } catch (err) { + console.log(err); + } + + (window as any)?.google?.accounts.id.prompt(); // also display the One Tap dialog + + setGsiScriptLoaded(true); + }, [handleSignIn, gsiScriptLoaded]); + + useEffect(() => { + if ((window as any)?.google?.accounts?.id) { + loadScript(); + } + return () => { + (window as any)?.google?.accounts.id.cancel(); + }; + }, [loadScript]); + + return ( + <> +