Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OV-1: Sign In flow #15

Merged
merged 22 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading