diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 000000000..e3d60a813 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,17 @@ +name: Task +description: Abstract task description +title: 'TASK:' +projects: 'BinaryStudioAcademy/30' +body: + - type: textarea + id: what-feature + attributes: + label: What task? + placeholder: Add descriptions + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Add screenshots + placeholder: Add screenshots, mockups, etc. diff --git a/backend/package.json b/backend/package.json index 084b4118d..62c259d96 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,9 @@ "scripts": { "lint:type": "npx tsc --noEmit", "lint:js": "npx eslint \"src/**/*.ts\"", + "lint:js:fix": "npx eslint --fix \"src/**/*.ts\"", "lint": "npm run lint:type && npm run lint:js", + "lint:fix": "npm run lint:type && npm run lint:js:fix", "start:dev": "nodemon --exec tsx src/index.ts", "migrate:dev": "node --loader ts-paths-esm-loader ../node_modules/knex/bin/cli.js migrate:latest", "migrate:dev:make": "node --loader ts-paths-esm-loader ../node_modules/knex/bin/cli.js migrate:make -x ts", diff --git a/backend/src/bundles/auth/auth.controller.ts b/backend/src/bundles/auth/auth.controller.ts index 88c59ce2f..aa3309522 100644 --- a/backend/src/bundles/auth/auth.controller.ts +++ b/backend/src/bundles/auth/auth.controller.ts @@ -1,4 +1,8 @@ -import { type UserSignUpRequestDto } from '~/bundles/users/users.js'; +import { + type UserSignInRequestDto, + type UserSignUpRequestDto, + userSignInValidationSchema, +} from '~/bundles/users/users.js'; import { userSignUpValidationSchema } from '~/bundles/users/users.js'; import { type ApiHandlerOptions, @@ -20,6 +24,20 @@ class AuthController extends BaseController { this.authService = authService; + this.addRoute({ + path: AuthApiPath.SIGN_IN, + method: 'POST', + validation: { + body: userSignInValidationSchema, + }, + handler: (options) => + this.signIn( + options as ApiHandlerOptions<{ + body: UserSignInRequestDto; + }>, + ), + }); + this.addRoute({ path: AuthApiPath.SIGN_UP, method: 'POST', @@ -35,6 +53,55 @@ class AuthController extends BaseController { }); } + /** + * @swagger + * /auth/sign-in: + * post: + * description: Sign in 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: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * 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; + }>, + ): Promise { + return { + payload: await this.authService.signIn(options.body), + status: HttpCode.OK, + }; + } + /** * @swagger * /auth/sign-up: @@ -67,6 +134,7 @@ class AuthController extends BaseController { * 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 5ae168e63..fabf0d934 100644 --- a/backend/src/bundles/auth/auth.service.ts +++ b/backend/src/bundles/auth/auth.service.ts @@ -3,6 +3,14 @@ import { type UserSignUpResponseDto, } from '~/bundles/users/types/types.js'; import { type UserService } from '~/bundles/users/user.service.js'; +import { + type UserSignInRequestDto, + type UserSignInResponseDto, +} from '~/bundles/users/users.js'; +import { HttpCode, HttpError } from '~/common/http/http.js'; +import { cryptService } from '~/common/services/services.js'; + +import { UserValidationMessage } from './enums/enums.js'; class AuthService { private userService: UserService; @@ -11,6 +19,36 @@ class AuthService { this.userService = userService; } + public async signIn( + userRequestDto: UserSignInRequestDto, + ): Promise { + const { email, password } = userRequestDto; + const user = await this.userService.findByEmail(email); + + if (!user) { + throw new HttpError({ + message: UserValidationMessage.WRONG_CREDENTIALS, + status: HttpCode.BAD_REQUEST, + }); + } + + const { passwordHash } = user.toNewObject(); + + const isPwdCorrect = cryptService.compareSyncPassword( + password, + passwordHash, + ); + + if (!isPwdCorrect) { + throw new HttpError({ + message: UserValidationMessage.WRONG_CREDENTIALS, + status: HttpCode.BAD_REQUEST, + }); + } + + return user.toObject(); + } + public signUp( userRequestDto: UserSignUpRequestDto, ): Promise { diff --git a/backend/src/bundles/auth/enums/enums.ts b/backend/src/bundles/auth/enums/enums.ts index 7cbd1669d..e208cd7f4 100644 --- a/backend/src/bundles/auth/enums/enums.ts +++ b/backend/src/bundles/auth/enums/enums.ts @@ -1 +1 @@ -export { AuthApiPath } from 'shared'; +export { AuthApiPath, UserValidationMessage } from 'shared'; diff --git a/backend/src/bundles/users/types/types.ts b/backend/src/bundles/users/types/types.ts index f44313789..6a3620c41 100644 --- a/backend/src/bundles/users/types/types.ts +++ b/backend/src/bundles/users/types/types.ts @@ -1,5 +1,7 @@ export { type UserGetAllResponseDto, + type UserSignInRequestDto, + type UserSignInResponseDto, type UserSignUpRequestDto, type UserSignUpResponseDto, } from 'shared'; diff --git a/backend/src/bundles/users/user.repository.ts b/backend/src/bundles/users/user.repository.ts index 79b4399d8..359af6884 100644 --- a/backend/src/bundles/users/user.repository.ts +++ b/backend/src/bundles/users/user.repository.ts @@ -13,6 +13,12 @@ class UserRepository implements Repository { return Promise.resolve(null); } + public async findByEmail(email: string): Promise { + const user = await this.userModel.query().findOne({ email }).execute(); + + return user ? UserEntity.initialize(user) : null; + } + public async findAll(): Promise { const users = await this.userModel.query().execute(); diff --git a/backend/src/bundles/users/user.service.ts b/backend/src/bundles/users/user.service.ts index afc46dbb0..a101fcacf 100644 --- a/backend/src/bundles/users/user.service.ts +++ b/backend/src/bundles/users/user.service.ts @@ -20,6 +20,10 @@ class UserService implements Service { return Promise.resolve(null); } + public async findByEmail(email: string): Promise { + return await this.userRepository.findByEmail(email); + } + public async findAll(): Promise { const items = await this.userRepository.findAll(); diff --git a/backend/src/bundles/users/users.ts b/backend/src/bundles/users/users.ts index 1ad8ad701..73f640e85 100644 --- a/backend/src/bundles/users/users.ts +++ b/backend/src/bundles/users/users.ts @@ -11,8 +11,13 @@ const userController = new UserController(logger, userService); export { userController, userService }; export { + type UserSignInRequestDto, + type UserSignInResponseDto, type UserSignUpRequestDto, type UserSignUpResponseDto, } from './types/types.js'; export { UserModel } from './user.model.js'; -export { userSignUpValidationSchema } from './validation-schemas/validation-schemas.js'; +export { + userSignInValidationSchema, + userSignUpValidationSchema, +} from './validation-schemas/validation-schemas.js'; diff --git a/backend/src/bundles/users/validation-schemas/validation-schemas.ts b/backend/src/bundles/users/validation-schemas/validation-schemas.ts index 7bc9a09c5..5952fe0cf 100644 --- a/backend/src/bundles/users/validation-schemas/validation-schemas.ts +++ b/backend/src/bundles/users/validation-schemas/validation-schemas.ts @@ -1 +1 @@ -export { userSignUpValidationSchema } from 'shared'; +export { userSignInValidationSchema, userSignUpValidationSchema } from 'shared'; 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..4cbc00ae9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,8 +9,10 @@ "scripts": { "lint:css": "npx stylelint \"src/**/*.scss\" --aei", "lint:js": "npx eslint \"src/**/*.{ts,tsx}\"", + "lint:js:fix": "npx eslint --fix \"src/**/*.{ts,tsx}\"", "lint:type": "npx tsc --noEmit", "lint": "npm run lint:type && npm run lint:js", + "lint:fix": "npm run lint:type && npm run lint:js:fix", "start:dev": "vite", "build": "tsc -p tsconfig.build.json && vite build", "preview": "vite preview" @@ -27,16 +29,21 @@ "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", + "@remotion/player": "4.0.201", "formik": "2.4.6", "framer-motion": "11.3.24", "react": "18.3.1", "react-dom": "18.3.1", "react-redux": "9.1.2", "react-router-dom": "6.26.0", + "remotion": "4.0.201", "shared": "*", "zod-formik-adapter": "1.3.0" } 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 ( <> - logo - -
    -
  • - 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; -
-
+ + + + ); +}; + +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..7e9dd441d 100644 --- a/frontend/src/bundles/common/enums/app-route.enum.ts +++ b/frontend/src/bundles/common/enums/app-route.enum.ts @@ -2,6 +2,8 @@ const AppRoute = { ROOT: '/', SIGN_IN: '/sign-in', SIGN_UP: '/sign-up', + STUDIO: '/studio', + ANY: '*', } 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/common/middlewares/error-handling.middleware.ts b/frontend/src/bundles/common/middlewares/error-handling.middleware.ts new file mode 100644 index 000000000..a6f150fae --- /dev/null +++ b/frontend/src/bundles/common/middlewares/error-handling.middleware.ts @@ -0,0 +1,39 @@ +import { + type Middleware, + isRejected, + isRejectedWithValue, +} from '@reduxjs/toolkit'; +import { type ServerValidationErrorResponse } from 'shared'; + +import { notificationService } from '../services/services.js'; + +const notificationId = 'redux-store-error'; + +const errorMiddleware: Middleware = () => { + return (next) => (action) => { + let message: string = ''; + if (isRejectedWithValue(action)) { + message += JSON.stringify(action.payload); + } else if (isRejected(action)) { + const error = action.error as ServerValidationErrorResponse; + message += `${error.message}\n`; + if (error.details) { + for (const errorDetail of error.details) { + message += `\t- ${errorDetail.message}\n`; + } + } + } + + if (message && !notificationService.isActive(notificationId)) { + notificationService.error({ + message, + id: notificationId, + title: 'An error occurred.', + }); + } + + return next(action); + }; +}; + +export { errorMiddleware }; diff --git a/frontend/src/bundles/common/middlewares/middlewares.ts b/frontend/src/bundles/common/middlewares/middlewares.ts new file mode 100644 index 000000000..6f713a537 --- /dev/null +++ b/frontend/src/bundles/common/middlewares/middlewares.ts @@ -0,0 +1 @@ +export { errorMiddleware } from './error-handling.middleware.js'; diff --git a/frontend/src/bundles/common/services/notification/notification.service.ts b/frontend/src/bundles/common/services/notification/notification.service.ts new file mode 100644 index 000000000..8c5269ed0 --- /dev/null +++ b/frontend/src/bundles/common/services/notification/notification.service.ts @@ -0,0 +1,91 @@ +import { type createStandaloneToast } from '@chakra-ui/react'; + +type Constructor = { + toast: ReturnType['toast']; +}; + +type NotifyProperties = { + message: string; + id: string; + title: string; + status?: 'info' | 'warning' | 'success' | 'error' | 'loading'; +}; + +class NotificationService { + private toast: ReturnType['toast']; + + public constructor({ toast }: Constructor) { + this.toast = toast; + } + + public warn = ({ message, id, title }: NotifyProperties): void => { + this.showToastNotification({ + message, + id, + title, + status: 'warning', + }); + }; + + public loading = ({ message, id, title }: NotifyProperties): void => { + this.showToastNotification({ + message, + id, + title, + status: 'loading', + }); + }; + + public error = ({ message, id, title }: NotifyProperties): void => { + this.showToastNotification({ + message, + id, + title, + status: 'error', + }); + }; + + public success = ({ message, id, title }: NotifyProperties): void => { + this.showToastNotification({ + message, + id, + title, + status: 'success', + }); + }; + + public info = ({ message, id, title }: NotifyProperties): void => { + this.showToastNotification({ + message, + id, + title, + status: 'info', + }); + }; + + public isActive = (id: string): boolean => { + return this.toast.isActive(id); + }; + + private showToastNotification = ({ + message, + id, + title, + status, + }: NotifyProperties): void => { + if (status) { + this.toast({ + id, + title, + description: message, + status, + duration: 7000, + isClosable: true, + position: 'top-right', + variant: 'solid', + }); + } + }; +} + +export { NotificationService }; diff --git a/frontend/src/bundles/common/services/notification/notification.ts b/frontend/src/bundles/common/services/notification/notification.ts new file mode 100644 index 000000000..fa7804cc1 --- /dev/null +++ b/frontend/src/bundles/common/services/notification/notification.ts @@ -0,0 +1,12 @@ +import { createStandaloneToast } from '@chakra-ui/react'; + +import { theme } from '~/framework/theme/theme.js'; + +import { NotificationService } from './notification.service.js'; + +const { toast } = createStandaloneToast({ theme: theme }); + +const notificationService = new NotificationService({ toast }); + +export { NotificationService } from './notification.service.js'; +export { notificationService }; diff --git a/frontend/src/bundles/common/services/services.ts b/frontend/src/bundles/common/services/services.ts new file mode 100644 index 000000000..e90e2c714 --- /dev/null +++ b/frontend/src/bundles/common/services/services.ts @@ -0,0 +1,4 @@ +export { + type NotificationService, + notificationService, +} from './notification/notification.js'; 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/components/components.ts b/frontend/src/bundles/users/components/components.ts new file mode 100644 index 000000000..dcbd2c9a1 --- /dev/null +++ b/frontend/src/bundles/users/components/components.ts @@ -0,0 +1 @@ +export { UserCard } from './user-card/user-card.js'; diff --git a/frontend/src/bundles/users/components/user-card/user-card.tsx b/frontend/src/bundles/users/components/user-card/user-card.tsx new file mode 100644 index 000000000..1d340aa6c --- /dev/null +++ b/frontend/src/bundles/users/components/user-card/user-card.tsx @@ -0,0 +1,32 @@ +import { + Button, + Circle, + Flex, + Text, + VStack, +} from '~/bundles/common/components/components.js'; + +const UserCard: React.FC = () => ( + + + {/* TODO: replace Circle and Text content with dynamic values */} + + FN + + Firstname Lastname + +