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 ( + <> +