Standardize auth forms layouts
This commit is contained in:
parent
4d7206f889
commit
1719a14ec5
6 changed files with 215 additions and 181 deletions
28
dashboard/src/modules/Auth/components/AuthFormLayout.tsx
Normal file
28
dashboard/src/modules/Auth/components/AuthFormLayout.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Container, Flex, SlideFade, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const AuthFormLayout: React.FC<IProps> = ({ children, title, description }) => {
|
||||
return (
|
||||
<Container maxW="1250px">
|
||||
<Flex flex={1} height="100vh" overflowY="hidden">
|
||||
<SlideFade in className="flex flex-1 flex-col justify-center items-center" offsetY="20px">
|
||||
<img className="self-center mb-5 logo" src="/tipi.png" width={512} height={512} />
|
||||
<Text className="text-xl md:text-2xl lg:text-5xl font-bold" size="3xl">
|
||||
{title}
|
||||
</Text>
|
||||
<Text className="md:text-lg lg:text-2xl text-center" color="gray.500">
|
||||
{description}
|
||||
</Text>
|
||||
{children}
|
||||
</SlideFade>
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthFormLayout;
|
57
dashboard/src/modules/Auth/components/LoginForm.tsx
Normal file
57
dashboard/src/modules/Auth/components/LoginForm.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { Button } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Field, Form } from 'react-final-form';
|
||||
import validator from 'validator';
|
||||
import FormInput from '../../../components/Form/FormInput';
|
||||
|
||||
type FormValues = { email: string; password: string };
|
||||
|
||||
interface IProps {
|
||||
onSubmit: (values: FormValues) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const LoginForm: React.FC<IProps> = ({ onSubmit, loading }) => {
|
||||
const validateFields = (values: FormValues) => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!validator.isEmail(values.email || '')) {
|
||||
errors.email = 'Invalid email';
|
||||
}
|
||||
|
||||
if (!values.password) {
|
||||
errors.password = 'Required';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
return (
|
||||
<Form<FormValues>
|
||||
onSubmit={onSubmit}
|
||||
validateOnBlur={true}
|
||||
validate={(values) => validateFields(values)}
|
||||
render={({ handleSubmit, validating, submitting }) => (
|
||||
<form className="flex flex-col" onSubmit={handleSubmit}>
|
||||
<Field
|
||||
name="email"
|
||||
render={({ input, meta }) => (
|
||||
<FormInput size="lg" className="mt-3 w-full" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} placeholder="Email" {...input} />
|
||||
)}
|
||||
/>
|
||||
<Field
|
||||
name="password"
|
||||
render={({ input, meta }) => (
|
||||
<FormInput size="lg" className="mt-3 w-full" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} placeholder="Password" type="password" {...input} />
|
||||
)}
|
||||
/>
|
||||
<Button isLoading={validating || submitting || loading} className="mt-2" colorScheme="green" type="submit">
|
||||
Login
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
75
dashboard/src/modules/Auth/components/RegisterForm.tsx
Normal file
75
dashboard/src/modules/Auth/components/RegisterForm.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { Button } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Field, Form } from 'react-final-form';
|
||||
import validator from 'validator';
|
||||
import FormInput from '../../../components/Form/FormInput';
|
||||
|
||||
interface IProps {
|
||||
onSubmit: (values: FormValues) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
type FormValues = { email: string; password: string; passwordConfirm: string };
|
||||
|
||||
const RegisterForm: React.FC<IProps> = ({ onSubmit, loading }) => {
|
||||
const validateFields = (values: FormValues) => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!validator.isEmail(values.email || '')) {
|
||||
errors.email = 'Invalid email';
|
||||
}
|
||||
|
||||
if (!values.password) {
|
||||
errors.password = 'Required';
|
||||
}
|
||||
|
||||
if (values.password !== values.passwordConfirm) {
|
||||
errors.passwordConfirm = 'Passwords do not match';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
return (
|
||||
<Form<FormValues>
|
||||
onSubmit={onSubmit}
|
||||
validateOnBlur={true}
|
||||
validate={(values) => validateFields(values)}
|
||||
render={({ handleSubmit, validating, submitting }) => (
|
||||
<form className="flex flex-col" onSubmit={handleSubmit}>
|
||||
<Field
|
||||
name="email"
|
||||
render={({ input, meta }) => (
|
||||
<FormInput size="lg" className="mt-3 w-full" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} placeholder="Email" {...input} />
|
||||
)}
|
||||
/>
|
||||
<Field
|
||||
name="password"
|
||||
render={({ input, meta }) => (
|
||||
<FormInput size="lg" className="mt-3 w-full" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} placeholder="Password" type="password" {...input} />
|
||||
)}
|
||||
/>
|
||||
<Field
|
||||
name="passwordConfirm"
|
||||
render={({ input, meta }) => (
|
||||
<FormInput
|
||||
size="lg"
|
||||
className="mt-3 w-full"
|
||||
error={meta.error}
|
||||
isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)}
|
||||
placeholder="Repeat password"
|
||||
type="password"
|
||||
{...input}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button isLoading={validating || submitting || loading} className="mt-2" colorScheme="green" type="submit">
|
||||
Enter
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterForm;
|
|
@ -1,4 +1,3 @@
|
|||
import { useToast } from '@chakra-ui/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import LoadingScreen from '../../../components/LoadingScreen';
|
||||
import { useAuthStore } from '../../../state/authStore';
|
||||
|
@ -7,8 +6,7 @@ import Onboarding from './Onboarding';
|
|||
|
||||
const AuthWrapper: React.FC = ({ children }) => {
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
const { configured, loading, user, login, me, fetchConfigured, register } = useAuthStore();
|
||||
const toast = useToast();
|
||||
const { configured, user, me, fetchConfigured } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
|
@ -20,36 +18,6 @@ const AuthWrapper: React.FC = ({ children }) => {
|
|||
if (!user) fetchUser();
|
||||
}, [fetchConfigured, me, user]);
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof Error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
position: 'top',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async (values: { email: string; password: string }) => {
|
||||
try {
|
||||
await login(values.email, values.password);
|
||||
await me();
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async (values: { email: string; password: string }) => {
|
||||
try {
|
||||
await register(values.email, values.password);
|
||||
await me();
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (initialLoad && !user) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
@ -59,10 +27,10 @@ const AuthWrapper: React.FC = ({ children }) => {
|
|||
}
|
||||
|
||||
if (!configured) {
|
||||
return <Onboarding loading={loading} onSubmit={handleRegister} />;
|
||||
return <Onboarding />;
|
||||
}
|
||||
|
||||
return <Login loading={loading} onSubmit={handleLogin} />;
|
||||
return <Login />;
|
||||
};
|
||||
|
||||
export default AuthWrapper;
|
||||
|
|
|
@ -1,77 +1,40 @@
|
|||
import { Button, Container, Flex, SlideFade, Text } from '@chakra-ui/react';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Field, Form } from 'react-final-form';
|
||||
import validator from 'validator';
|
||||
import FormInput from '../../../components/Form/FormInput';
|
||||
import { useAuthStore } from '../../../state/authStore';
|
||||
import AuthFormLayout from '../components/AuthFormLayout';
|
||||
import LoginForm from '../components/LoginForm';
|
||||
|
||||
type FormValues = { email: string; password: string };
|
||||
|
||||
interface IProps {
|
||||
onSubmit: (values: FormValues) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
const Login: React.FC = () => {
|
||||
const { me, login, loading } = useAuthStore();
|
||||
const toast = useToast();
|
||||
|
||||
const Login: React.FC<IProps> = ({ onSubmit, loading }) => {
|
||||
const validateFields = (values: FormValues) => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!validator.isEmail(values.email || '')) {
|
||||
errors.email = 'Invalid email';
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof Error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
position: 'top',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!values.password) {
|
||||
errors.password = 'Required';
|
||||
const handleLogin = async (values: FormValues) => {
|
||||
try {
|
||||
await login(values.email, values.password);
|
||||
await me();
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxW="1250px">
|
||||
<Flex flex={1} height="100vh" overflowY="hidden">
|
||||
<SlideFade in className="flex flex-1 flex-col justify-center items-center" offsetY="20px">
|
||||
<img className="self-center mb-5 logo" src="/tipi.png" width={512} height={512} />
|
||||
<Text className="text-xl md:text-2xl lg:text-5xl font-bold" size="3xl">
|
||||
Welcome back
|
||||
</Text>
|
||||
<Text className="md:text-lg lg:text-2xl text-center" color="gray.500">
|
||||
Enter your credentials to login to your Tipi
|
||||
</Text>
|
||||
<Form<FormValues>
|
||||
onSubmit={onSubmit}
|
||||
validateOnBlur={true}
|
||||
validate={(values) => validateFields(values)}
|
||||
render={({ handleSubmit, validating, submitting }) => (
|
||||
<form className="flex flex-col" onSubmit={handleSubmit}>
|
||||
<Field
|
||||
name="email"
|
||||
render={({ input, meta }) => (
|
||||
<FormInput size="lg" className="mt-3 w-full" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} placeholder="Email" {...input} />
|
||||
)}
|
||||
/>
|
||||
<Field
|
||||
name="password"
|
||||
render={({ input, meta }) => (
|
||||
<FormInput
|
||||
size="lg"
|
||||
className="mt-3 w-full"
|
||||
error={meta.error}
|
||||
isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
{...input}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button isLoading={validating || submitting || loading} className="mt-2" colorScheme="green" type="submit">
|
||||
Login
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
</SlideFade>
|
||||
</Flex>
|
||||
</Container>
|
||||
<AuthFormLayout title="Welcome back" description="Enter your credentials to login to your Tipi">
|
||||
<LoginForm onSubmit={handleLogin} loading={loading} />
|
||||
</AuthFormLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,95 +1,38 @@
|
|||
import { Button, Container, Flex, SlideFade, Text } from '@chakra-ui/react';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Field, Form } from 'react-final-form';
|
||||
import validator from 'validator';
|
||||
import FormInput from '../../../components/Form/FormInput';
|
||||
import { useAuthStore } from '../../../state/authStore';
|
||||
import AuthFormLayout from '../components/AuthFormLayout';
|
||||
import RegisterForm from '../components/RegisterForm';
|
||||
|
||||
type FormValues = { email: string; password: string; passwordConfirm: string };
|
||||
const Onboarding: React.FC = () => {
|
||||
const toast = useToast();
|
||||
const { me, register, loading } = useAuthStore();
|
||||
|
||||
interface IProps {
|
||||
onSubmit: (values: FormValues) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const Onboarding: React.FC<IProps> = ({ onSubmit, loading }) => {
|
||||
const validateFields = (values: FormValues) => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!validator.isEmail(values.email || '')) {
|
||||
errors.email = 'Invalid email';
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof Error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
position: 'top',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!values.password) {
|
||||
errors.password = 'Required';
|
||||
const handleRegister = async (values: { email: string; password: string }) => {
|
||||
try {
|
||||
await register(values.email, values.password);
|
||||
await me();
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
if (values.password !== values.passwordConfirm) {
|
||||
errors.passwordConfirm = 'Passwords do not match';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxW="1250px">
|
||||
<Flex flex={1} height="100vh" overflowY="hidden">
|
||||
<SlideFade in className="flex flex-1 flex-col justify-center items-center" offsetY="20px">
|
||||
<img className="self-center mb-5 logo" src="/tipi.png" width={512} height={512} />
|
||||
<Text className="text-xl md:text-2xl lg:text-5xl font-bold" size="3xl">
|
||||
Welcome to your Tipi
|
||||
</Text>
|
||||
<Text className="md:text-lg lg:text-2xl text-center" color="gray.500">
|
||||
Register your account to get started
|
||||
</Text>
|
||||
<Form<FormValues>
|
||||
onSubmit={onSubmit}
|
||||
validateOnBlur={true}
|
||||
validate={(values) => validateFields(values)}
|
||||
render={({ handleSubmit, validating, submitting }) => (
|
||||
<form className="flex flex-col" onSubmit={handleSubmit}>
|
||||
<Field
|
||||
name="email"
|
||||
render={({ input, meta }) => (
|
||||
<FormInput size="lg" className="mt-3 w-full" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} placeholder="Email" {...input} />
|
||||
)}
|
||||
/>
|
||||
<Field
|
||||
name="password"
|
||||
render={({ input, meta }) => (
|
||||
<FormInput
|
||||
size="lg"
|
||||
className="mt-3 w-full"
|
||||
error={meta.error}
|
||||
isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
{...input}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Field
|
||||
name="passwordConfirm"
|
||||
render={({ input, meta }) => (
|
||||
<FormInput
|
||||
size="lg"
|
||||
className="mt-3 w-full"
|
||||
error={meta.error}
|
||||
isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)}
|
||||
placeholder="Repeat password"
|
||||
type="password"
|
||||
{...input}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button isLoading={validating || submitting || loading} className="mt-2" colorScheme="green" type="submit">
|
||||
Enter
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
</SlideFade>
|
||||
</Flex>
|
||||
</Container>
|
||||
<AuthFormLayout title="Welcome to your Tipi" description="Register your account to get started">
|
||||
<RegisterForm onSubmit={handleRegister} loading={loading} />
|
||||
</AuthFormLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue