Skip to content

Commit

Permalink
Merge pull request #15 from BinaryStudioAcademy/task/OV-1-add-sign-in…
Browse files Browse the repository at this point in the history
…-flow

OV-1: Sign In flow
  • Loading branch information
nikita-remeslov authored Aug 21, 2024
2 parents 7c3d9b7 + ac9ecf6 commit b1dee31
Show file tree
Hide file tree
Showing 25 changed files with 335 additions and 92 deletions.
56 changes: 2 additions & 54 deletions frontend/src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,9 @@
import reactLogo from '~/assets/img/react.svg';
import { Link, RouterOutlet } from '~/bundles/common/components/components.js';
import { AppRoute } from '~/bundles/common/enums/enums.js';
import {
useAppDispatch,
useAppSelector,
useEffect,
useLocation,
} from '~/bundles/common/hooks/hooks.js';
import { actions as userActions } from '~/bundles/users/store/users.js';
import { RouterOutlet } from '~/bundles/common/components/components.js';

const App: React.FC = () => {
const { pathname } = useLocation();
const dispatch = useAppDispatch();
const { users, dataStatus } = useAppSelector(({ users }) => ({
users: users.users,
dataStatus: users.dataStatus,
}));

const isRoot = pathname === AppRoute.ROOT;

useEffect(() => {
if (isRoot) {
void dispatch(userActions.loadAll());
}
}, [isRoot, dispatch]);

return (
<>
<img src={reactLogo} width="30" alt="logo" />

<ul>
<li>
<Link to={AppRoute.ROOT}>Root</Link>
</li>
<li>
<Link to={AppRoute.SIGN_IN}>Sign in</Link>
</li>
<li>
<Link to={AppRoute.SIGN_UP}>Sign up</Link>
</li>
</ul>
<p>Current path: {pathname}</p>

<div>
<RouterOutlet />
</div>
{isRoot && (
<>
<h2>Users:</h2>
<h3>Status: {dataStatus}</h3>
<ul>
{users.map((it) => (
<li key={it.id}>{it.email}</li>
))}
</ul>
</>
)}
<RouterOutlet />
</>
);
};
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/bundles/auth/auth-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ApiPath, ContentType } from '~/bundles/common/enums/enums.js';
import {
type UserSignInRequestDto,
type UserSignInResponseDto,
type UserSignUpRequestDto,
type UserSignUpResponseDto,
} from '~/bundles/users/users.js';
Expand All @@ -20,6 +22,22 @@ class AuthApi extends BaseHttpApi {
super({ path: ApiPath.AUTH, baseUrl, http, storage });
}

public async signIn(
payload: UserSignInRequestDto,
): Promise<UserSignInResponseDto> {
const response = await this.load(
this.getFullEndpoint(AuthApiPath.SIGN_IN, {}),
{
method: 'POST',
contentType: ContentType.JSON,
payload: JSON.stringify(payload),
hasAuth: false,
},
);

return await response.json<UserSignInResponseDto>();
}

public async signUp(
payload: UserSignUpRequestDto,
): Promise<UserSignUpResponseDto> {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/bundles/auth/components/common/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { FormError } from './form-error/form-error.js';
export { FormHeader } from './form-header/form-header.js';
export { PasswordInput } from './password-input/password-input.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
FormControl,
FormErrorMessage,
} from '~/bundles/common/components/components.js';

type Properties = {
isVisible: boolean;
message: string;
};

const FormError: React.FC<Properties> = ({ isVisible, message }) => {
return (
<FormControl isInvalid={isVisible}>
<FormErrorMessage>{message}</FormErrorMessage>
</FormControl>
);
};

export { FormError };
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Heading, Text } from '~/bundles/common/components/components.js';

type Properties = {
headerText: string;
subheader: React.ReactNode;
};

const FormHeader: React.FC<Properties> = ({ headerText, subheader }) => {
return (
<>
{/* TODO: Add logo */}
<h2 style={{ marginBottom: '50px' }}>LOGO</h2>
<Heading as="h1" color="white" mb="6px" fontSize="30px">
{headerText}
</Heading>
<Text mb="24px" fontSize="14px">
{subheader}
</Text>
</>
);
};

export { FormHeader };
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
IconButton,
Input,
InputGroup,
InputRightElement,
ViewIcon,
ViewOffIcon,
} from '~/bundles/common/components/components.js';
import { useCallback, useState } from '~/bundles/common/hooks/hooks.js';

type Properties = {
label: string;
name: string;
hasError: boolean;
};

const PasswordInput: React.FC<Properties> = ({ label, name, hasError }) => {
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);

const handlePasswordIconClick = useCallback((): void => {
setIsPasswordVisible(
(previousIsPasswordVisible) => !previousIsPasswordVisible,
);
}, []);

return (
<InputGroup size="md">
<Input
type={isPasswordVisible ? 'text' : 'password'}
label={label}
placeholder="••••••••"
name={name}
icon="right"
/>
<InputRightElement top="unset" bottom={hasError ? '25px' : 0}>
<IconButton
aria-label={
isPasswordVisible ? 'Hide password' : 'Show password'
}
icon={isPasswordVisible ? <ViewIcon /> : <ViewOffIcon />}
onClick={handlePasswordIconClick}
variant="ghostIcon"
/>
</InputRightElement>
</InputGroup>
);
};

export { PasswordInput };
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type UserSignInRequestDto } from '~/bundles/users/users.js';

const DEFAULT_SIGN_IN_PAYLOAD: UserSignInRequestDto = {
email: '',
password: '',
};

export { DEFAULT_SIGN_IN_PAYLOAD };
100 changes: 90 additions & 10 deletions frontend/src/bundles/auth/components/sign-in-form/sign-in-form.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,97 @@
import { Button, Heading } from '~/bundles/common/components/components.js';
import {
FormError,
FormHeader,
PasswordInput,
} from '~/bundles/auth/components/common/components.js';
import {
Box,
Button,
FormProvider,
Input,
Link,
VStack,
} from '~/bundles/common/components/components.js';
import {
AppRoute,
DataStatus,
UserValidationMessage,
} from '~/bundles/common/enums/enums.js';
import {
useAppForm,
useAppSelector,
useMemo,
} from '~/bundles/common/hooks/hooks.js';
import {
type UserSignInRequestDto,
userSignInValidationSchema,
} from '~/bundles/users/users.js';

import { DEFAULT_SIGN_IN_PAYLOAD } from './constants/constants.js';

type Properties = {
onSubmit: () => void;
onSubmit: (payload: UserSignInRequestDto) => void;
};

const SignInForm: React.FC<Properties> = () => (
<>
<Heading as="h1">Sign In</Heading>
const SignInForm: React.FC<Properties> = ({ onSubmit }) => {
const { dataStatus } = useAppSelector(({ auth }) => ({
dataStatus: auth.dataStatus,
}));
const form = useAppForm<UserSignInRequestDto>({
initialValues: DEFAULT_SIGN_IN_PAYLOAD,
validationSchema: userSignInValidationSchema,
onSubmit,
});

const { handleSubmit, errors, values } = form;

<form>
<Button label="Sign in" />
</form>
</>
);
const isEmpty = useMemo(
() => Object.values(values).some((value) => value.trim().length === 0),
[values],
);

return (
<FormProvider value={form}>
<Box w="55%" color="white">
<FormHeader
headerText="Sign In"
subheader={
<>
Don’t have an account?{' '}
<Link to={AppRoute.SIGN_UP} variant="secondary">
Go to registration
</Link>
</>
}
/>
<form onSubmit={handleSubmit}>
<VStack spacing="20px" align="flex-start">
<Input
type="text"
label="Email"
placeholder="[email protected]"
name="email"
/>
<PasswordInput
label="Password"
name="password"
hasError={Boolean(errors.password)}
/>
<FormError
isVisible={dataStatus === DataStatus.REJECTED}
message={UserValidationMessage.INVALID_DATA}
/>
<Button
type="submit"
label="Sign in"
size="lg"
sx={{ mt: '16px' }}
isDisabled={isEmpty}
/>
</VStack>
</form>
</Box>
</FormProvider>
);
};

export { SignInForm };
38 changes: 29 additions & 9 deletions frontend/src/bundles/auth/pages/auth.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { AppRoute } from '~/bundles/common/enums/enums.js';
import { Navigate } from 'react-router-dom';

import { Center, SimpleGrid } from '~/bundles/common/components/components.js';
import { AppRoute, DataStatus } from '~/bundles/common/enums/enums.js';
import {
useAppDispatch,
useAppSelector,
useCallback,
useLocation,
} from '~/bundles/common/hooks/hooks.js';
import { type UserSignUpRequestDto } from '~/bundles/users/users.js';
import {
type UserSignInRequestDto,
type UserSignUpRequestDto,
} from '~/bundles/users/users.js';

import { SignInForm, SignUpForm } from '../components/components.js';
import { actions as authActions } from '../store/auth.js';
Expand All @@ -17,9 +23,12 @@ const Auth: React.FC = () => {
}));
const { pathname } = useLocation();

const handleSignInSubmit = useCallback((): void => {
// handle sign in
}, []);
const handleSignInSubmit = useCallback(
(payload: UserSignInRequestDto): void => {
void dispatch(authActions.signIn(payload));
},
[dispatch],
);

const handleSignUpSubmit = useCallback(
(payload: UserSignUpRequestDto): void => {
Expand All @@ -28,6 +37,10 @@ const Auth: React.FC = () => {
[dispatch],
);

if (dataStatus === DataStatus.FULFILLED) {
return <Navigate to={AppRoute.ROOT} replace />;
}

const getScreen = (screen: string): React.ReactNode => {
switch (screen) {
case AppRoute.SIGN_IN: {
Expand All @@ -42,10 +55,17 @@ const Auth: React.FC = () => {
};

return (
<>
state: {dataStatus}
{getScreen(pathname)}
</>
<SimpleGrid columns={2} height="100vh">
{/* TODO: Replace with valid loader */}
{dataStatus === DataStatus.PENDING && (
<p style={{ position: 'absolute', top: 0, color: 'white' }}>
Loading...
</p>
)}
<Center bgColor="background.600">{getScreen(pathname)}</Center>
{/* TODO: Add logo */}
<Center bgColor="background.900">LOGO</Center>
</SimpleGrid>
);
};

Expand Down
Loading

0 comments on commit b1dee31

Please sign in to comment.