From 60b5c74ed62a664d9950a4b9d76da37737e53af4 Mon Sep 17 00:00:00 2001 From: m5r Date: Sat, 25 Sep 2021 20:58:28 +0800 Subject: [PATCH] style login form --- app/auth/components/auth-form.tsx | 102 ++++++++++++++++++ app/auth/components/labeled-text-field.tsx | 66 ++++++++++++ app/auth/components/login-form.tsx | 55 ---------- app/auth/pages/sign-in.tsx | 52 +++++++-- app/{settings => core}/components/alert.tsx | 0 app/core/components/logo.tsx | 15 +++ app/pages/_app.tsx | 7 +- .../components/profile-informations.tsx | 2 +- app/settings/components/update-password.tsx | 2 +- 9 files changed, 234 insertions(+), 67 deletions(-) create mode 100644 app/auth/components/auth-form.tsx create mode 100644 app/auth/components/labeled-text-field.tsx delete mode 100644 app/auth/components/login-form.tsx rename app/{settings => core}/components/alert.tsx (100%) create mode 100644 app/core/components/logo.tsx diff --git a/app/auth/components/auth-form.tsx b/app/auth/components/auth-form.tsx new file mode 100644 index 0000000..843a2c2 --- /dev/null +++ b/app/auth/components/auth-form.tsx @@ -0,0 +1,102 @@ +import { useState, ReactNode, PropsWithoutRef } from "react"; +import { FormProvider, useForm, UseFormProps } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import Alert from "../../core/components/alert"; +import clsx from "clsx"; +import Logo from "../../core/components/logo"; +import { Link, Routes } from "blitz"; + +export interface FormProps> + extends Omit, "onSubmit"> { + /** All your form fields */ + children?: ReactNode; + texts: { + title: string; + subtitle: ReactNode; + submit: string; + }; + schema?: S; + onSubmit: (values: z.infer) => Promise; + initialValues?: UseFormProps>["defaultValues"]; +} + +interface OnSubmitResult { + FORM_ERROR?: string; + + [prop: string]: any; +} + +export const FORM_ERROR = "FORM_ERROR"; + +export function AuthForm>({ + children, + texts, + schema, + initialValues, + onSubmit, + ...props +}: FormProps) { + const ctx = useForm>({ + mode: "onBlur", + resolver: schema ? zodResolver(schema) : undefined, + defaultValues: initialValues, + }); + const [formError, setFormError] = useState(null); + + return ( +
+
+ +

{texts.title}

+

{texts.subtitle}

+
+ +
+ +
{ + const result = (await onSubmit(values)) || {}; + for (const [key, value] of Object.entries(result)) { + if (key === FORM_ERROR) { + setFormError(value); + } else { + ctx.setError(key as any, { + type: "submit", + message: value, + }); + } + } + })} + className="form" + {...props} + > + {children} + + {formError ? ( +
+ +
+ ) : null} + + +
+
+
+
+ ); +} + +export default AuthForm; diff --git a/app/auth/components/labeled-text-field.tsx b/app/auth/components/labeled-text-field.tsx new file mode 100644 index 0000000..c62fe54 --- /dev/null +++ b/app/auth/components/labeled-text-field.tsx @@ -0,0 +1,66 @@ +import { forwardRef, PropsWithoutRef } from "react"; +import { Link, Routes } from "blitz"; +import { useFormContext } from "react-hook-form"; +import clsx from "clsx"; + +export interface LabeledTextFieldProps extends PropsWithoutRef { + /** Field name. */ + name: string; + /** Field label. */ + label: string; + /** Field type. Doesn't include radio buttons and checkboxes */ + type?: "text" | "password" | "email" | "number"; + showForgotPasswordLabel?: boolean; +} + +export const LabeledTextField = forwardRef( + ({ label, name, showForgotPasswordLabel, ...props }, ref) => { + const { + register, + formState: { isSubmitting, errors }, + } = useFormContext(); + const error = Array.isArray(errors[name]) ? errors[name].join(", ") : errors[name]?.message || errors[name]; + + return ( +
+ +
+ +
+ + {error ? ( +
+ {error} +
+ ) : null} +
+ ); + }, +); + +export default LabeledTextField; diff --git a/app/auth/components/login-form.tsx b/app/auth/components/login-form.tsx deleted file mode 100644 index d013248..0000000 --- a/app/auth/components/login-form.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { AuthenticationError, Link, useMutation, Routes } from "blitz"; - -import { LabeledTextField } from "../../core/components/labeled-text-field"; -import { Form, FORM_ERROR } from "../../core/components/form"; -import login from "../../../app/auth/mutations/login"; -import { Login } from "../validations"; - -type LoginFormProps = { - onSuccess?: () => void; -}; - -export const LoginForm = (props: LoginFormProps) => { - const [loginMutation] = useMutation(login); - - return ( -
-

Login

- -
{ - try { - await loginMutation(values); - props.onSuccess?.(); - } catch (error: any) { - if (error instanceof AuthenticationError) { - return { [FORM_ERROR]: "Sorry, those credentials are invalid" }; - } else { - return { - [FORM_ERROR]: - "Sorry, we had an unexpected error. Please try again. - " + error.toString(), - }; - } - } - }} - > - - - - - -
- Or Sign Up -
-
- ); -}; - -export default LoginForm; diff --git a/app/auth/pages/sign-in.tsx b/app/auth/pages/sign-in.tsx index b158a2f..ed4abcc 100644 --- a/app/auth/pages/sign-in.tsx +++ b/app/auth/pages/sign-in.tsx @@ -1,23 +1,61 @@ import type { BlitzPage } from "blitz"; -import { useRouter, Routes } from "blitz"; +import { useRouter, Routes, AuthenticationError, Link, useMutation } from "blitz"; import BaseLayout from "../../core/layouts/base-layout"; -import { LoginForm } from "../components/login-form"; +import { AuthForm as Form, FORM_ERROR } from "../components/auth-form"; +import { Login } from "../validations"; +import { LabeledTextField } from "../components/labeled-text-field"; +import login from "../mutations/login"; const SignIn: BlitzPage = () => { const router = useRouter(); + const [loginMutation] = useMutation(login); return ( -
- { +
+ Need an account?  + + + Create yours for free + + + + ), + submit: "Sign in", + }} + schema={Login} + initialValues={{ email: "", password: "" }} + onSubmit={async (values) => { + try { + await loginMutation(values); const next = router.query.next ? decodeURIComponent(router.query.next as string) : Routes.Messages(); router.push(next); - }} + } catch (error: any) { + if (error instanceof AuthenticationError) { + return { [FORM_ERROR]: "Sorry, those credentials are invalid" }; + } else { + return { + [FORM_ERROR]: "Sorry, we had an unexpected error. Please try again. - " + error.toString(), + }; + } + } + }} + > + + -
+ ); }; diff --git a/app/settings/components/alert.tsx b/app/core/components/alert.tsx similarity index 100% rename from app/settings/components/alert.tsx rename to app/core/components/alert.tsx diff --git a/app/core/components/logo.tsx b/app/core/components/logo.tsx new file mode 100644 index 0000000..0c0469c --- /dev/null +++ b/app/core/components/logo.tsx @@ -0,0 +1,15 @@ +import type { FunctionComponent } from "react"; +import Image from "next/image"; +import clsx from "clsx"; + +type Props = { + className?: string; +}; + +const Logo: FunctionComponent = ({ className }) => ( +
+ app logo +
+); + +export default Logo; diff --git a/app/pages/_app.tsx b/app/pages/_app.tsx index 7a8b117..d698c68 100644 --- a/app/pages/_app.tsx +++ b/app/pages/_app.tsx @@ -5,6 +5,8 @@ import { AuthenticationError, AuthorizationError, ErrorFallbackProps, + RedirectError, + Routes, useQueryErrorResetBoundary, getConfig, useSession, @@ -12,7 +14,6 @@ import { import Sentry from "../../integrations/sentry"; import ErrorComponent from "../core/components/error-component"; -import LoginForm from "../auth/components/login-form"; import { usePanelbear } from "../core/hooks/use-panelbear"; import "app/core/styles/index.css"; @@ -46,9 +47,9 @@ export default function App({ Component, pageProps }: AppProps) { ); } -function RootErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) { +function RootErrorFallback({ error }: ErrorFallbackProps) { if (error instanceof AuthenticationError) { - return ; + throw new RedirectError(Routes.SignIn()); } else if (error instanceof AuthorizationError) { return ; } else { diff --git a/app/settings/components/profile-informations.tsx b/app/settings/components/profile-informations.tsx index 039ee2c..8f1888f 100644 --- a/app/settings/components/profile-informations.tsx +++ b/app/settings/components/profile-informations.tsx @@ -4,7 +4,7 @@ import { useMutation } from "blitz"; import { useForm } from "react-hook-form"; import updateUser from "../mutations/update-user"; -import Alert from "./alert"; +import Alert from "../../core/components/alert"; import Button from "./button"; import SettingsSection from "./settings-section"; import useCurrentUser from "../../core/hooks/use-current-user"; diff --git a/app/settings/components/update-password.tsx b/app/settings/components/update-password.tsx index 98a5dd7..93f481b 100644 --- a/app/settings/components/update-password.tsx +++ b/app/settings/components/update-password.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useMutation } from "blitz"; import { useForm } from "react-hook-form"; -import Alert from "./alert"; +import Alert from "../../core/components/alert"; import Button from "./button"; import SettingsSection from "./settings-section";