diff --git a/backend/src/bundles/auth/auth.controller.ts b/backend/src/bundles/auth/auth.controller.ts
index 148002532..f25c3d2d5 100644
--- a/backend/src/bundles/auth/auth.controller.ts
+++ b/backend/src/bundles/auth/auth.controller.ts
@@ -55,9 +55,9 @@ class AuthController extends BaseController {
/**
* @swagger
- * /auth/sign-up:
+ * /auth/sign-in:
* post:
- * description: Sign up user into the application
+ * description: Sign in user into the application
* requestBody:
* description: User auth data
* required: true
@@ -72,7 +72,7 @@ class AuthController extends BaseController {
* password:
* type: string
* responses:
- * 201:
+ * 200:
* description: Successful operation
* content:
* application/json:
@@ -82,7 +82,15 @@ class AuthController extends BaseController {
* message:
* type: object
* $ref: '#/components/schemas/User'
+ * 400:
+ * description: Failed operation
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * $ref: '#/components/schemas/Error'
*/
+
private async signIn(
options: ApiHandlerOptions<{
body: UserSignInRequestDto;
@@ -94,6 +102,38 @@ class AuthController extends BaseController {
};
}
+ /**
+ * @swagger
+ * /auth/sign-up:
+ * post:
+ * description: Sign up user into the application
+ * requestBody:
+ * description: User auth data
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * email:
+ * type: string
+ * format: email
+ * password:
+ * type: string
+ * responses:
+ * 201:
+ * description: Successful operation
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: object
+ * $ref: '#/components/schemas/User'
+ */
+
+
private async signUp(
options: ApiHandlerOptions<{
body: UserSignUpRequestDto;
diff --git a/backend/src/bundles/auth/auth.service.ts b/backend/src/bundles/auth/auth.service.ts
index fdcac92fc..24566df74 100644
--- a/backend/src/bundles/auth/auth.service.ts
+++ b/backend/src/bundles/auth/auth.service.ts
@@ -8,8 +8,10 @@ import {
type UserSignInResponseDto,
} from '~/bundles/users/users.js';
import { HttpCode, HttpError } from '~/common/http/http.js';
+
import { cryptService, tokenService } from '~/common/services/services.js';
+
import { UserValidationMessage } from './enums/enums.js';
class AuthService {
@@ -45,6 +47,7 @@ class AuthService {
status: HttpCode.BAD_REQUEST,
});
}
+
const id = user.toObject().id;
const token = await tokenService.createToken(id);
return { ...user.toObject(), token };
diff --git a/backend/src/common/server-application/base-server-app-api.ts b/backend/src/common/server-application/base-server-app-api.ts
index 6630eaebc..e86461e11 100644
--- a/backend/src/common/server-application/base-server-app-api.ts
+++ b/backend/src/common/server-application/base-server-app-api.ts
@@ -38,11 +38,27 @@ class BaseServerAppApi implements ServerAppApi {
definition: {
openapi: '3.0.0',
info: {
- title: 'Hello World',
+ title: 'OutreachVids API documentation',
version: `${this.version}.0.0`,
},
+ components: {
+ schemas: {
+ Error: {
+ type: 'object',
+ properties: {
+ errorType: {
+ type: 'string',
+ enum: ['COMMON', 'VALIDATION'],
+ },
+ message: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ },
},
- apis: [`src/packages/**/*.controller.${controllerExtension}`],
+ apis: [`src/bundles/**/*.controller.${controllerExtension}`],
});
}
}
diff --git a/frontend/package.json b/frontend/package.json
index 881769413..25983088a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -27,9 +27,12 @@
"vite": "5.4.0"
},
"dependencies": {
+ "@chakra-ui/icons": "2.1.1",
"@chakra-ui/react": "2.8.2",
"@emotion/react": "11.13.0",
"@emotion/styled": "11.13.0",
+ "@fortawesome/free-solid-svg-icons": "6.6.0",
+ "@fortawesome/react-fontawesome": "0.2.2",
"@reduxjs/toolkit": "2.2.7",
"formik": "2.4.6",
"framer-motion": "11.3.24",
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..69b312847 100644
--- a/frontend/src/bundles/common/components/button/button.tsx
+++ b/frontend/src/bundles/common/components/button/button.tsx
@@ -1,12 +1,33 @@
-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';
+ variant?: string;
+ size?: 'md' | 'lg';
+ isDisabled?: boolean;
+ sx?: SystemStyleObject;
};
-const Button: React.FC = ({ type = 'button', label }) => (
-
+const Button: React.FC = ({
+ label,
+ type = 'button',
+ variant = 'solid',
+ 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 71c774689..ab06c5910 100644
--- a/frontend/src/bundles/common/components/components.ts
+++ b/frontend/src/bundles/common/components/components.ts
@@ -1,15 +1,26 @@
export { Button } from './button/button.js';
+export { Header } from './header/header.js';
export { Input } from './input/input.js';
export { Link } from './link/link.js';
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 { DownloadIcon } from '@chakra-ui/icons';
+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/header/header.tsx b/frontend/src/bundles/common/components/header/header.tsx
new file mode 100644
index 000000000..7846ddee7
--- /dev/null
+++ b/frontend/src/bundles/common/components/header/header.tsx
@@ -0,0 +1,38 @@
+import { Box, Flex, Text } from '@chakra-ui/react';
+
+type Properties = {
+ left?: React.ReactNode;
+ center?: React.ReactNode;
+ right?: React.ReactNode;
+};
+
+const Header: React.FC = ({ left, center, right }) => {
+ return (
+
+ {left ?? (
+ // {/* TODO: Add logo */}
+
+ Logo
+
+ )}
+ {center}
+ {right}
+
+ );
+};
+
+export { Header };
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/components/video-modal/components/components.ts b/frontend/src/bundles/common/components/video-modal/components/components.ts
new file mode 100644
index 000000000..9ba9d1e9d
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/components/components.ts
@@ -0,0 +1 @@
+export { VideoModalContent } from './video-modal-content/video-modal-content.js';
diff --git a/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/components.ts b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/components.ts
new file mode 100644
index 000000000..d179bd19c
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/components.ts
@@ -0,0 +1,2 @@
+export { Tab } from './tab/tab.js';
+export { VideoPreview } from './video-preview/video-preview.js';
diff --git a/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/tab/tab.tsx b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/tab/tab.tsx
new file mode 100644
index 000000000..ea8761154
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/tab/tab.tsx
@@ -0,0 +1,29 @@
+import { Icon, Tab as ChakraTab } from '@chakra-ui/react';
+import { type IconDefinition } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+type Properties = {
+ label: string;
+ icon: IconDefinition;
+};
+
+const Tab = ({ label, icon }: Properties): JSX.Element => {
+ return (
+
+ {' '}
+ {label}
+
+ );
+};
+
+export { Tab };
diff --git a/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/enums/enums.ts b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/enums/enums.ts
new file mode 100644
index 000000000..9a2f32d26
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/enums/enums.ts
@@ -0,0 +1,2 @@
+export { VideoPreview } from './video-preview.enum.js';
+export { VideoSizeLabel } from './video-sizes.enum.js';
diff --git a/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/enums/video-preview.enum.ts b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/enums/video-preview.enum.ts
new file mode 100644
index 000000000..55e124bb8
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/enums/video-preview.enum.ts
@@ -0,0 +1,6 @@
+const VideoPreview = {
+ PORTRAIT: 'portrait',
+ LANDSCAPE: 'landscape',
+} as const;
+
+export { VideoPreview };
diff --git a/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/enums/video-sizes.enum.ts b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/enums/video-sizes.enum.ts
new file mode 100644
index 000000000..8be9ed814
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/enums/video-sizes.enum.ts
@@ -0,0 +1,6 @@
+const VideoSizeLabel = {
+ PORTRAIT: '1080 x 1920',
+ LANDSCAPE: '1920 x 1080',
+} as const;
+
+export { VideoSizeLabel };
diff --git a/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/types/types.ts b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/types/types.ts
new file mode 100644
index 000000000..be952c440
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/types/types.ts
@@ -0,0 +1 @@
+export { type VideoPreview } from './video-preview.type.js';
diff --git a/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/types/video-preview.type.ts b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/types/video-preview.type.ts
new file mode 100644
index 000000000..5ba129e47
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/libs/types/video-preview.type.ts
@@ -0,0 +1,3 @@
+type VideoPreview = 'portrait' | 'landscape';
+
+export { type VideoPreview };
diff --git a/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/video-preview.tsx b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/video-preview.tsx
new file mode 100644
index 000000000..c3a4a6308
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/components/video-preview/video-preview.tsx
@@ -0,0 +1,78 @@
+import { Button, Flex, Icon, Text } from '@chakra-ui/react';
+import { faPlay } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { useCallback, useState } from 'react';
+
+import {
+ VideoPreview as VideoPreviewValues,
+ VideoSizeLabel,
+} from './libs/enums/enums.js';
+import { type VideoPreview as VideoPreviewType } from './libs/types/types.js';
+
+const VideoPreview = (): JSX.Element => {
+ const [view, setView] = useState(
+ VideoPreviewValues.PORTRAIT,
+ );
+
+ const handleSetPortraitView = useCallback((): void => {
+ setView(VideoPreviewValues.PORTRAIT);
+ }, []);
+
+ const handleSetLandscapeView = useCallback((): void => {
+ setView(VideoPreviewValues.LANDSCAPE);
+ }, []);
+
+ return (
+
+
+
+
+
+ {view === VideoPreviewValues.PORTRAIT
+ ? VideoSizeLabel.PORTRAIT
+ : VideoSizeLabel.LANDSCAPE}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { VideoPreview };
diff --git a/frontend/src/bundles/common/components/video-modal/components/video-modal-content/video-modal-content.tsx b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/video-modal-content.tsx
new file mode 100644
index 000000000..737d6bb2e
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/components/video-modal-content/video-modal-content.tsx
@@ -0,0 +1,29 @@
+import { TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
+import { faPlay } from '@fortawesome/free-solid-svg-icons/faPlay';
+
+import { Tab, VideoPreview } from './components/components.js';
+
+const VideoModalContent = (): JSX.Element => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { VideoModalContent };
diff --git a/frontend/src/bundles/common/components/video-modal/video-modal.tsx b/frontend/src/bundles/common/components/video-modal/video-modal.tsx
new file mode 100644
index 000000000..dfcd36a13
--- /dev/null
+++ b/frontend/src/bundles/common/components/video-modal/video-modal.tsx
@@ -0,0 +1,44 @@
+import {
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+} from '@chakra-ui/react';
+
+import { VideoModalContent } from './components/components.js';
+
+type Properties = {
+ isOpen: boolean;
+ onModalClose: () => void;
+};
+
+const VideoModal = ({ isOpen, onModalClose }: Properties): JSX.Element => {
+ return (
+
+
+
+
+ Create video
+
+
+
+
+
+
+
+ );
+};
+
+export { VideoModal };
diff --git a/frontend/src/bundles/common/enums/app-route.enum.ts b/frontend/src/bundles/common/enums/app-route.enum.ts
index dfc5352ec..9d1c5334e 100644
--- a/frontend/src/bundles/common/enums/app-route.enum.ts
+++ b/frontend/src/bundles/common/enums/app-route.enum.ts
@@ -2,6 +2,7 @@ const AppRoute = {
ROOT: '/',
SIGN_IN: '/sign-in',
SIGN_UP: '/sign-up',
+ STUDIO: '/studio',
} as const;
export { AppRoute };
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/studio/pages/studio.tsx b/frontend/src/bundles/studio/pages/studio.tsx
new file mode 100644
index 000000000..fd0acd3d0
--- /dev/null
+++ b/frontend/src/bundles/studio/pages/studio.tsx
@@ -0,0 +1,31 @@
+import {
+ Button,
+ DownloadIcon,
+ Header,
+ IconButton,
+} from '~/bundles/common/components/components.js';
+
+const Studio: React.FC = () => {
+ return (
+ <>
+
+ }
+ right={
+ }
+ />
+ }
+ />
+ >
+ );
+};
+
+export { Studio };
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/frontend/src/framework/theme/styles/colors.styles.ts b/frontend/src/framework/theme/styles/colors.styles.ts
index 10ec09c65..8ae8d0cce 100644
--- a/frontend/src/framework/theme/styles/colors.styles.ts
+++ b/frontend/src/framework/theme/styles/colors.styles.ts
@@ -7,9 +7,8 @@ const colors = {
50: '#e2e1ec',
},
brand: {
- 900: '#1a365d',
- 200: '#b3e0ff',
secondary: {
+ 50: '#3BAF28',
300: '#ff6e1c',
600: '#eb5500',
900: '#e13b00',
diff --git a/frontend/src/framework/theme/styles/components.styles.ts b/frontend/src/framework/theme/styles/components.styles.ts
index 417d40ef9..070571443 100644
--- a/frontend/src/framework/theme/styles/components.styles.ts
+++ b/frontend/src/framework/theme/styles/components.styles.ts
@@ -3,7 +3,55 @@ import { colors } from './colors.styles.js';
const components = {
Heading: {
baseStyle: {
- color: colors.text.accent,
+ color: colors.white,
+ fontFamily: 'Poppins, sans-serif',
+ textAlign: 'left',
+ },
+ variants: {
+ H1: {
+ fontSize: '54px',
+ fontWeight: '600',
+ lineHeight: '64px',
+ },
+ H2: {
+ fontSize: '30px',
+ fontWeight: '600',
+ lineHeight: '32px',
+ },
+ H3: {
+ fontSize: '18px',
+ fontWeight: '700',
+ lineHeight: '27px',
+ },
+ },
+ },
+ Text: {
+ baseStyle: {
+ color: colors.white,
+ fontFamily: 'Poppins, sans-serif',
+ textAlign: 'left',
+ },
+ variants: {
+ body1: {
+ fontSize: '16px',
+ fontWeight: '500',
+ lineHeight: '22px',
+ },
+ bodySmall: {
+ fontSize: '14px',
+ fontWeight: '400',
+ lineHeight: '20px',
+ },
+ caption: {
+ fontSize: '12px',
+ fontWeight: '400',
+ lineHeight: '16px',
+ },
+ button: {
+ fontSize: '14px',
+ fontWeight: '600',
+ lineHeight: '16px',
+ },
},
},
Button: {
@@ -18,6 +66,19 @@ const components = {
},
},
},
+ primaryOutlined: {
+ color: colors.background[300],
+ border: '1px solid',
+ borderColor: colors.background[300],
+ _hover: {
+ color: 'white',
+ bg: colors.background[300],
+ _disabled: {
+ color: colors.background[300],
+ bg: 'none',
+ },
+ },
+ },
ghostIcon: {
color: colors.white,
_hover: {
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 1412d521b..901c2d1de 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -9,6 +9,7 @@ import {
StoreProvider,
} from '~/bundles/common/components/components.js';
import { AppRoute } from '~/bundles/common/enums/enums.js';
+import { Studio } from '~/bundles/studio/pages/studio.js';
import { store } from '~/framework/store/store.js';
import { theme } from '~/framework/theme/theme.js';
@@ -29,6 +30,10 @@ const routes = [
path: AppRoute.SIGN_UP,
element: ,
},
+ {
+ path: AppRoute.STUDIO,
+ element: ,
+ },
],
},
];
diff --git a/package-lock.json b/package-lock.json
index 52258ca3a..c07253538 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -72,9 +72,12 @@
"frontend": {
"version": "1.0.0",
"dependencies": {
+ "@chakra-ui/icons": "2.1.1",
"@chakra-ui/react": "2.8.2",
"@emotion/react": "11.13.0",
"@emotion/styled": "11.13.0",
+ "@fortawesome/free-solid-svg-icons": "6.6.0",
+ "@fortawesome/react-fontawesome": "0.2.2",
"@reduxjs/toolkit": "2.2.7",
"formik": "2.4.6",
"framer-motion": "11.3.24",
@@ -804,6 +807,18 @@
"react": ">=18"
}
},
+ "node_modules/@chakra-ui/icons": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/icons/-/icons-2.1.1.tgz",
+ "integrity": "sha512-3p30hdo4LlRZTT5CwoAJq3G9fHI0wDc0pBaMHj4SUn0yomO+RcDRlzhdXqdr5cVnzax44sqXJVnf3oQG0eI+4g==",
+ "dependencies": {
+ "@chakra-ui/icon": "3.2.0"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
"node_modules/@chakra-ui/image": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-2.1.0.tgz",
@@ -2787,6 +2802,49 @@
"yaml": "^2.2.2"
}
},
+ "node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
+ "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
+ "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
+ "peer": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-solid-svg-icons": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
+ "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/react-fontawesome": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
+ "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "~1 || ~6",
+ "react": ">=16.3"
+ }
+ },
"node_modules/@gitbeaker/core": {
"version": "38.12.1",
"resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-38.12.1.tgz",
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..c821289e6 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,10 @@
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..f9e076841 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,8 @@
-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..b768f4b64 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,9 @@
-import { type UserSignUpResponseDto } from './user-sign-up-response-dto.type.js';
-type UserSignInResponseDto = UserSignUpResponseDto;
+type UserSignInResponseDto = {
+ id: number;
+ email: string;
+ token: string;
+};
+
export { type UserSignInResponseDto };
diff --git a/shared/src/bundles/users/validation-schemas/user-sign-in.validation-schema.ts b/shared/src/bundles/users/validation-schemas/user-sign-in.validation-schema.ts
new file mode 100644
index 000000000..100cb3dad
--- /dev/null
+++ b/shared/src/bundles/users/validation-schemas/user-sign-in.validation-schema.ts
@@ -0,0 +1,36 @@
+import { z } from 'zod';
+
+import { UserValidationMessage, UserValidationRule } from '../enums/enums.js';
+
+type UserSignInRequestValidationDto = {
+ email: z.ZodString;
+ password: z.ZodString;
+};
+
+const userSignIn = z
+ .object({
+ email: z
+ .string({ required_error: UserValidationMessage.FIELD_REQUIRE })
+ .trim()
+ .min(UserValidationRule.EMAIL_MINIMUM_LENGTH, {
+ message: UserValidationMessage.EMAIL_INVALID,
+ })
+ .max(UserValidationRule.EMAIL_MAXIMUM_LENGTH, {
+ message: UserValidationMessage.EMAIL_INVALID,
+ })
+ .email({
+ message: UserValidationMessage.EMAIL_INVALID,
+ }),
+ password: z
+ .string({ required_error: UserValidationMessage.FIELD_REQUIRE })
+ .trim()
+ .min(UserValidationRule.PASSWORD_MINIMUM_LENGTH, {
+ message: UserValidationMessage.PASSWORD_LENGTH,
+ })
+ .max(UserValidationRule.PASSWORD_MAXIMUM_LENGTH, {
+ message: UserValidationMessage.PASSWORD_LENGTH,
+ }),
+ })
+ .required();
+
+export { userSignIn };
diff --git a/shared/src/bundles/users/validation-schemas/validation-schemas.ts b/shared/src/bundles/users/validation-schemas/validation-schemas.ts
index 58cd817cf..3e53438d4 100644
--- a/shared/src/bundles/users/validation-schemas/validation-schemas.ts
+++ b/shared/src/bundles/users/validation-schemas/validation-schemas.ts
@@ -1,2 +1,4 @@
-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';