diff --git a/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx b/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx deleted file mode 100644 index 7b4ed614233..00000000000 --- a/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx +++ /dev/null @@ -1,66 +0,0 @@ -// next imports -import Link from "next/link"; -import Image from "next/image"; -import { Metadata, ResolvingMetadata } from "next"; -// components -import IssueNavbar from "components/issues/navbar"; -import IssueFilter from "components/issues/filters-render"; -// service -import ProjectService from "services/project.service"; -import { redirect } from "next/navigation"; - -type LayoutProps = { - params: { workspace_slug: string; project_slug: string }; -}; - -export async function generateMetadata({ params }: LayoutProps): Promise { - // read route params - const { workspace_slug, project_slug } = params; - const projectServiceInstance = new ProjectService(); - - try { - const project = await projectServiceInstance?.getProjectSettingsAsync(workspace_slug, project_slug); - - return { - title: `${project?.project_details?.name} | ${workspace_slug}`, - description: `${ - project?.project_details?.description || `${project?.project_details?.name} | ${workspace_slug}` - }`, - icons: `data:image/svg+xml,${ - typeof project?.project_details?.emoji != "object" - ? String.fromCodePoint(parseInt(project?.project_details?.emoji)) - : "✈️" - }`, - }; - } catch (error: any) { - if (error?.data?.error) { - redirect(`/project-not-published`); - } - return {}; - } -} - -const RootLayout = ({ children }: { children: React.ReactNode }) => ( -
-
- -
- {/*
- -
*/} -
{children}
- -
- -
- plane logo -
-
- Powered by Plane Deploy -
- -
-
-); - -export default RootLayout; diff --git a/apps/space/app/[workspace_slug]/[project_slug]/page.tsx b/apps/space/app/[workspace_slug]/[project_slug]/page.tsx deleted file mode 100644 index 81c2b48c2c1..00000000000 --- a/apps/space/app/[workspace_slug]/[project_slug]/page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -// next imports -import { useRouter, useParams, useSearchParams } from "next/navigation"; -// mobx -import { observer } from "mobx-react-lite"; -// components -import { IssueListView } from "components/issues/board-views/list"; -import { IssueKanbanView } from "components/issues/board-views/kanban"; -import { IssueCalendarView } from "components/issues/board-views/calendar"; -import { IssueSpreadsheetView } from "components/issues/board-views/spreadsheet"; -import { IssueGanttView } from "components/issues/board-views/gantt"; -// mobx store -import { RootStore } from "store/root"; -import { useMobxStore } from "lib/mobx/store-provider"; -// types -import { TIssueBoardKeys } from "store/types"; - -const WorkspaceProjectPage = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const routerParams = useParams(); - const routerSearchparams = useSearchParams(); - - const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string }; - const board = - routerSearchparams && - routerSearchparams.get("board") != null && - (routerSearchparams.get("board") as TIssueBoardKeys | ""); - - // updating default board view when we are in the issues page - useEffect(() => { - if (workspace_slug && project_slug && store?.project?.workspaceProjectSettings) { - const workspacePRojectSettingViews = store?.project?.workspaceProjectSettings?.views; - const userAccessViews: TIssueBoardKeys[] = []; - - Object.keys(workspacePRojectSettingViews).filter((_key) => { - if (_key === "list" && workspacePRojectSettingViews.list === true) userAccessViews.push(_key); - if (_key === "kanban" && workspacePRojectSettingViews.kanban === true) userAccessViews.push(_key); - if (_key === "calendar" && workspacePRojectSettingViews.calendar === true) userAccessViews.push(_key); - if (_key === "spreadsheet" && workspacePRojectSettingViews.spreadsheet === true) userAccessViews.push(_key); - if (_key === "gantt" && workspacePRojectSettingViews.gantt === true) userAccessViews.push(_key); - }); - - if (userAccessViews && userAccessViews.length > 0) { - if (!board) { - store.issue.setCurrentIssueBoardView(userAccessViews[0]); - router.replace(`/${workspace_slug}/${project_slug}?board=${userAccessViews[0]}`); - } else { - if (userAccessViews.includes(board)) { - if (store.issue.currentIssueBoardView === null) store.issue.setCurrentIssueBoardView(board); - else { - if (board === store.issue.currentIssueBoardView) - router.replace(`/${workspace_slug}/${project_slug}?board=${board}`); - else { - store.issue.setCurrentIssueBoardView(board); - router.replace(`/${workspace_slug}/${project_slug}?board=${board}`); - } - } - } else { - store.issue.setCurrentIssueBoardView(userAccessViews[0]); - router.replace(`/${workspace_slug}/${project_slug}?board=${userAccessViews[0]}`); - } - } - } - } - }, [workspace_slug, project_slug, board, router, store?.issue, store?.project?.workspaceProjectSettings]); - - useEffect(() => { - if (workspace_slug && project_slug) { - store?.project?.getProjectSettingsAsync(workspace_slug, project_slug); - store?.issue?.getIssuesAsync(workspace_slug, project_slug); - } - }, [workspace_slug, project_slug, store?.project, store?.issue]); - - return ( -
- {store?.issue?.loader && !store.issue.issues ? ( -
Loading...
- ) : ( - <> - {store?.issue?.error ? ( -
Something went wrong.
- ) : ( - store?.issue?.currentIssueBoardView && ( - <> - {store?.issue?.currentIssueBoardView === "list" && ( -
-
- -
-
- )} - {store?.issue?.currentIssueBoardView === "kanban" && ( -
- -
- )} - {store?.issue?.currentIssueBoardView === "calendar" && } - {store?.issue?.currentIssueBoardView === "spreadsheet" && } - {store?.issue?.currentIssueBoardView === "gantt" && } - - ) - )} - - )} -
- ); -}); - -export default WorkspaceProjectPage; diff --git a/apps/space/app/layout.tsx b/apps/space/app/layout.tsx deleted file mode 100644 index b63f748e8ab..00000000000 --- a/apps/space/app/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -// root styles -import "styles/globals.css"; -// mobx store provider -import { MobxStoreProvider } from "lib/mobx/store-provider"; -import MobxStoreInit from "lib/mobx/store-init"; - -const RootLayout = ({ children }: { children: React.ReactNode }) => ( - - - - -
{children}
-
- - -); - -export default RootLayout; diff --git a/apps/space/app/page.tsx b/apps/space/app/page.tsx deleted file mode 100644 index 6a18b728395..00000000000 --- a/apps/space/app/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -import React from "react"; - -const HomePage = () => ( -
Plane Deploy
-); - -export default HomePage; diff --git a/apps/space/components/accounts/email-code-form.tsx b/apps/space/components/accounts/email-code-form.tsx new file mode 100644 index 00000000000..b760ccfbb2a --- /dev/null +++ b/apps/space/components/accounts/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/components/accounts/email-password-form.tsx b/apps/space/components/accounts/email-password-form.tsx new file mode 100644 index 00000000000..23742eefe15 --- /dev/null +++ b/apps/space/components/accounts/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/components/accounts/email-reset-password-form.tsx b/apps/space/components/accounts/email-reset-password-form.tsx new file mode 100644 index 00000000000..c850b305cec --- /dev/null +++ b/apps/space/components/accounts/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/components/accounts/github-login-button.tsx b/apps/space/components/accounts/github-login-button.tsx new file mode 100644 index 00000000000..4ba47b421b2 --- /dev/null +++ b/apps/space/components/accounts/github-login-button.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState, FC } from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { useRouter } from "next/router"; +// 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 router = useRouter(); + + const { code } = router.query; + + 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/components/accounts/google-login.tsx b/apps/space/components/accounts/google-login.tsx new file mode 100644 index 00000000000..82916d7b569 --- /dev/null +++ b/apps/space/components/accounts/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 ( + <> +