diff --git a/frontend/src/app/app.tsx b/frontend/src/app/app.tsx
index e1f671b35..54a2eff7f 100644
--- a/frontend/src/app/app.tsx
+++ b/frontend/src/app/app.tsx
@@ -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 (
<>
-
-
-
- -
- Root
-
- -
- Sign in
-
- -
- Sign up
-
-
- Current path: {pathname}
-
-
-
-
- {isRoot && (
- <>
- Users:
- Status: {dataStatus}
-
- {users.map((it) => (
- - {it.email}
- ))}
-
- >
- )}
+
>
);
};
diff --git a/frontend/src/bundles/auth/auth-api.ts b/frontend/src/bundles/auth/auth-api.ts
index a81074fa4..53c946f10 100644
--- a/frontend/src/bundles/auth/auth-api.ts
+++ b/frontend/src/bundles/auth/auth-api.ts
@@ -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';
@@ -20,6 +22,22 @@ class AuthApi extends BaseHttpApi {
super({ path: ApiPath.AUTH, baseUrl, http, storage });
}
+ public async signIn(
+ payload: UserSignInRequestDto,
+ ): Promise {
+ 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();
+ }
+
public async signUp(
payload: UserSignUpRequestDto,
): Promise {
diff --git a/frontend/src/bundles/auth/components/common/components.ts b/frontend/src/bundles/auth/components/common/components.ts
new file mode 100644
index 000000000..22726d9a9
--- /dev/null
+++ b/frontend/src/bundles/auth/components/common/components.ts
@@ -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';
diff --git a/frontend/src/bundles/auth/components/common/form-error/form-error.tsx b/frontend/src/bundles/auth/components/common/form-error/form-error.tsx
new file mode 100644
index 000000000..965799129
--- /dev/null
+++ b/frontend/src/bundles/auth/components/common/form-error/form-error.tsx
@@ -0,0 +1,19 @@
+import {
+ FormControl,
+ FormErrorMessage,
+} from '~/bundles/common/components/components.js';
+
+type Properties = {
+ isVisible: boolean;
+ message: string;
+};
+
+const FormError: React.FC = ({ isVisible, message }) => {
+ return (
+
+ {message}
+
+ );
+};
+
+export { FormError };
diff --git a/frontend/src/bundles/auth/components/common/form-header/form-header.tsx b/frontend/src/bundles/auth/components/common/form-header/form-header.tsx
new file mode 100644
index 000000000..47397c365
--- /dev/null
+++ b/frontend/src/bundles/auth/components/common/form-header/form-header.tsx
@@ -0,0 +1,23 @@
+import { Heading, Text } from '~/bundles/common/components/components.js';
+
+type Properties = {
+ headerText: string;
+ subheader: React.ReactNode;
+};
+
+const FormHeader: React.FC = ({ headerText, subheader }) => {
+ return (
+ <>
+ {/* TODO: Add logo */}
+ LOGO
+
+ {headerText}
+
+
+ {subheader}
+
+ >
+ );
+};
+
+export { FormHeader };
diff --git a/frontend/src/bundles/auth/components/common/password-input/password-input.tsx b/frontend/src/bundles/auth/components/common/password-input/password-input.tsx
new file mode 100644
index 000000000..57e8cf7ae
--- /dev/null
+++ b/frontend/src/bundles/auth/components/common/password-input/password-input.tsx
@@ -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 = ({ label, name, hasError }) => {
+ const [isPasswordVisible, setIsPasswordVisible] = useState(false);
+
+ const handlePasswordIconClick = useCallback((): void => {
+ setIsPasswordVisible(
+ (previousIsPasswordVisible) => !previousIsPasswordVisible,
+ );
+ }, []);
+
+ return (
+
+
+
+ : }
+ onClick={handlePasswordIconClick}
+ variant="ghostIcon"
+ />
+
+
+ );
+};
+
+export { PasswordInput };
diff --git a/frontend/src/bundles/auth/components/sign-in-form/constants/constants.ts b/frontend/src/bundles/auth/components/sign-in-form/constants/constants.ts
new file mode 100644
index 000000000..3e3264e83
--- /dev/null
+++ b/frontend/src/bundles/auth/components/sign-in-form/constants/constants.ts
@@ -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 };
diff --git a/frontend/src/bundles/auth/components/sign-in-form/sign-in-form.tsx b/frontend/src/bundles/auth/components/sign-in-form/sign-in-form.tsx
index bbcad3139..f3a73d1f5 100644
--- a/frontend/src/bundles/auth/components/sign-in-form/sign-in-form.tsx
+++ b/frontend/src/bundles/auth/components/sign-in-form/sign-in-form.tsx
@@ -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 = () => (
- <>
- Sign In
+const SignInForm: React.FC = ({ onSubmit }) => {
+ const { dataStatus } = useAppSelector(({ auth }) => ({
+ dataStatus: auth.dataStatus,
+ }));
+ const form = useAppForm({
+ initialValues: DEFAULT_SIGN_IN_PAYLOAD,
+ validationSchema: userSignInValidationSchema,
+ onSubmit,
+ });
+
+ const { handleSubmit, errors, values } = form;
-
- >
-);
+ const isEmpty = useMemo(
+ () => Object.values(values).some((value) => value.trim().length === 0),
+ [values],
+ );
+
+ return (
+
+
+
+ Don’t have an account?{' '}
+
+ Go to registration
+
+ >
+ }
+ />
+
+
+
+ );
+};
export { SignInForm };
diff --git a/frontend/src/bundles/auth/pages/auth.tsx b/frontend/src/bundles/auth/pages/auth.tsx
index 6592265c3..26f0ef42e 100644
--- a/frontend/src/bundles/auth/pages/auth.tsx
+++ b/frontend/src/bundles/auth/pages/auth.tsx
@@ -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';
@@ -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 => {
@@ -28,6 +37,10 @@ const Auth: React.FC = () => {
[dispatch],
);
+ if (dataStatus === DataStatus.FULFILLED) {
+ return ;
+ }
+
const getScreen = (screen: string): React.ReactNode => {
switch (screen) {
case AppRoute.SIGN_IN: {
@@ -42,10 +55,17 @@ const Auth: React.FC = () => {
};
return (
- <>
- state: {dataStatus}
- {getScreen(pathname)}
- >
+
+ {/* TODO: Replace with valid loader */}
+ {dataStatus === DataStatus.PENDING && (
+
+ Loading...
+
+ )}
+ {getScreen(pathname)}
+ {/* TODO: Add logo */}
+ LOGO
+
);
};
diff --git a/frontend/src/bundles/auth/store/actions.ts b/frontend/src/bundles/auth/store/actions.ts
index 267bbf82d..75f929b7f 100644
--- a/frontend/src/bundles/auth/store/actions.ts
+++ b/frontend/src/bundles/auth/store/actions.ts
@@ -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,
@@ -18,4 +30,4 @@ const signUp = createAsyncThunk<
return authApi.signUp(registerPayload);
});
-export { signUp };
+export { signIn, signUp };
diff --git a/frontend/src/bundles/auth/store/auth.ts b/frontend/src/bundles/auth/store/auth.ts
index b99cc3038..821d63396 100644
--- a/frontend/src/bundles/auth/store/auth.ts
+++ b/frontend/src/bundles/auth/store/auth.ts
@@ -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,
};
diff --git a/frontend/src/bundles/auth/store/slice.ts b/frontend/src/bundles/auth/store/slice.ts
index 746d4b107..52cf77b51 100644
--- a/frontend/src/bundles/auth/store/slice.ts
+++ b/frontend/src/bundles/auth/store/slice.ts
@@ -2,15 +2,18 @@ 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;
+ user: UserSignInResponseDto | null;
};
const initialState: State = {
dataStatus: DataStatus.IDLE,
+ user: null,
};
const { reducer, actions, name } = createSlice({
@@ -18,6 +21,17 @@ const { reducer, actions, name } = createSlice({
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;
});
diff --git a/frontend/src/bundles/common/components/button/button.tsx b/frontend/src/bundles/common/components/button/button.tsx
index fa9006438..7fb25ff6b 100644
--- a/frontend/src/bundles/common/components/button/button.tsx
+++ b/frontend/src/bundles/common/components/button/button.tsx
@@ -1,12 +1,30 @@
-import { Button as LibraryButton } from '@chakra-ui/react';
+import {
+ type SystemStyleObject,
+ Button as LibraryButton,
+} from '@chakra-ui/react';
type Properties = {
label: string;
type?: 'button' | 'submit';
+ size?: 'md' | 'lg';
+ isDisabled?: boolean;
+ sx?: SystemStyleObject;
};
-const Button: React.FC = ({ type = 'button', label }) => (
-
+const Button: React.FC = ({
+ label,
+ type = 'button',
+ size = 'md',
+ isDisabled = false,
+ sx = {},
+}) => (
+
{label}
);
diff --git a/frontend/src/bundles/common/components/components.ts b/frontend/src/bundles/common/components/components.ts
index 2c6def68e..9f1d96ca1 100644
--- a/frontend/src/bundles/common/components/components.ts
+++ b/frontend/src/bundles/common/components/components.ts
@@ -6,12 +6,20 @@ export { Loader } from './loader/loader.js';
export { Overlay } from './overlay/overlay.js';
export { RouterProvider } from './router-provider/router-provider.js';
export { VideoModal } from './video-modal/video-modal.js';
+export { ViewIcon, ViewOffIcon } from '@chakra-ui/icons';
export {
Box,
+ Center,
Circle,
ChakraProvider as ComponentsProvider,
Flex,
+ FormControl,
+ FormErrorMessage,
Heading,
+ IconButton,
+ InputGroup,
+ InputRightElement,
+ SimpleGrid,
Text,
VStack,
} from '@chakra-ui/react';
diff --git a/frontend/src/bundles/common/components/input/input.tsx b/frontend/src/bundles/common/components/input/input.tsx
index 571c3fe72..286cdfc75 100644
--- a/frontend/src/bundles/common/components/input/input.tsx
+++ b/frontend/src/bundles/common/components/input/input.tsx
@@ -14,6 +14,7 @@ type Properties = {
label: string;
name: FieldInputProps['name'];
placeholder?: string;
+ icon?: 'right' | 'none';
};
const Input = ({
@@ -21,6 +22,7 @@ const Input = ({
label,
name,
placeholder = '',
+ icon = 'none',
}: Properties): JSX.Element => {
const [field, meta] = useFormField({ name });
@@ -36,6 +38,7 @@ const Input = ({
type={type}
placeholder={placeholder}
error={error}
+ style={{ paddingRight: icon === 'right' ? '40px' : 0 }}
as={LibraryInput}
/>
{error}
diff --git a/frontend/src/bundles/common/components/link/link.tsx b/frontend/src/bundles/common/components/link/link.tsx
index 222ce9fd2..593734682 100644
--- a/frontend/src/bundles/common/components/link/link.tsx
+++ b/frontend/src/bundles/common/components/link/link.tsx
@@ -1,3 +1,4 @@
+import { Link as LibraryLink } from '@chakra-ui/react';
import { NavLink } from 'react-router-dom';
import { type AppRoute } from '~/bundles/common/enums/enums.js';
@@ -6,10 +7,13 @@ import { type ValueOf } from '~/bundles/common/types/types.js';
type Properties = {
to: ValueOf;
children: React.ReactNode;
+ variant?: 'primary' | 'secondary';
};
-const Link: React.FC = ({ children, to }) => (
- {children}
+const Link: React.FC = ({ children, to, variant = 'primary' }) => (
+
+ {children}
+
);
export { Link };
diff --git a/frontend/src/bundles/common/enums/enums.ts b/frontend/src/bundles/common/enums/enums.ts
index 0e65d3c5b..ef77ebca3 100644
--- a/frontend/src/bundles/common/enums/enums.ts
+++ b/frontend/src/bundles/common/enums/enums.ts
@@ -1,3 +1,9 @@
export { AppRoute } from './app-route.enum.js';
export { DataStatus } from './data-status.enum.js';
-export { ApiPath, AppEnvironment, ContentType, ServerErrorType } from 'shared';
+export {
+ ApiPath,
+ AppEnvironment,
+ ContentType,
+ ServerErrorType,
+ UserValidationMessage,
+} from 'shared';
diff --git a/frontend/src/bundles/users/types/types.ts b/frontend/src/bundles/users/types/types.ts
index fb8e20c76..66268a76e 100644
--- a/frontend/src/bundles/users/types/types.ts
+++ b/frontend/src/bundles/users/types/types.ts
@@ -1,6 +1,8 @@
export {
type UserGetAllItemResponseDto,
type UserGetAllResponseDto,
+ type UserSignInRequestDto,
+ type UserSignInResponseDto,
type UserSignUpRequestDto,
type UserSignUpResponseDto,
} from 'shared';
diff --git a/frontend/src/bundles/users/users.ts b/frontend/src/bundles/users/users.ts
index 443083e1c..1c0f3bf9d 100644
--- a/frontend/src/bundles/users/users.ts
+++ b/frontend/src/bundles/users/users.ts
@@ -14,7 +14,12 @@ export { userApi };
export {
type UserGetAllItemResponseDto,
type UserGetAllResponseDto,
+ type UserSignInRequestDto,
+ type UserSignInResponseDto,
type UserSignUpRequestDto,
type UserSignUpResponseDto,
} from './types/types.js';
-export { userSignUpValidationSchema } from './validation-schemas/validation-schemas.js';
+export {
+ userSignInValidationSchema,
+ userSignUpValidationSchema,
+} from './validation-schemas/validation-schemas.js';
diff --git a/frontend/src/bundles/users/validation-schemas/validation-schemas.ts b/frontend/src/bundles/users/validation-schemas/validation-schemas.ts
index 7bc9a09c5..5952fe0cf 100644
--- a/frontend/src/bundles/users/validation-schemas/validation-schemas.ts
+++ b/frontend/src/bundles/users/validation-schemas/validation-schemas.ts
@@ -1 +1 @@
-export { userSignUpValidationSchema } from 'shared';
+export { userSignInValidationSchema, userSignUpValidationSchema } from 'shared';
diff --git a/shared/src/bundles/users/enums/user-validation-message.enum.ts b/shared/src/bundles/users/enums/user-validation-message.enum.ts
index 1a168422a..3aa42e486 100644
--- a/shared/src/bundles/users/enums/user-validation-message.enum.ts
+++ b/shared/src/bundles/users/enums/user-validation-message.enum.ts
@@ -1,8 +1,8 @@
const UserValidationMessage = {
EMAIL_REQUIRE: 'Email is required',
EMAIL_WRONG: 'Email is wrong',
- EMAIL_INVALID: 'Please enter a valid email',
FIELD_REQUIRE: 'Please fill out this field',
+ EMAIL_INVALID: 'Please enter a valid email',
PASSWORD_LENGTH: 'Password must have from 6 to 12 characters',
INVALID_DATA: 'Incorrect email or password. Please try again.',
WRONG_CREDENTIALS: 'Email or password are incorrect',
diff --git a/shared/src/bundles/users/types/user-sign-in-request-dto.type.ts b/shared/src/bundles/users/types/user-sign-in-request-dto.type.ts
index 6d445fe1e..8098c64cf 100644
--- a/shared/src/bundles/users/types/user-sign-in-request-dto.type.ts
+++ b/shared/src/bundles/users/types/user-sign-in-request-dto.type.ts
@@ -1,5 +1,6 @@
-import { type UserSignUpRequestDto } from './types.js';
-
-type UserSignInRequestDto = Pick;
+type UserSignInRequestDto = {
+ email: string;
+ password: string;
+};
export { type UserSignInRequestDto };
diff --git a/shared/src/bundles/users/types/user-sign-in-response-dto.type.ts b/shared/src/bundles/users/types/user-sign-in-response-dto.type.ts
index 61be88ee0..afb72b351 100644
--- a/shared/src/bundles/users/types/user-sign-in-response-dto.type.ts
+++ b/shared/src/bundles/users/types/user-sign-in-response-dto.type.ts
@@ -1,5 +1,6 @@
-import { type UserSignUpResponseDto } from './user-sign-up-response-dto.type.js';
-
-type UserSignInResponseDto = UserSignUpResponseDto;
+type UserSignInResponseDto = {
+ id: number;
+ email: string;
+};
export { type UserSignInResponseDto };
diff --git a/shared/src/bundles/users/validation-schemas/user-sig-in.validation-schema.ts b/shared/src/bundles/users/validation-schemas/user-sign-in.validation-schema.ts
similarity index 100%
rename from shared/src/bundles/users/validation-schemas/user-sig-in.validation-schema.ts
rename to shared/src/bundles/users/validation-schemas/user-sign-in.validation-schema.ts
diff --git a/shared/src/bundles/users/validation-schemas/validation-schemas.ts b/shared/src/bundles/users/validation-schemas/validation-schemas.ts
index 58cd817cf..f6c85f13c 100644
--- a/shared/src/bundles/users/validation-schemas/validation-schemas.ts
+++ b/shared/src/bundles/users/validation-schemas/validation-schemas.ts
@@ -1,2 +1,2 @@
-export { userSignIn } from './user-sig-in.validation-schema.js';
+export { userSignIn } from './user-sign-in.validation-schema.js';
export { userSignUp } from './user-sign-up.validation-schema.js';