Skip to content

Commit

Permalink
Merge branch 'next' into task/OV-27-add-studio-page
Browse files Browse the repository at this point in the history
  • Loading branch information
Oleksandra Nedashkivska committed Aug 21, 2024
2 parents 2123895 + b1dee31 commit eff4222
Show file tree
Hide file tree
Showing 23 changed files with 311 additions and 35 deletions.
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
14 changes: 13 additions & 1 deletion frontend/src/bundles/auth/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@ import { createAsyncThunk } from '@reduxjs/toolkit';

import { type AsyncThunkConfig } from '~/bundles/common/types/types.js';
import {
type UserSignInRequestDto,
type UserSignInResponseDto,
type UserSignUpRequestDto,
type UserSignUpResponseDto,
} from '~/bundles/users/users.js';

import { name as sliceName } from './slice.js';

const signIn = createAsyncThunk<
UserSignInResponseDto,
UserSignInRequestDto,
AsyncThunkConfig
>(`${sliceName}/sign-in`, (signInPayload, { extra }) => {
const { authApi } = extra;

return authApi.signIn(signInPayload);
});

const signUp = createAsyncThunk<
UserSignUpResponseDto,
UserSignUpRequestDto,
Expand All @@ -18,4 +30,4 @@ const signUp = createAsyncThunk<
return authApi.signUp(registerPayload);
});

export { signUp };
export { signIn, signUp };
3 changes: 2 additions & 1 deletion frontend/src/bundles/auth/store/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { signUp } from './actions.js';
import { signIn, signUp } from './actions.js';
import { actions } from './slice.js';

const allActions = {
...actions,
signIn,
signUp,
};

Expand Down
16 changes: 15 additions & 1 deletion frontend/src/bundles/auth/store/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,36 @@ import { createSlice } from '@reduxjs/toolkit';

import { DataStatus } from '~/bundles/common/enums/enums.js';
import { type ValueOf } from '~/bundles/common/types/types.js';
import { type UserSignInResponseDto } from '~/bundles/users/users.js';

import { signUp } from './actions.js';
import { signIn, signUp } from './actions.js';

type State = {
dataStatus: ValueOf<typeof DataStatus>;
user: UserSignInResponseDto | null;
};

const initialState: State = {
dataStatus: DataStatus.IDLE,
user: null,
};

const { reducer, actions, name } = createSlice({
initialState,
name: 'auth',
reducers: {},
extraReducers(builder) {
builder.addCase(signIn.pending, (state) => {
state.dataStatus = DataStatus.PENDING;
});
builder.addCase(signIn.fulfilled, (state, action) => {
state.user = action.payload;
state.dataStatus = DataStatus.FULFILLED;
});
builder.addCase(signIn.rejected, (state) => {
state.user = null;
state.dataStatus = DataStatus.REJECTED;
});
builder.addCase(signUp.pending, (state) => {
state.dataStatus = DataStatus.PENDING;
});
Expand Down
Loading

0 comments on commit eff4222

Please sign in to comment.